diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3eefea21..0e85eb8e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -35,11 +35,15 @@ jobs: python-version: '3.8' architecture: 'x64' - run: python -m pip install --upgrade pip setuptools jsonschema - - run: pip install -e .[pylint,pycodestyle,pyflakes] + # If we don't install pycodestyle, pylint will throw an unused-argument error in pylsp/plugins/pycodestyle_lint.py:72 + # This error cannot be resolved by adding a pylint: disable=unused-argument comment ... + - run: | + pip install -e .[pylint,pycodestyle,pyflakes] + pip install black - name: Pylint checks run: pylint pylsp test - - name: Code style checks - run: pycodestyle pylsp test + - name: Code style checks with black + run: black --check pylsp test - name: Pyflakes checks run: pyflakes pylsp test - name: Validate JSON schema diff --git a/pylsp/__init__.py b/pylsp/__init__.py index 151dddc0..eeb80bdc 100644 --- a/pylsp/__init__.py +++ b/pylsp/__init__.py @@ -22,8 +22,8 @@ def convert_version_info(version: str) -> (int, ..., str): _version.VERSION_INFO = convert_version_info(__version__) -PYLSP = 'pylsp' -IS_WIN = os.name == 'nt' +PYLSP = "pylsp" +IS_WIN = os.name == "nt" hookspec = pluggy.HookspecMarker(PYLSP) hookimpl = pluggy.HookimplMarker(PYLSP) diff --git a/pylsp/__main__.py b/pylsp/__main__.py index 50950a30..d6691740 100644 --- a/pylsp/__main__.py +++ b/pylsp/__main__.py @@ -12,58 +12,58 @@ except Exception: # pylint: disable=broad-except import json -from .python_lsp import (PythonLSPServer, start_io_lang_server, - start_tcp_lang_server, start_ws_lang_server) +from .python_lsp import ( + PythonLSPServer, + start_io_lang_server, + start_tcp_lang_server, + start_ws_lang_server, +) from ._version import __version__ LOG_FORMAT = "%(asctime)s {0} - %(levelname)s - %(name)s - %(message)s".format( - time.localtime().tm_zone) + time.localtime().tm_zone +) def add_arguments(parser): parser.description = "Python Language Server" parser.add_argument( - "--tcp", action="store_true", - help="Use TCP server instead of stdio" + "--tcp", action="store_true", help="Use TCP server instead of stdio" ) parser.add_argument( - "--ws", action="store_true", - help="Use Web Sockets server instead of stdio" + "--ws", action="store_true", help="Use Web Sockets server instead of stdio" ) + parser.add_argument("--host", default="127.0.0.1", help="Bind to this address") + parser.add_argument("--port", type=int, default=2087, help="Bind to this port") parser.add_argument( - "--host", default="127.0.0.1", - help="Bind to this address" - ) - parser.add_argument( - "--port", type=int, default=2087, - help="Bind to this port" - ) - parser.add_argument( - '--check-parent-process', action="store_true", + "--check-parent-process", + action="store_true", help="Check whether parent process is still alive using os.kill(ppid, 0) " "and auto shut down language server process when parent process is not alive." - "Note that this may not work on a Windows machine." + "Note that this may not work on a Windows machine.", ) log_group = parser.add_mutually_exclusive_group() log_group.add_argument( - "--log-config", - help="Path to a JSON file containing Python logging config." + "--log-config", help="Path to a JSON file containing Python logging config." ) log_group.add_argument( "--log-file", help="Redirect logs to the given file instead of writing to stderr." - "Has no effect if used with --log-config." + "Has no effect if used with --log-config.", ) parser.add_argument( - '-v', '--verbose', action='count', default=0, - help="Increase verbosity of log output, overrides log config file" + "-v", + "--verbose", + action="count", + default=0, + help="Increase verbosity of log output, overrides log config file", ) parser.add_argument( - '-V', '--version', action='version', version='%(prog)s v' + __version__ + "-V", "--version", action="version", version="%(prog)s v" + __version__ ) @@ -74,15 +74,14 @@ def main(): _configure_logger(args.verbose, args.log_config, args.log_file) if args.tcp: - start_tcp_lang_server(args.host, args.port, args.check_parent_process, - PythonLSPServer) + start_tcp_lang_server( + args.host, args.port, args.check_parent_process, PythonLSPServer + ) elif args.ws: - start_ws_lang_server(args.port, args.check_parent_process, - PythonLSPServer) + start_ws_lang_server(args.port, args.check_parent_process, PythonLSPServer) else: stdin, stdout = _binary_stdio() - start_io_lang_server(stdin, stdout, args.check_parent_process, - PythonLSPServer) + start_io_lang_server(stdin, stdout, args.check_parent_process, PythonLSPServer) def _binary_stdio(): @@ -99,14 +98,18 @@ def _configure_logger(verbose=0, log_config=None, log_file=None): root_logger = logging.root if log_config: - with open(log_config, 'r', encoding='utf-8') as f: + with open(log_config, "r", encoding="utf-8") as f: logging.config.dictConfig(json.load(f)) else: formatter = logging.Formatter(LOG_FORMAT) if log_file: log_handler = logging.handlers.RotatingFileHandler( - log_file, mode='a', maxBytes=50*1024*1024, - backupCount=10, encoding=None, delay=0 + log_file, + mode="a", + maxBytes=50 * 1024 * 1024, + backupCount=10, + encoding=None, + delay=0, ) else: log_handler = logging.StreamHandler() @@ -123,5 +126,5 @@ def _configure_logger(verbose=0, log_config=None, log_file=None): root_logger.setLevel(level) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 610cdbde..644a00e0 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -17,7 +17,7 @@ # Eol chars accepted by the LSP protocol # the ordering affects performance -EOL_CHARS = ['\r\n', '\r', '\n'] +EOL_CHARS = ["\r\n", "\r", "\n"] EOL_REGEX = re.compile(f'({"|".join(EOL_CHARS)})') log = logging.getLogger(__name__) @@ -25,6 +25,7 @@ def debounce(interval_s, keyed_by=None): """Debounce calls to this function until interval_s seconds have passed.""" + def wrapper(func): timers = {} lock = threading.Lock() @@ -48,7 +49,9 @@ def run(): timer = threading.Timer(interval_s, run) timers[key] = timer timer.start() + return debounced + return wrapper @@ -78,7 +81,9 @@ def find_parents(root, path, names): # Search each of /a/b/c, /a/b, /a while dirs: search_dir = os.path.join(*dirs) - existing = list(filter(os.path.exists, [os.path.join(search_dir, n) for n in names])) + existing = list( + filter(os.path.exists, [os.path.join(search_dir, n) for n in names]) + ) if existing: return existing dirs.pop() @@ -92,11 +97,11 @@ def path_to_dot_name(path): directory = os.path.dirname(path) module_name, _ = os.path.splitext(os.path.basename(path)) full_name = [module_name] - while os.path.exists(os.path.join(directory, '__init__.py')): + while os.path.exists(os.path.join(directory, "__init__.py")): this_directory = os.path.basename(directory) directory = os.path.dirname(directory) full_name = [this_directory] + full_name - return '.'.join(full_name) + return ".".join(full_name) def match_uri_to_workspace(uri, workspaces): @@ -128,6 +133,7 @@ def merge_dicts(dict_a, dict_b): If override_nones is True, then """ + def _merge_dicts_(a, b): for key in set(a.keys()).union(b.keys()): if key in a and key in b: @@ -143,6 +149,7 @@ def _merge_dicts_(a, b): yield (key, a[key]) elif b[key] is not None: yield (key, b[key]) + return dict(_merge_dicts_(dict_a, dict_b)) @@ -150,8 +157,8 @@ def escape_plain_text(contents: str) -> str: """ Format plain text to display nicely in environments which do not respect whitespaces. """ - contents = contents.replace('\t', '\u00A0' * 4) - contents = contents.replace(' ', '\u00A0' * 2) + contents = contents.replace("\t", "\u00A0" * 4) + contents = contents.replace(" ", "\u00A0" * 2) return contents @@ -160,17 +167,17 @@ def escape_markdown(contents: str) -> str: Format plain text to display nicely in Markdown environment. """ # escape markdown syntax - contents = re.sub(r'([\\*_#[\]])', r'\\\1', contents) + contents = re.sub(r"([\\*_#[\]])", r"\\\1", contents) # preserve white space characters contents = escape_plain_text(contents) return contents def wrap_signature(signature): - return '```python\n' + signature + '\n```\n' + return "```python\n" + signature + "\n```\n" -SERVER_SUPPORTED_MARKUP_KINDS = {'markdown', 'plaintext'} +SERVER_SUPPORTED_MARKUP_KINDS = {"markdown", "plaintext"} def choose_markup_kind(client_supported_markup_kinds: List[str]): @@ -181,10 +188,12 @@ def choose_markup_kind(client_supported_markup_kinds: List[str]): for kind in client_supported_markup_kinds: if kind in SERVER_SUPPORTED_MARKUP_KINDS: return kind - return 'markdown' + return "markdown" -def format_docstring(contents: str, markup_kind: str, signatures: Optional[List[str]] = None): +def format_docstring( + contents: str, markup_kind: str, signatures: Optional[List[str]] = None +): """Transform the provided docstring into a MarkupContent object. If `markup_kind` is 'markdown' the docstring will get converted to @@ -195,33 +204,24 @@ def format_docstring(contents: str, markup_kind: str, signatures: Optional[List[ to the provided contents of the docstring if given. """ if not isinstance(contents, str): - contents = '' + contents = "" - if markup_kind == 'markdown': + if markup_kind == "markdown": try: value = docstring_to_markdown.convert(contents) - return { - 'kind': 'markdown', - 'value': value - } + return {"kind": "markdown", "value": value} except docstring_to_markdown.UnknownFormatError: # try to escape the Markdown syntax instead: value = escape_markdown(contents) if signatures: - value = wrap_signature('\n'.join(signatures)) + '\n\n' + value + value = wrap_signature("\n".join(signatures)) + "\n\n" + value - return { - 'kind': 'markdown', - 'value': value - } + return {"kind": "markdown", "value": value} value = contents if signatures: - value = '\n'.join(signatures) + '\n\n' + value - return { - 'kind': 'plaintext', - 'value': escape_plain_text(value) - } + value = "\n".join(signatures) + "\n\n" + value + return {"kind": "plaintext", "value": escape_plain_text(value)} def clip_column(column, lines, line_number): @@ -230,7 +230,9 @@ def clip_column(column, lines, line_number): https://microsoft.github.io/language-server-protocol/specification#position """ - max_column = len(lines[line_number].rstrip('\r\n')) if len(lines) > line_number else 0 + max_column = ( + len(lines[line_number].rstrip("\r\n")) if len(lines) > line_number else 0 + ) return min(column, max_column) @@ -242,14 +244,16 @@ def position_to_jedi_linecolumn(document, position): """ code_position = {} if position: - code_position = {'line': position['line'] + 1, - 'column': clip_column(position['character'], - document.lines, - position['line'])} + code_position = { + "line": position["line"] + 1, + "column": clip_column( + position["character"], document.lines, position["line"] + ), + } return code_position -if os.name == 'nt': +if os.name == "nt": import ctypes kernel32 = ctypes.windll.kernel32 diff --git a/pylsp/config/config.py b/pylsp/config/config.py index f5de6f4d..1efd3cd5 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -15,11 +15,10 @@ log = logging.getLogger(__name__) # Sources of config, first source overrides next source -DEFAULT_CONFIG_SOURCES = ['pycodestyle'] +DEFAULT_CONFIG_SOURCES = ["pycodestyle"] class PluginManager(pluggy.PluginManager): - def _hookexec( self, hook_name: str, @@ -50,12 +49,14 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): self._config_sources = {} try: from .flake8_conf import Flake8Config - self._config_sources['flake8'] = Flake8Config(self._root_path) + + self._config_sources["flake8"] = Flake8Config(self._root_path) except ImportError: pass try: from .pycodestyle_conf import PyCodeStyleConfig - self._config_sources['pycodestyle'] = PyCodeStyleConfig(self._root_path) + + self._config_sources["pycodestyle"] = PyCodeStyleConfig(self._root_path) except ImportError: pass @@ -71,7 +72,9 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): try: entry_point.load() except Exception as e: # pylint: disable=broad-except - log.info("Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e) + log.info( + "Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e + ) self._pm.set_blocked(entry_point.name) # Load the entry points into pluggy, having blocked any failing ones @@ -82,7 +85,9 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): log.info("Loaded pylsp plugin %s from %s", name, plugin) for plugin_conf in self._pm.hook.pylsp_settings(config=self): - self._plugin_settings = _utils.merge_dicts(self._plugin_settings, plugin_conf) + self._plugin_settings = _utils.merge_dicts( + self._plugin_settings, plugin_conf + ) self._update_disabled_plugins() @@ -123,7 +128,7 @@ def settings(self, document_path=None): settings.cache_clear() when the config is updated """ settings = {} - sources = self._settings.get('configurationSources', DEFAULT_CONFIG_SOURCES) + sources = self._settings.get("configurationSources", DEFAULT_CONFIG_SOURCES) # Plugin configuration settings = _utils.merge_dicts(settings, self._plugin_settings) @@ -137,7 +142,9 @@ def settings(self, document_path=None): if not source: continue source_conf = source.user_config() - log.debug("Got user config from %s: %s", source.__class__.__name__, source_conf) + log.debug( + "Got user config from %s: %s", source.__class__.__name__, source_conf + ) settings = _utils.merge_dicts(settings, source_conf) # Project configuration @@ -146,7 +153,9 @@ def settings(self, document_path=None): if not source: continue source_conf = source.project_config(document_path or self._root_path) - log.debug("Got project config from %s: %s", source.__class__.__name__, source_conf) + log.debug( + "Got project config from %s: %s", source.__class__.__name__, source_conf + ) settings = _utils.merge_dicts(settings, source_conf) log.debug("With configuration: %s", settings) @@ -158,7 +167,11 @@ def find_parents(self, path, names): return _utils.find_parents(root_path, path, names) def plugin_settings(self, plugin, document_path=None): - return self.settings(document_path=document_path).get('plugins', {}).get(plugin, {}) + return ( + self.settings(document_path=document_path) + .get("plugins", {}) + .get(plugin, {}) + ) def update(self, settings): """Recursively merge the given settings into the current settings.""" @@ -170,7 +183,8 @@ def update(self, settings): def _update_disabled_plugins(self): # All plugins default to enabled self._disabled_plugins = [ - plugin for name, plugin in self.plugin_manager.list_name_plugin() - if not self.settings().get('plugins', {}).get(name, {}).get('enabled', True) + plugin + for name, plugin in self.plugin_manager.list_name_plugin() + if not self.settings().get("plugins", {}).get(name, {}).get("enabled", True) ] log.info("Disabled plugins: %s", self._disabled_plugins) diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py index 56debe4e..485945df 100644 --- a/pylsp/config/flake8_conf.py +++ b/pylsp/config/flake8_conf.py @@ -8,30 +8,30 @@ log = logging.getLogger(__name__) -CONFIG_KEY = 'flake8' -PROJECT_CONFIGS = ['.flake8', 'setup.cfg', 'tox.ini'] +CONFIG_KEY = "flake8" +PROJECT_CONFIGS = [".flake8", "setup.cfg", "tox.ini"] OPTIONS = [ # mccabe - ('max-complexity', 'plugins.mccabe.threshold', int), + ("max-complexity", "plugins.mccabe.threshold", int), # pycodestyle - ('exclude', 'plugins.pycodestyle.exclude', list), - ('filename', 'plugins.pycodestyle.filename', list), - ('hang-closing', 'plugins.pycodestyle.hangClosing', bool), - ('ignore', 'plugins.pycodestyle.ignore', list), - ('max-line-length', 'plugins.pycodestyle.maxLineLength', int), - ('indent-size', 'plugins.pycodestyle.indentSize', int), - ('select', 'plugins.pycodestyle.select', list), + ("exclude", "plugins.pycodestyle.exclude", list), + ("filename", "plugins.pycodestyle.filename", list), + ("hang-closing", "plugins.pycodestyle.hangClosing", bool), + ("ignore", "plugins.pycodestyle.ignore", list), + ("max-line-length", "plugins.pycodestyle.maxLineLength", int), + ("indent-size", "plugins.pycodestyle.indentSize", int), + ("select", "plugins.pycodestyle.select", list), # flake8 - ('exclude', 'plugins.flake8.exclude', list), - ('filename', 'plugins.flake8.filename', list), - ('hang-closing', 'plugins.flake8.hangClosing', bool), - ('ignore', 'plugins.flake8.ignore', list), - ('max-complexity', 'plugins.flake8.maxComplexity', int), - ('max-line-length', 'plugins.flake8.maxLineLength', int), - ('indent-size', 'plugins.flake8.indentSize', int), - ('select', 'plugins.flake8.select', list), - ('per-file-ignores', 'plugins.flake8.perFileIgnores', list), + ("exclude", "plugins.flake8.exclude", list), + ("filename", "plugins.flake8.filename", list), + ("hang-closing", "plugins.flake8.hangClosing", bool), + ("ignore", "plugins.flake8.ignore", list), + ("max-complexity", "plugins.flake8.maxComplexity", int), + ("max-line-length", "plugins.flake8.maxLineLength", int), + ("indent-size", "plugins.flake8.indentSize", int), + ("select", "plugins.flake8.select", list), + ("per-file-ignores", "plugins.flake8.perFileIgnores", list), ] @@ -45,8 +45,8 @@ def user_config(self): def _user_config_file(self): if self.is_windows: - return os.path.expanduser('~\\.flake8') - return os.path.join(self.xdg_home, 'flake8') + return os.path.expanduser("~\\.flake8") + return os.path.join(self.xdg_home, "flake8") def project_config(self, document_path): files = find_parents(self.root_path, document_path, PROJECT_CONFIGS) diff --git a/pylsp/config/pycodestyle_conf.py b/pylsp/config/pycodestyle_conf.py index 6ac5941e..98d8a1b1 100644 --- a/pylsp/config/pycodestyle_conf.py +++ b/pylsp/config/pycodestyle_conf.py @@ -6,24 +6,23 @@ from .source import ConfigSource -CONFIG_KEY = 'pycodestyle' +CONFIG_KEY = "pycodestyle" USER_CONFIGS = [pycodestyle.USER_CONFIG] if pycodestyle.USER_CONFIG else [] -PROJECT_CONFIGS = ['pycodestyle.cfg', 'setup.cfg', 'tox.ini'] +PROJECT_CONFIGS = ["pycodestyle.cfg", "setup.cfg", "tox.ini"] OPTIONS = [ - ('exclude', 'plugins.pycodestyle.exclude', list), - ('filename', 'plugins.pycodestyle.filename', list), - ('hang-closing', 'plugins.pycodestyle.hangClosing', bool), - ('ignore', 'plugins.pycodestyle.ignore', list), - ('max-line-length', 'plugins.pycodestyle.maxLineLength', int), - ('indent-size', 'plugins.pycodestyle.indentSize', int), - ('select', 'plugins.pycodestyle.select', list), - ('aggressive', 'plugins.pycodestyle.aggressive', int), + ("exclude", "plugins.pycodestyle.exclude", list), + ("filename", "plugins.pycodestyle.filename", list), + ("hang-closing", "plugins.pycodestyle.hangClosing", bool), + ("ignore", "plugins.pycodestyle.ignore", list), + ("max-line-length", "plugins.pycodestyle.maxLineLength", int), + ("indent-size", "plugins.pycodestyle.indentSize", int), + ("select", "plugins.pycodestyle.select", list), + ("aggressive", "plugins.pycodestyle.aggressive", int), ] class PyCodeStyleConfig(ConfigSource): - def user_config(self): config = self.read_config_from_files(USER_CONFIGS) return self.parse_config(config, CONFIG_KEY, OPTIONS) diff --git a/pylsp/config/source.py b/pylsp/config/source.py index 6a21a84c..c82bfaeb 100644 --- a/pylsp/config/source.py +++ b/pylsp/config/source.py @@ -14,9 +14,9 @@ class ConfigSource: def __init__(self, root_path): self.root_path = root_path - self.is_windows = sys.platform == 'win32' + self.is_windows = sys.platform == "win32" self.xdg_home = os.environ.get( - 'XDG_CONFIG_HOME', os.path.expanduser('~/.config') + "XDG_CONFIG_HOME", os.path.expanduser("~/.config") ) def user_config(self): @@ -49,7 +49,7 @@ def parse_config(cls, config, key, options): @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('-', '_')]: + for opt_key in [option, option.replace("-", "_")]: if not config.has_option(key, opt_key): continue @@ -77,7 +77,7 @@ def _set_opt(cls, config_dict, path, value): if value is None: return - if '.' not in path: + if "." not in path: config_dict[path] = value return diff --git a/pylsp/plugins/_resolvers.py b/pylsp/plugins/_resolvers.py index 3483d3dc..d0d2dc4c 100644 --- a/pylsp/plugins/_resolvers.py +++ b/pylsp/plugins/_resolvers.py @@ -16,7 +16,6 @@ # ---- Base class # ----------------------------------------------------------------------------- class Resolver: - def __init__(self, callback, resolve_on_error, time_to_live=60 * 30): self.callback = callback self.resolve_on_error = resolve_on_error @@ -25,7 +24,7 @@ def __init__(self, callback, resolve_on_error, time_to_live=60 * 30): 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'} + self._cached_modules = {"pandas", "numpy", "tensorflow", "matplotlib"} @property def cached_modules(self): @@ -37,11 +36,7 @@ def cached_modules(self, new_value): def clear_outdated(self): now = self.time_key() - to_clear = [ - timestamp - for timestamp in self._cache_ttl - if timestamp < now - ] + 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] @@ -54,7 +49,7 @@ def get_or_create(self, completion: Completion): if not completion.full_name: use_cache = False else: - module_parts = completion.full_name.split('.') + module_parts = completion.full_name.split(".") use_cache = module_parts and module_parts[0] in self._cached_modules if use_cache: @@ -71,9 +66,11 @@ def get_or_create(self, completion: Completion): def _create_completion_id(self, completion: Completion): return ( - completion.full_name, completion.module_path, - completion.line, completion.column, - self.time_key() + completion.full_name, + completion.module_path, + completion.line, + completion.column, + self.time_key(), ) def resolve(self, completion): @@ -81,21 +78,23 @@ def resolve(self, completion): sig = completion.get_signatures() return self.callback(completion, sig) except Exception as e: # pylint: disable=broad-except - log.warning(f'Something went wrong when resolving label for {completion}: {e}') + log.warning( + f"Something went wrong when resolving label for {completion}: {e}" + ) return self.resolve_on_error # ---- Label resolver # ----------------------------------------------------------------------------- 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) + 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 = Resolver(callback=format_label, resolve_on_error='') +LABEL_RESOLVER = Resolver(callback=format_label, resolve_on_error="") # ---- Snippets resolver @@ -106,25 +105,27 @@ def format_snippet(completion, sig): snippet_completion = {} - positional_args = [param for param in sig[0].params - if '=' not in param.description and - param.name not in {'/', '*'}] + positional_args = [ + param + for param in sig[0].params + if "=" not in param.description and param.name not in {"/", "*"} + ] if len(positional_args) > 1: # For completions with params, we can generate a snippet instead - snippet_completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet - snippet = completion.name + '(' + snippet_completion["insertTextFormat"] = lsp.InsertTextFormat.Snippet + snippet = completion.name + "(" for i, param in enumerate(positional_args): - snippet += '${%s:%s}' % (i + 1, param.name) + snippet += "${%s:%s}" % (i + 1, param.name) if i < len(positional_args) - 1: - snippet += ', ' - snippet += ')$0' - snippet_completion['insertText'] = snippet + snippet += ", " + snippet += ")$0" + snippet_completion["insertText"] = snippet elif len(positional_args) == 1: - snippet_completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet - snippet_completion['insertText'] = completion.name + '($0)' + snippet_completion["insertTextFormat"] = lsp.InsertTextFormat.Snippet + snippet_completion["insertText"] = completion.name + "($0)" else: - snippet_completion['insertText'] = completion.name + '()' + snippet_completion["insertText"] = completion.name + "()" return snippet_completion diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py index 50be2c33..1ae3e5f1 100644 --- a/pylsp/plugins/autopep8_format.py +++ b/pylsp/plugins/autopep8_format.py @@ -13,7 +13,9 @@ @hookimpl(tryfirst=True) # Prefer autopep8 over YAPF -def pylsp_format_document(config, workspace, document, options): # pylint: disable=unused-argument +def pylsp_format_document( + config, workspace, document, options +): # pylint: disable=unused-argument with workspace.report_progress("format: autopep8"): log.info("Formatting document %s with autopep8", document) return _format(config, document) @@ -26,22 +28,22 @@ def pylsp_format_range( log.info("Formatting document %s in range %s with autopep8", document, range) # First we 'round' the range up/down to full lines only - range['start']['character'] = 0 - range['end']['line'] += 1 - range['end']['character'] = 0 + range["start"]["character"] = 0 + range["end"]["line"] += 1 + range["end"]["character"] = 0 # Add 1 for 1-indexing vs LSP's 0-indexing - line_range = (range['start']['line'] + 1, range['end']['line'] + 1) + line_range = (range["start"]["line"] + 1, range["end"]["line"] + 1) return _format(config, document, line_range=line_range) def _format(config, document, line_range=None): options = _autopep8_config(config, document) if line_range: - options['line_range'] = list(line_range) + options["line_range"] = list(line_range) # Temporarily re-monkey-patch the continued_indentation checker - #771 - del pycodestyle._checks['logical_line'][pycodestyle.continued_indentation] + del pycodestyle._checks["logical_line"][pycodestyle.continued_indentation] pycodestyle.register_check(autopep8_c_i) # Autopep8 doesn't work with CR line endings, so we replace them by '\n' @@ -49,45 +51,47 @@ def _format(config, document, line_range=None): replace_cr = False source = document.source eol_chars = get_eol_chars(source) - if eol_chars == '\r': + if eol_chars == "\r": replace_cr = True - source = source.replace('\r', '\n') + source = source.replace("\r", "\n") new_source = fix_code(source, options=options) # Switch it back - del pycodestyle._checks['logical_line'][autopep8_c_i] + del pycodestyle._checks["logical_line"][autopep8_c_i] pycodestyle.register_check(pycodestyle.continued_indentation) if new_source == source: return [] if replace_cr: - new_source = new_source.replace('\n', '\r') + new_source = new_source.replace("\n", "\r") # I'm too lazy at the moment to parse diffs into TextEdit items # So let's just return the entire file... - return [{ - 'range': { - 'start': {'line': 0, 'character': 0}, - # End char 0 of the line after our document - 'end': {'line': len(document.lines), 'character': 0} - }, - 'newText': new_source - }] + return [ + { + "range": { + "start": {"line": 0, "character": 0}, + # End char 0 of the line after our document + "end": {"line": len(document.lines), "character": 0}, + }, + "newText": new_source, + } + ] def _autopep8_config(config, document=None): # We user pycodestyle settings to avoid redefining things path = document.path if document is not None else None - settings = config.plugin_settings('pycodestyle', document_path=path) + settings = config.plugin_settings("pycodestyle", document_path=path) options = { - 'exclude': settings.get('exclude'), - 'hang_closing': settings.get('hangClosing'), - 'ignore': settings.get('ignore'), - 'max_line_length': settings.get('maxLineLength'), - 'select': settings.get('select'), - 'aggressive': settings.get('aggressive'), + "exclude": settings.get("exclude"), + "hang_closing": settings.get("hangClosing"), + "ignore": settings.get("ignore"), + "max_line_length": settings.get("maxLineLength"), + "select": settings.get("select"), + "aggressive": settings.get("aggressive"), } # Filter out null options diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index fe06ab3c..a5ccbd70 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -9,30 +9,32 @@ @hookimpl def pylsp_definitions(config, document, position): - settings = config.plugin_settings('jedi_definition') + settings = config.plugin_settings("jedi_definition") code_position = _utils.position_to_jedi_linecolumn(document, position) definitions = document.jedi_script(use_document_path=True).goto( - follow_imports=settings.get('follow_imports', True), - follow_builtin_imports=settings.get('follow_builtin_imports', True), - **code_position) + follow_imports=settings.get("follow_imports", True), + follow_builtin_imports=settings.get("follow_builtin_imports", True), + **code_position, + ) follow_builtin_defns = settings.get("follow_builtin_definitions", True) return [ { - 'uri': uris.uri_with(document.uri, path=str(d.module_path)), - 'range': { - 'start': {'line': d.line - 1, 'character': d.column}, - 'end': {'line': d.line - 1, 'character': d.column + len(d.name)}, - } + "uri": uris.uri_with(document.uri, path=str(d.module_path)), + "range": { + "start": {"line": d.line - 1, "character": d.column}, + "end": {"line": d.line - 1, "character": d.column + len(d.name)}, + }, } - for d in definitions if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d)) + for d in definitions + if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d)) ] def _not_internal_definition(definition): return ( - definition.line is not None and - definition.column is not None and - definition.module_path is not None and - not definition.in_builtin_module() + definition.line is not None + and definition.column is not None + and definition.module_path is not None + and not definition.in_builtin_module() ) diff --git a/pylsp/plugins/flake8_lint.py b/pylsp/plugins/flake8_lint.py index 3a779db0..70452cf9 100644 --- a/pylsp/plugins/flake8_lint.py +++ b/pylsp/plugins/flake8_lint.py @@ -12,27 +12,27 @@ from pylsp import hookimpl, lsp log = logging.getLogger(__name__) -FIX_IGNORES_RE = re.compile(r'([^a-zA-Z0-9_,]*;.*(\W+||$))') +FIX_IGNORES_RE = re.compile(r"([^a-zA-Z0-9_,]*;.*(\W+||$))") UNNECESSITY_CODES = { - 'F401', # `module` imported but unused - 'F504', # % format unused named arguments - 'F522', # .format(...) unused named arguments - 'F523', # .format(...) unused positional arguments - 'F841' # local variable `name` is assigned to but never used + "F401", # `module` imported but unused + "F504", # % format unused named arguments + "F522", # .format(...) unused named arguments + "F523", # .format(...) unused positional arguments + "F841", # local variable `name` is assigned to but never used } @hookimpl def pylsp_settings(): # Default flake8 to disabled - return {'plugins': {'flake8': {'enabled': False}}} + return {"plugins": {"flake8": {"enabled": False}}} @hookimpl def pylsp_lint(workspace, document): with workspace.report_progress("lint: flake8"): config = workspace._config - settings = config.plugin_settings('flake8', document_path=document.path) + settings = config.plugin_settings("flake8", document_path=document.path) log.debug("Got flake8 settings: %s", settings) ignores = settings.get("ignore", []) @@ -48,8 +48,7 @@ def pylsp_lint(workspace, document): # It's legal to just specify another error type for the same # file pattern: if prev_file_pat is None: - log.warning( - "skipping a Per-file-ignore with no file pattern") + log.warning("skipping a Per-file-ignore with no file pattern") continue file_pat = prev_file_pat errors = path @@ -57,27 +56,27 @@ def pylsp_lint(workspace, document): ignores.extend(errors.split(",")) opts = { - 'config': settings.get('config'), - 'exclude': settings.get('exclude'), - 'filename': settings.get('filename'), - 'hang-closing': settings.get('hangClosing'), - 'ignore': ignores or None, - 'max-complexity': settings.get('maxComplexity'), - 'max-line-length': settings.get('maxLineLength'), - 'indent-size': settings.get('indentSize'), - 'select': settings.get('select'), + "config": settings.get("config"), + "exclude": settings.get("exclude"), + "filename": settings.get("filename"), + "hang-closing": settings.get("hangClosing"), + "ignore": ignores or None, + "max-complexity": settings.get("maxComplexity"), + "max-line-length": settings.get("maxLineLength"), + "indent-size": settings.get("indentSize"), + "select": settings.get("select"), } # flake takes only absolute path to the config. So we should check and # convert if necessary - if opts.get('config') and not os.path.isabs(opts.get('config')): - opts['config'] = os.path.abspath(os.path.expanduser(os.path.expandvars( - opts.get('config') - ))) - log.debug("using flake8 with config: %s", opts['config']) + if opts.get("config") and not os.path.isabs(opts.get("config")): + opts["config"] = os.path.abspath( + os.path.expanduser(os.path.expandvars(opts.get("config"))) + ) + log.debug("using flake8 with config: %s", opts["config"]) # Call the flake8 utility then parse diagnostics from stdout - flake8_executable = settings.get('executable', 'flake8') + flake8_executable = settings.get("executable", "flake8") args = build_args(opts) output = run_flake8(flake8_executable, args, document) @@ -89,11 +88,19 @@ def run_flake8(flake8_executable, args, document): from stderr if any. """ # a quick temporary fix to deal with Atom - args = [(i if not i.startswith('--ignore=') else FIX_IGNORES_RE.sub('', i)) - for i in args if i is not None] + args = [ + (i if not i.startswith("--ignore=") else FIX_IGNORES_RE.sub("", i)) + for i in args + if i is not None + ] if document.path and document.path.startswith(document._workspace.root_path): - args.extend(["--stdin-display-name", os.path.relpath(document.path, document._workspace.root_path)]) + args.extend( + [ + "--stdin-display-name", + os.path.relpath(document.path, document._workspace.root_path), + ] + ) # if executable looks like a path resolve it if not os.path.isfile(flake8_executable) and os.sep in flake8_executable: @@ -105,10 +112,16 @@ def run_flake8(flake8_executable, args, document): try: cmd = [flake8_executable] cmd.extend(args) - p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=document._workspace.root_path) + p = Popen( + cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=document._workspace.root_path + ) except IOError: - log.debug("Can't execute %s. Trying with '%s -m flake8'", flake8_executable, sys.executable) - cmd = [sys.executable, '-m', 'flake8'] + log.debug( + "Can't execute %s. Trying with '%s -m flake8'", + flake8_executable, + sys.executable, + ) + cmd = [sys.executable, "-m", "flake8"] cmd.extend(args) p = Popen( # pylint: disable=consider-using-with cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=document._workspace.root_path @@ -125,18 +138,18 @@ def build_args(options): Args: options: dictionary of argument names and their values. """ - args = ['-'] # use stdin + args = ["-"] # use stdin for arg_name, arg_val in options.items(): if arg_val is None: continue arg = None if isinstance(arg_val, list): - arg = '--{}={}'.format(arg_name, ','.join(arg_val)) + arg = "--{}={}".format(arg_name, ",".join(arg_val)) elif isinstance(arg_val, bool): if arg_val: - arg = '--{}'.format(arg_name) + arg = "--{}".format(arg_name) else: - arg = '--{}={}'.format(arg_name, arg_val) + arg = "--{}={}".format(arg_name, arg_val) args.append(arg) return args @@ -172,7 +185,7 @@ def parse_stdout(document, stdout): diagnostics = [] lines = stdout.splitlines() for raw_line in lines: - parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*) (.*)', raw_line) + parsed_line = re.match(r"(.*):(\d*):(\d*): (\w*) (.*)", raw_line) if not parsed_line: log.debug("Flake8 output parser can't parse line '%s'", raw_line) continue @@ -186,29 +199,26 @@ def parse_stdout(document, stdout): line = int(line) - 1 character = int(character) - 1 # show also the code in message - msg = code + ' ' + msg + msg = code + " " + msg severity = lsp.DiagnosticSeverity.Warning if code == "E999" or code[0] == "F": severity = lsp.DiagnosticSeverity.Error diagnostic = { - 'source': 'flake8', - 'code': code, - 'range': { - 'start': { - 'line': line, - 'character': character - }, - 'end': { - 'line': line, + "source": "flake8", + "code": code, + "range": { + "start": {"line": line, "character": character}, + "end": { + "line": line, # no way to determine the column - 'character': len(document.lines[line]) - } + "character": len(document.lines[line]), + }, }, - 'message': msg, - 'severity': severity, + "message": msg, + "severity": severity, } if code in UNNECESSITY_CODES: - diagnostic['tags'] = [lsp.DiagnosticTag.Unnecessary] + diagnostic["tags"] = [lsp.DiagnosticTag.Unnecessary] diagnostics.append(diagnostic) return diagnostics diff --git a/pylsp/plugins/folding.py b/pylsp/plugins/folding.py index 91469d3d..123ba4a8 100644 --- a/pylsp/plugins/folding.py +++ b/pylsp/plugins/folding.py @@ -9,26 +9,28 @@ from pylsp import hookimpl SKIP_NODES = (tree_nodes.Module, tree_nodes.IfStmt, tree_nodes.TryStmt) -IDENTATION_REGEX = re.compile(r'(\s+).+') +IDENTATION_REGEX = re.compile(r"(\s+).+") @hookimpl def pylsp_folding_range(document): - program = document.source + '\n' + program = document.source + "\n" lines = program.splitlines() tree = parso.parse(program) ranges = __compute_folding_ranges(tree, lines) results = [] - for (start_line, end_line) in ranges: + for start_line, end_line in ranges: start_line -= 1 end_line -= 1 # If start/end character is not defined, then it defaults to the # corresponding line last character - results.append({ - 'startLine': start_line, - 'endLine': end_line, - }) + results.append( + { + "startLine": start_line, + "endLine": end_line, + } + ) return results @@ -41,8 +43,9 @@ def __merge_folding_ranges(left, right): return left -def __empty_identation_stack(identation_stack, level_limits, - current_line, folding_ranges): +def __empty_identation_stack( + identation_stack, level_limits, current_line, folding_ranges +): while identation_stack != []: upper_level = identation_stack.pop(0) level_start = level_limits.pop(upper_level) @@ -50,8 +53,9 @@ def __empty_identation_stack(identation_stack, level_limits, return folding_ranges -def __match_identation_stack(identation_stack, level, level_limits, - folding_ranges, current_line): +def __match_identation_stack( + identation_stack, level, level_limits, folding_ranges, current_line +): upper_level = identation_stack.pop(0) while upper_level >= level: level_start = level_limits.pop(upper_level) @@ -68,7 +72,7 @@ def __compute_folding_ranges_identation(text): level_limits = {} current_level = 0 current_line = 0 - while lines[current_line] == '': + while lines[current_line] == "": current_line += 1 for i, line in enumerate(lines): if i < current_line: @@ -84,17 +88,19 @@ def __compute_folding_ranges_identation(text): current_level = level elif level < current_level: identation_stack, folding_ranges = __match_identation_stack( - identation_stack, level, level_limits, folding_ranges, - current_line) + identation_stack, level, level_limits, folding_ranges, current_line + ) current_level = level else: folding_ranges = __empty_identation_stack( - identation_stack, level_limits, current_line, folding_ranges) + identation_stack, level_limits, current_line, folding_ranges + ) current_level = 0 - if line.strip() != '': + if line.strip() != "": current_line = i folding_ranges = __empty_identation_stack( - identation_stack, level_limits, current_line, folding_ranges) + identation_stack, level_limits, current_line, folding_ranges + ) return dict(folding_ranges) @@ -102,9 +108,13 @@ def __check_if_node_is_valid(node): valid = True if isinstance(node, tree_nodes.PythonNode): kind = node.type - valid = kind not in {'decorated', 'parameters', 'dictorsetmaker', - 'testlist_comp'} - if kind == 'suite': + valid = kind not in { + "decorated", + "parameters", + "dictorsetmaker", + "testlist_comp", + } + if kind == "suite": if isinstance(node.parent, tree_nodes.Function): valid = False return valid @@ -113,9 +123,9 @@ def __check_if_node_is_valid(node): def __handle_skip(stack, skip): body = stack[skip] children = [body] - if hasattr(body, 'children'): + if hasattr(body, "children"): children = body.children - stack = stack[:skip] + children + stack[skip + 1:] + stack = stack[:skip] + children + stack[skip + 1 :] node = body end_line, _ = body.end_pos return node, end_line @@ -125,17 +135,17 @@ def __handle_flow_nodes(node, end_line, stack): from_keyword = False if isinstance(node, tree_nodes.Keyword): from_keyword = True - if node.value in {'if', 'elif', 'with', 'while'}: + if node.value in {"if", "elif", "with", "while"}: node, end_line = __handle_skip(stack, 2) - elif node.value in {'except'}: + elif node.value in {"except"}: first_node = stack[0] if isinstance(first_node, tree_nodes.Operator): node, end_line = __handle_skip(stack, 1) else: node, end_line = __handle_skip(stack, 2) - elif node.value in {'for'}: + elif node.value in {"for"}: node, end_line = __handle_skip(stack, 4) - elif node.value in {'else'}: + elif node.value in {"else"}: node, end_line = __handle_skip(stack, 1) return end_line, from_keyword, node, stack @@ -144,8 +154,7 @@ def __compute_start_end_lines(node, stack): start_line, _ = node.start_pos end_line, _ = node.end_pos modified = False - end_line, from_keyword, node, stack = __handle_flow_nodes( - node, end_line, stack) + end_line, from_keyword, node, stack = __handle_flow_nodes(node, end_line, stack) last_leaf = node.get_last_leaf() last_newline = isinstance(last_leaf, tree_nodes.Newline) @@ -157,7 +166,7 @@ def __compute_start_end_lines(node, stack): if isinstance(node.parent, tree_nodes.PythonNode) and not from_keyword: kind = node.type - if kind in {'suite', 'atom', 'atom_expr', 'arglist'}: + if kind in {"suite", "atom", "atom_expr", "arglist"}: if len(stack) > 0: next_node = stack[0] next_line, _ = next_node.start_pos @@ -182,21 +191,19 @@ def __compute_folding_ranges(tree, lines): # Fallback to indentation-based (best-effort) folding start_line, _ = node.start_pos start_line -= 1 - padding = [''] * start_line - text = '\n'.join(padding + lines[start_line:]) + '\n' + padding = [""] * start_line + text = "\n".join(padding + lines[start_line:]) + "\n" identation_ranges = __compute_folding_ranges_identation(text) - folding_ranges = __merge_folding_ranges( - folding_ranges, identation_ranges) + folding_ranges = __merge_folding_ranges(folding_ranges, identation_ranges) break if not isinstance(node, SKIP_NODES): valid = __check_if_node_is_valid(node) if valid: - start_line, end_line, stack = __compute_start_end_lines( - node, stack) + start_line, end_line, stack = __compute_start_end_lines(node, stack) if end_line > start_line: current_end = folding_ranges.get(start_line, -1) folding_ranges[start_line] = max(current_end, end_line) - if hasattr(node, 'children'): + if hasattr(node, "children"): stack = node.children + stack folding_ranges = sorted(folding_ranges.items()) diff --git a/pylsp/plugins/highlight.py b/pylsp/plugins/highlight.py index a67979ac..0dd896c6 100644 --- a/pylsp/plugins/highlight.py +++ b/pylsp/plugins/highlight.py @@ -16,12 +16,20 @@ def is_valid(definition): return definition.line is not None and definition.column is not None def local_to_document(definition): - return not definition.module_path or str(definition.module_path) == document.path + return ( + not definition.module_path or str(definition.module_path) == document.path + ) - return [{ - 'range': { - 'start': {'line': d.line - 1, 'character': d.column}, - 'end': {'line': d.line - 1, 'character': d.column + len(d.name)} - }, - 'kind': lsp.DocumentHighlightKind.Write if d.is_definition() else lsp.DocumentHighlightKind.Read - } for d in usages if is_valid(d) and local_to_document(d)] + return [ + { + "range": { + "start": {"line": d.line - 1, "character": d.column}, + "end": {"line": d.line - 1, "character": d.column + len(d.name)}, + }, + "kind": lsp.DocumentHighlightKind.Write + if d.is_definition() + else lsp.DocumentHighlightKind.Read, + } + for d in usages + if is_valid(d) and local_to_document(d) + ] diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py index 6f8b3b76..ae07b3dc 100644 --- a/pylsp/plugins/hover.py +++ b/pylsp/plugins/hover.py @@ -24,26 +24,27 @@ def pylsp_hover(config, document, position): definition = definitions[0] if not definition: - return {'contents': ''} + return {"contents": ""} - hover_capabilities = config.capabilities.get('textDocument', {}).get('hover', {}) - supported_markup_kinds = hover_capabilities.get('contentFormat', ['markdown']) + hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {}) + supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) # Find first exact matching signature signature = next( ( - x.to_string() for x in definition.get_signatures() + x.to_string() + for x in definition.get_signatures() if (x.name == word and x.type not in ["module"]) ), - '' + "", ) return { - 'contents': _utils.format_docstring( + "contents": _utils.format_docstring( # raw docstring returns only doc, without signature definition.docstring(raw=True), preferred_markup_kind, - signatures=[signature] if signature else None + signatures=[signature] if signature else None, ) } diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 90b4c191..2ecf0bec 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -16,58 +16,70 @@ # > ``param``, ``path``, ``keyword``, ``property`` and ``statement``. # see: https://jedi.readthedocs.io/en/latest/docs/api-classes.html#jedi.api.classes.BaseName.type _TYPE_MAP = { - 'module': lsp.CompletionItemKind.Module, - 'namespace': lsp.CompletionItemKind.Module, # to be added in Jedi 0.18+ - 'class': lsp.CompletionItemKind.Class, - 'instance': lsp.CompletionItemKind.Reference, - 'function': lsp.CompletionItemKind.Function, - 'param': lsp.CompletionItemKind.Variable, - 'path': lsp.CompletionItemKind.File, - 'keyword': lsp.CompletionItemKind.Keyword, - 'property': lsp.CompletionItemKind.Property, # added in Jedi 0.18 - 'statement': lsp.CompletionItemKind.Variable + "module": lsp.CompletionItemKind.Module, + "namespace": lsp.CompletionItemKind.Module, # to be added in Jedi 0.18+ + "class": lsp.CompletionItemKind.Class, + "instance": lsp.CompletionItemKind.Reference, + "function": lsp.CompletionItemKind.Function, + "param": lsp.CompletionItemKind.Variable, + "path": lsp.CompletionItemKind.File, + "keyword": lsp.CompletionItemKind.Keyword, + "property": lsp.CompletionItemKind.Property, # added in Jedi 0.18 + "statement": lsp.CompletionItemKind.Variable, } # Types of parso nodes for which snippet is not included in the completion -_IMPORTS = ('import_name', 'import_from') +_IMPORTS = ("import_name", "import_from") # Types of parso node for errors -_ERRORS = ('error_node', ) +_ERRORS = ("error_node",) @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) + 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: return None - completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) - item_capabilities = completion_capabilities.get('completionItem', {}) - snippet_support = item_capabilities.get('snippetSupport') - supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + completion_capabilities = config.capabilities.get("textDocument", {}).get( + "completion", {} + ) + item_capabilities = completion_capabilities.get("completionItem", {}) + snippet_support = item_capabilities.get("snippetSupport") + supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) - should_include_params = settings.get('include_params') - should_include_class_objects = settings.get('include_class_objects', False) - should_include_function_objects = settings.get('include_function_objects', False) + should_include_params = settings.get("include_params") + should_include_class_objects = settings.get("include_class_objects", False) + should_include_function_objects = settings.get("include_function_objects", False) - max_to_resolve = settings.get('resolve_at_most', 25) - modules_to_cache_for = settings.get('cache_for', None) + max_to_resolve = settings.get("resolve_at_most", 25) + modules_to_cache_for = settings.get("cache_for", None) if modules_to_cache_for is not None: LABEL_RESOLVER.cached_modules = modules_to_cache_for SNIPPET_RESOLVER.cached_modules = modules_to_cache_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) - include_function_objects = snippet_support and should_include_function_objects and use_snippets(document, position) + 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) + ) + include_function_objects = ( + snippet_support + and should_include_function_objects + and use_snippets(document, position) + ) ready_completions = [ _format_completion( @@ -75,7 +87,7 @@ def pylsp_completions(config, document, position): markup_kind=preferred_markup_kind, include_params=include_params if c.type in ["class", "function"] else False, resolve=resolve_eagerly, - resolve_label_or_snippet=(i < max_to_resolve) + resolve_label_or_snippet=(i < max_to_resolve), ) for i, c in enumerate(completions) ] @@ -83,41 +95,39 @@ def pylsp_completions(config, document, position): # TODO split up once other improvements are merged if include_class_objects: for i, c in enumerate(completions): - if c.type == 'class': + if c.type == "class": completion_dict = _format_completion( c, markup_kind=preferred_markup_kind, include_params=False, resolve=resolve_eagerly, - resolve_label_or_snippet=(i < max_to_resolve) + resolve_label_or_snippet=(i < max_to_resolve), ) - completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter - completion_dict['label'] += ' object' + completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter + completion_dict["label"] += " object" ready_completions.append(completion_dict) if include_function_objects: for i, c in enumerate(completions): - if c.type == 'function': + if c.type == "function": completion_dict = _format_completion( c, markup_kind=preferred_markup_kind, include_params=False, resolve=resolve_eagerly, - resolve_label_or_snippet=(i < max_to_resolve) + resolve_label_or_snippet=(i < max_to_resolve), ) - completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter - completion_dict['label'] += ' object' + 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 - } + completion_dict["data"] = {"doc_uri": document.uri} # most recently retrieved completion items, used for resolution - document.shared_data['LAST_JEDI_COMPLETIONS'] = { + document.shared_data["LAST_JEDI_COMPLETIONS"] = { # label is the only required property; here it is assumed to be unique - completion['label']: (completion, data) + completion["label"]: (completion, data) for completion, data in zip(ready_completions, completions) } @@ -127,11 +137,15 @@ def pylsp_completions(config, document, position): @hookimpl def pylsp_completion_item_resolve(config, completion_item, document): """Resolve formatted completion for given non-resolved completion""" - shared_data = document.shared_data['LAST_JEDI_COMPLETIONS'].get(completion_item['label']) - - completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) - item_capabilities = completion_capabilities.get('completionItem', {}) - supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get( + completion_item["label"] + ) + + completion_capabilities = config.capabilities.get("textDocument", {}).get( + "completion", {} + ) + item_capabilities = completion_capabilities.get("completionItem", {}) + supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) if shared_data: @@ -162,78 +176,82 @@ def use_snippets(document, position): This returns `False` if a completion is being requested on an import statement, `True` otherwise. """ - line = position['line'] - lines = document.source.split('\n', line) - act_lines = [lines[line][:position['character']]] + line = position["line"] + lines = document.source.split("\n", line) + act_lines = [lines[line][: position["character"]]] line -= 1 - last_character = '' + last_character = "" while line > -1: act_line = lines[line] - if (act_line.rstrip().endswith('\\') or - act_line.rstrip().endswith('(') or - act_line.rstrip().endswith(',')): + if ( + act_line.rstrip().endswith("\\") + or act_line.rstrip().endswith("(") + or act_line.rstrip().endswith(",") + ): act_lines.insert(0, act_line) line -= 1 - if act_line.rstrip().endswith('('): + if act_line.rstrip().endswith("("): # Needs to be added to the end of the code before parsing # to make it valid, otherwise the node type could end # being an 'error_node' for multi-line imports that use '(' - last_character = ')' + last_character = ")" else: break - if '(' in act_lines[-1].strip(): - last_character = ')' - code = '\n'.join(act_lines).rsplit(';', maxsplit=1)[-1].strip() + last_character + if "(" in act_lines[-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)) + return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code) def _resolve_completion(completion, d, markup_kind: str): # pylint: disable=broad-except - completion['detail'] = _detail(d) + completion["detail"] = _detail(d) try: docs = _utils.format_docstring( d.docstring(raw=True), - signatures=[ - signature.to_string() - for signature in d.get_signatures() - ], - markup_kind=markup_kind + signatures=[signature.to_string() for signature in d.get_signatures()], + markup_kind=markup_kind, ) except Exception: - docs = '' - completion['documentation'] = docs + docs = "" + completion["documentation"] = docs return completion -def _format_completion(d, markup_kind: str, include_params=True, resolve=False, resolve_label_or_snippet=False): +def _format_completion( + d, + markup_kind: str, + include_params=True, + resolve=False, + resolve_label_or_snippet=False, +): completion = { - 'label': _label(d, resolve_label_or_snippet), - 'kind': _TYPE_MAP.get(d.type), - 'sortText': _sort_text(d), - 'insertText': d.name + "label": _label(d, resolve_label_or_snippet), + "kind": _TYPE_MAP.get(d.type), + "sortText": _sort_text(d), + "insertText": d.name, } if resolve: completion = _resolve_completion(completion, d, markup_kind) # Adjustments for file completions - if d.type == 'path': + if d.type == "path": path = os.path.normpath(d.name) - path = path.replace('\\', '\\\\') - path = path.replace('/', '\\/') + path = path.replace("\\", "\\\\") + path = path.replace("/", "\\/") # If the completion ends with os.sep, it means it's a directory. So we add an escaped os.sep # at the end to ease additional file completions. if d.name.endswith(os.sep): - if os.name == 'nt': - path = path + '\\\\' + if os.name == "nt": + path = path + "\\\\" else: - path = path + '\\/' + path = path + "\\/" - completion['insertText'] = path + completion["insertText"] = path if include_params and not is_exception_class(d.name): snippet = _snippet(d, resolve_label_or_snippet) @@ -260,16 +278,16 @@ def _snippet(definition, resolve=False): def _detail(definition): try: - return definition.parent().full_name or '' + return definition.parent().full_name or "" except AttributeError: - return definition.full_name or '' + return definition.full_name or "" def _sort_text(definition): - """ Ensure builtins appear at the bottom. + """Ensure builtins appear at the bottom. Description is of format : . """ # If its 'hidden', put it next last - prefix = 'z{}' if definition.name.startswith('_') else 'a{}' + prefix = "z{}" if definition.name.startswith("_") else "a{}" return prefix.format(definition.name) diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index d44b28e9..700da508 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -9,44 +9,49 @@ @hookimpl -def pylsp_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument - log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name) +def pylsp_rename( + config, workspace, document, position, new_name +): # pylint: disable=unused-argument + log.debug( + "Executing rename of %s to %s", document.word_at_position(position), new_name + ) kwargs = _utils.position_to_jedi_linecolumn(document, position) - kwargs['new_name'] = new_name + kwargs["new_name"] = new_name try: refactoring = document.jedi_script().rename(**kwargs) except NotImplementedError as exc: # pylint: disable=broad-exception-raised - raise Exception('No support for renaming in Python 2/3.5 with Jedi. ' - 'Consider using the rope_rename plugin instead') from exc - log.debug('Finished rename: %s', refactoring.get_diff()) + raise Exception( + "No support for renaming in Python 2/3.5 with Jedi. " + "Consider using the rope_rename plugin instead" + ) from exc + log.debug("Finished rename: %s", refactoring.get_diff()) changes = [] changed_files = refactoring.get_changed_files() for file_path, changed_file in changed_files.items(): uri = uris.from_fs_path(str(file_path)) doc = workspace.get_maybe_document(uri) - changes.append({ - 'textDocument': { - 'uri': uri, - 'version': doc.version if doc else None - }, - 'edits': [ - { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': { - 'line': _num_lines(changed_file.get_new_code()), - 'character': 0, + changes.append( + { + "textDocument": {"uri": uri, "version": doc.version if doc else None}, + "edits": [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": { + "line": _num_lines(changed_file.get_new_code()), + "character": 0, + }, }, - }, - 'newText': changed_file.get_new_code(), - } - ], - }) - return {'documentChanges': changes} + "newText": changed_file.get_new_code(), + } + ], + } + ) + return {"documentChanges": changes} def _num_lines(file_contents): - 'Count the number of lines in the given string.' + "Count the number of lines in the given string." return len(file_contents.splitlines()) diff --git a/pylsp/plugins/mccabe_lint.py b/pylsp/plugins/mccabe_lint.py index 41182e4c..f115a3ce 100644 --- a/pylsp/plugins/mccabe_lint.py +++ b/pylsp/plugins/mccabe_lint.py @@ -8,14 +8,16 @@ log = logging.getLogger(__name__) -THRESHOLD = 'threshold' +THRESHOLD = "threshold" DEFAULT_THRESHOLD = 15 @hookimpl def pylsp_lint(config, workspace, document): with workspace.report_progress("lint: mccabe"): - threshold = config.plugin_settings('mccabe', document_path=document.path).get(THRESHOLD, DEFAULT_THRESHOLD) + threshold = config.plugin_settings("mccabe", document_path=document.path).get( + THRESHOLD, DEFAULT_THRESHOLD + ) log.debug("Running mccabe lint with threshold: %s", threshold) try: @@ -30,14 +32,23 @@ def pylsp_lint(config, workspace, document): diags = [] for graph in visitor.graphs.values(): if graph.complexity() >= threshold: - diags.append({ - 'source': 'mccabe', - 'range': { - 'start': {'line': graph.lineno - 1, 'character': graph.column}, - 'end': {'line': graph.lineno - 1, 'character': len(document.lines[graph.lineno])}, - }, - 'message': 'Cyclomatic complexity too high: %s (threshold %s)' % (graph.complexity(), threshold), - 'severity': lsp.DiagnosticSeverity.Warning - }) + diags.append( + { + "source": "mccabe", + "range": { + "start": { + "line": graph.lineno - 1, + "character": graph.column, + }, + "end": { + "line": graph.lineno - 1, + "character": len(document.lines[graph.lineno]), + }, + }, + "message": "Cyclomatic complexity too high: %s (threshold %s)" + % (graph.complexity(), threshold), + "severity": lsp.DiagnosticSeverity.Warning, + } + ) return diags diff --git a/pylsp/plugins/preload_imports.py b/pylsp/plugins/preload_imports.py index b3994606..0b98febe 100644 --- a/pylsp/plugins/preload_imports.py +++ b/pylsp/plugins/preload_imports.py @@ -7,13 +7,52 @@ log = logging.getLogger(__name__) MODULES = [ - "OpenGL", "PIL", - "array", "audioop", "binascii", "cPickle", "cStringIO", "cmath", "collections", - "datetime", "errno", "exceptions", "gc", "imageop", "imp", "itertools", - "marshal", "math", "matplotlib", "mmap", "mpmath", "msvcrt", "networkx", "nose", "nt", - "numpy", "operator", "os", "os.path", "pandas", "parser", "rgbimg", "scipy", "signal", - "skimage", "sklearn", "statsmodels", "strop", "sympy", "sys", "thread", "time", - "wx", "xxsubtype", "zipimport", "zlib" + "OpenGL", + "PIL", + "array", + "audioop", + "binascii", + "cPickle", + "cStringIO", + "cmath", + "collections", + "datetime", + "errno", + "exceptions", + "gc", + "imageop", + "imp", + "itertools", + "marshal", + "math", + "matplotlib", + "mmap", + "mpmath", + "msvcrt", + "networkx", + "nose", + "nt", + "numpy", + "operator", + "os", + "os.path", + "pandas", + "parser", + "rgbimg", + "scipy", + "signal", + "skimage", + "sklearn", + "statsmodels", + "strop", + "sympy", + "sys", + "thread", + "time", + "wx", + "xxsubtype", + "zipimport", + "zlib", ] @@ -21,14 +60,14 @@ def pylsp_settings(): # Setup default modules to preload, and rope extension modules return { - 'plugins': {'preload': {'modules': MODULES}}, - 'rope': {'extensionModules': MODULES} + "plugins": {"preload": {"modules": MODULES}}, + "rope": {"extensionModules": MODULES}, } @hookimpl def pylsp_initialize(config): - for mod_name in config.plugin_settings('preload').get('modules', []): + for mod_name in config.plugin_settings("preload").get("modules", []): try: __import__(mod_name) log.debug("Preloaded module %s", mod_name) diff --git a/pylsp/plugins/pycodestyle_lint.py b/pylsp/plugins/pycodestyle_lint.py index 8cdcee5c..62b0b8ad 100644 --- a/pylsp/plugins/pycodestyle_lint.py +++ b/pylsp/plugins/pycodestyle_lint.py @@ -16,8 +16,8 @@ # Check if autopep8's continued_indentation implementation # is overriding pycodestyle's and if so, re-register # the check using pycodestyle's implementation as expected - if autopep8_c_i in pycodestyle._checks['logical_line']: - del pycodestyle._checks['logical_line'][autopep8_c_i] + if autopep8_c_i in pycodestyle._checks["logical_line"]: + del pycodestyle._checks["logical_line"][autopep8_c_i] pycodestyle.register_check(pycodestyle.continued_indentation) log = logging.getLogger(__name__) @@ -27,17 +27,17 @@ def pylsp_lint(workspace, document): with workspace.report_progress("lint: pycodestyle"): config = workspace._config - settings = config.plugin_settings('pycodestyle', document_path=document.path) + settings = config.plugin_settings("pycodestyle", document_path=document.path) log.debug("Got pycodestyle settings: %s", settings) opts = { - 'exclude': settings.get('exclude'), - 'filename': settings.get('filename'), - 'hang_closing': settings.get('hangClosing'), - 'ignore': settings.get('ignore'), - 'max_line_length': settings.get('maxLineLength'), - 'indent_size': settings.get('indentSize'), - 'select': settings.get('select'), + "exclude": settings.get("exclude"), + "filename": settings.get("filename"), + "hang_closing": settings.get("hangClosing"), + "ignore": settings.get("ignore"), + "max_line_length": settings.get("maxLineLength"), + "indent_size": settings.get("indentSize"), + "select": settings.get("select"), } kwargs = {k: v for k, v in opts.items() if v} styleguide = pycodestyle.StyleGuide(kwargs) @@ -46,15 +46,17 @@ def pylsp_lint(workspace, document): # See spyder-ide/spyder#19565 for context. source = document.source eol_chars = get_eol_chars(source) - if eol_chars in ['\r', '\r\n']: - source = source.replace(eol_chars, '\n') + if eol_chars in ["\r", "\r\n"]: + source = source.replace(eol_chars, "\n") lines = source.splitlines(keepends=True) else: lines = document.lines c = pycodestyle.Checker( - filename=document.path, lines=lines, options=styleguide.options, - report=PyCodeStyleDiagnosticReport(styleguide.options) + filename=document.path, + lines=lines, + options=styleguide.options, + report=PyCodeStyleDiagnosticReport(styleguide.options), ) c.check_all() diagnostics = c.report.diagnostics @@ -63,7 +65,6 @@ def pylsp_lint(workspace, document): class PyCodeStyleDiagnosticReport(pycodestyle.BaseReport): - def __init__(self, options): self.diagnostics = [] super().__init__(options=options) @@ -82,29 +83,31 @@ def error(self, line_number, offset, text, check): # In that case, the end offset should just be some number ~100 # (because why not? There's nothing to underline anyways) err_range = { - 'start': {'line': line_number - 1, 'character': offset}, - 'end': { + "start": {"line": line_number - 1, "character": offset}, + "end": { # FIXME: It's a little naiive to mark until the end of the line, can we not easily do better? - 'line': line_number - 1, - 'character': 100 if line_number > len(self.lines) else len(self.lines[line_number - 1]) + "line": line_number - 1, + "character": 100 + if line_number > len(self.lines) + else len(self.lines[line_number - 1]), }, } diagnostic = { - 'source': 'pycodestyle', - 'range': err_range, - 'message': text, - 'code': code, + "source": "pycodestyle", + "range": err_range, + "message": text, + "code": code, # Are style errors really ever errors? - 'severity': _get_severity(code) + "severity": _get_severity(code), } - if code.startswith('W6'): - diagnostic['tags'] = [lsp.DiagnosticTag.Deprecated] + if code.startswith("W6"): + diagnostic["tags"] = [lsp.DiagnosticTag.Deprecated] self.diagnostics.append(diagnostic) def _get_severity(code): # Are style errors ever really errors? - if code[0] == 'E' or code[0] == 'W': + if code[0] == "E" or code[0] == "W": return lsp.DiagnosticSeverity.Warning # If no severity is specified, why wouldn't this be informational only? return lsp.DiagnosticSeverity.Information diff --git a/pylsp/plugins/pydocstyle_lint.py b/pylsp/plugins/pydocstyle_lint.py index 2e34cccf..3a4df1c1 100644 --- a/pylsp/plugins/pydocstyle_lint.py +++ b/pylsp/plugins/pydocstyle_lint.py @@ -23,40 +23,40 @@ @hookimpl def pylsp_settings(): # Default pydocstyle to disabled - return {'plugins': {'pydocstyle': {'enabled': False}}} + return {"plugins": {"pydocstyle": {"enabled": False}}} @hookimpl def pylsp_lint(config, workspace, document): # pylint: disable=too-many-locals with workspace.report_progress("lint: pydocstyle"): - settings = config.plugin_settings('pydocstyle', document_path=document.path) + settings = config.plugin_settings("pydocstyle", document_path=document.path) log.debug("Got pydocstyle settings: %s", settings) # Explicitly passing a path to pydocstyle means it doesn't respect the --match flag, so do it ourselves - filename_match_re = re.compile(settings.get('match', DEFAULT_MATCH_RE) + '$') + filename_match_re = re.compile(settings.get("match", DEFAULT_MATCH_RE) + "$") if not filename_match_re.match(os.path.basename(document.path)): return [] # Likewise with --match-dir - dir_match_re = re.compile(settings.get('matchDir', DEFAULT_MATCH_DIR_RE) + '$') + dir_match_re = re.compile(settings.get("matchDir", DEFAULT_MATCH_DIR_RE) + "$") if not dir_match_re.match(os.path.basename(os.path.dirname(document.path))): return [] args = [document.path] - if settings.get('convention'): - args.append('--convention=' + settings['convention']) + if settings.get("convention"): + args.append("--convention=" + settings["convention"]) - if settings.get('addSelect'): - args.append('--add-select=' + ','.join(settings['addSelect'])) - if settings.get('addIgnore'): - args.append('--add-ignore=' + ','.join(settings['addIgnore'])) + if settings.get("addSelect"): + args.append("--add-select=" + ",".join(settings["addSelect"])) + if settings.get("addIgnore"): + args.append("--add-ignore=" + ",".join(settings["addIgnore"])) - elif settings.get('select'): - args.append('--select=' + ','.join(settings['select'])) - elif settings.get('ignore'): - args.append('--ignore=' + ','.join(settings['ignore'])) + elif settings.get("select"): + args.append("--select=" + ",".join(settings["select"])) + elif settings.get("ignore"): + args.append("--ignore=" + ",".join(settings["ignore"])) log.info("Using pydocstyle args: %s", args) @@ -103,20 +103,14 @@ def _parse_diagnostic(document, error): end_character = len(line) return { - 'source': 'pydocstyle', - 'code': error.code, - 'message': error.message, - 'severity': lsp.DiagnosticSeverity.Warning, - 'range': { - 'start': { - 'line': lineno, - 'character': start_character - }, - 'end': { - 'line': lineno, - 'character': end_character - } - } + "source": "pydocstyle", + "code": error.code, + "message": error.message, + "severity": lsp.DiagnosticSeverity.Warning, + "range": { + "start": {"line": lineno, "character": start_character}, + "end": {"line": lineno, "character": end_character}, + }, } diff --git a/pylsp/plugins/pyflakes_lint.py b/pylsp/plugins/pyflakes_lint.py index 72e16a2e..8707746a 100644 --- a/pylsp/plugins/pyflakes_lint.py +++ b/pylsp/plugins/pyflakes_lint.py @@ -24,27 +24,30 @@ def pylsp_lint(workspace, document): with workspace.report_progress("lint: pyflakes"): reporter = PyflakesDiagnosticReport(document.lines) - pyflakes_api.check(document.source.encode('utf-8'), document.path, reporter=reporter) + pyflakes_api.check( + document.source.encode("utf-8"), document.path, reporter=reporter + ) return reporter.diagnostics class PyflakesDiagnosticReport: - def __init__(self, lines): self.lines = lines self.diagnostics = [] def unexpectedError(self, _filename, msg): # pragma: no cover err_range = { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 0}, + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, } - self.diagnostics.append({ - 'source': 'pyflakes', - 'range': err_range, - 'message': msg, - 'severity': lsp.DiagnosticSeverity.Error, - }) + self.diagnostics.append( + { + "source": "pyflakes", + "range": err_range, + "message": msg, + "severity": lsp.DiagnosticSeverity.Error, + } + ) def syntaxError(self, _filename, msg, lineno, offset, text): # We've seen that lineno and offset can sometimes be None @@ -52,21 +55,26 @@ def syntaxError(self, _filename, msg, lineno, offset, text): offset = offset or 0 err_range = { - 'start': {'line': lineno - 1, 'character': offset}, - 'end': {'line': lineno - 1, 'character': offset + len(text)}, + "start": {"line": lineno - 1, "character": offset}, + "end": {"line": lineno - 1, "character": offset + len(text)}, } - self.diagnostics.append({ - 'source': 'pyflakes', - 'range': err_range, - 'message': msg, - 'severity': lsp.DiagnosticSeverity.Error, - }) + self.diagnostics.append( + { + "source": "pyflakes", + "range": err_range, + "message": msg, + "severity": lsp.DiagnosticSeverity.Error, + } + ) def flake(self, message): - """ Get message like :: """ + """Get message like :: """ err_range = { - 'start': {'line': message.lineno - 1, 'character': message.col}, - 'end': {'line': message.lineno - 1, 'character': len(self.lines[message.lineno - 1])}, + "start": {"line": message.lineno - 1, "character": message.col}, + "end": { + "line": message.lineno - 1, + "character": len(self.lines[message.lineno - 1]), + }, } severity = lsp.DiagnosticSeverity.Warning @@ -75,9 +83,11 @@ def flake(self, message): severity = lsp.DiagnosticSeverity.Error break - self.diagnostics.append({ - 'source': 'pyflakes', - 'range': err_range, - 'message': message.message % message.message_args, - 'severity': severity - }) + self.diagnostics.append( + { + "source": "pyflakes", + "range": err_range, + "message": message.message % message.message_args, + "severity": severity, + } + ) diff --git a/pylsp/plugins/pylint_lint.py b/pylsp/plugins/pylint_lint.py index f5d168de..67f68faf 100644 --- a/pylsp/plugins/pylint_lint.py +++ b/pylsp/plugins/pylint_lint.py @@ -27,20 +27,20 @@ # import via an (otherwise harmless) environment variable. This is an ad-hoc # fix for a very specific upstream issue. # Related: https://github.com/PyCQA/pylint/issues/3518 -os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = 'hide' +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" DEPRECATION_CODES = { - 'W0402', # Uses of a deprecated module %r - 'W1505', # Using deprecated method %s() - 'W1511', # Using deprecated argument %s of method %s() - 'W1512', # Using deprecated class %s of module %s - 'W1513', # Using deprecated decorator %s() + "W0402", # Uses of a deprecated module %r + "W1505", # Using deprecated method %s() + "W1511", # Using deprecated argument %s of method %s() + "W1512", # Using deprecated class %s of module %s + "W1513", # Using deprecated decorator %s() } UNNECESSITY_CODES = { - 'W0611', # Unused import %s - 'W0612', # Unused variable %r - 'W0613', # Unused argument %r - 'W0614', # Unused import %s from wildcard import - 'W1304', # Unused-format-string-argument + "W0611", # Unused import %s + "W0612", # Unused variable %r + "W0613", # Unused argument %r + "W0614", # Unused import %s from wildcard import + "W1304", # Unused-format-string-argument } @@ -48,7 +48,9 @@ class PylintLinter: last_diags = collections.defaultdict(list) @classmethod - def lint(cls, document, is_saved, flags=''): # pylint: disable=too-many-locals,too-many-branches + def lint( + cls, document, is_saved, flags="" + ): # pylint: disable=too-many-locals,too-many-branches """Plugin interface to pylsp linter. Args: @@ -87,23 +89,24 @@ def lint(cls, document, is_saved, flags=''): # pylint: disable=too-many-locals, cmd = [ sys.executable, - '-c', - 'import sys; from pylint.lint import Run; Run(sys.argv[1:])', - '-f', - 'json', - document.path + "-c", + "import sys; from pylint.lint import Run; Run(sys.argv[1:])", + "-f", + "json", + document.path, ] + (shlex.split(str(flags)) if flags else []) - log.debug("Calling pylint with '%s'", ' '.join(cmd)) + log.debug("Calling pylint with '%s'", " ".join(cmd)) cwd = document._workspace.root_path if not cwd: cwd = os.path.dirname(__file__) - with Popen(cmd, stdout=PIPE, stderr=PIPE, - cwd=cwd, universal_newlines=True) as process: + with Popen( + cmd, stdout=PIPE, stderr=PIPE, cwd=cwd, universal_newlines=True + ) as process: json_out, err = process.communicate() - if err != '': + if err != "": log.error("Error calling pylint: '%s'", err) # pylint prints nothing rather than [] when there are no diagnostics. @@ -137,49 +140,49 @@ def lint(cls, document, is_saved, flags=''): # pylint: disable=too-many-locals, diagnostics = [] for diag in json.loads(json_out): # pylint lines index from 1, pylsp lines index from 0 - line = diag['line'] - 1 + line = diag["line"] - 1 err_range = { - 'start': { - 'line': line, + "start": { + "line": line, # Index columns start from 0 - 'character': diag['column'], + "character": diag["column"], }, - 'end': { - 'line': line, + "end": { + "line": line, # It's possible that we're linting an empty file. Even an empty # file might fail linting if it isn't named properly. - 'character': len(document.lines[line]) if document.lines else 0, + "character": len(document.lines[line]) if document.lines else 0, }, } - if diag['type'] == 'convention': + if diag["type"] == "convention": severity = lsp.DiagnosticSeverity.Information - elif diag['type'] == 'information': + elif diag["type"] == "information": severity = lsp.DiagnosticSeverity.Information - elif diag['type'] == 'error': + elif diag["type"] == "error": severity = lsp.DiagnosticSeverity.Error - elif diag['type'] == 'fatal': + elif diag["type"] == "fatal": severity = lsp.DiagnosticSeverity.Error - elif diag['type'] == 'refactor': + elif diag["type"] == "refactor": severity = lsp.DiagnosticSeverity.Hint - elif diag['type'] == 'warning': + elif diag["type"] == "warning": severity = lsp.DiagnosticSeverity.Warning - code = diag['message-id'] + code = diag["message-id"] diagnostic = { - 'source': 'pylint', - 'range': err_range, - 'message': '[{}] {}'.format(diag['symbol'], diag['message']), - 'severity': severity, - 'code': code + "source": "pylint", + "range": err_range, + "message": "[{}] {}".format(diag["symbol"], diag["message"]), + "severity": severity, + "code": code, } if code in UNNECESSITY_CODES: - diagnostic['tags'] = [lsp.DiagnosticTag.Unnecessary] + diagnostic["tags"] = [lsp.DiagnosticTag.Unnecessary] if code in DEPRECATION_CODES: - diagnostic['tags'] = [lsp.DiagnosticTag.Deprecated] + diagnostic["tags"] = [lsp.DiagnosticTag.Deprecated] diagnostics.append(diagnostic) cls.last_diags[document.path] = diagnostics @@ -188,35 +191,39 @@ def lint(cls, document, is_saved, flags=''): # pylint: disable=too-many-locals, def _build_pylint_flags(settings): """Build arguments for calling pylint.""" - pylint_args = settings.get('args') + pylint_args = settings.get("args") if pylint_args is None: - return '' - return ' '.join(pylint_args) + return "" + return " ".join(pylint_args) @hookimpl def pylsp_settings(): # Default pylint to disabled because it requires a config # file to be useful. - return {'plugins': {'pylint': { - 'enabled': False, - 'args': [], - # disabled by default as it can slow down the workflow - 'executable': None, - }}} + return { + "plugins": { + "pylint": { + "enabled": False, + "args": [], + # disabled by default as it can slow down the workflow + "executable": None, + } + } + } @hookimpl def pylsp_lint(config, workspace, document, is_saved): """Run pylint linter.""" with workspace.report_progress("lint: pylint"): - settings = config.plugin_settings('pylint') + settings = config.plugin_settings("pylint") log.debug("Got pylint settings: %s", settings) # pylint >= 2.5.0 is required for working through stdin and only # available with python3 - if settings.get('executable') and sys.version_info[0] >= 3: + if settings.get("executable") and sys.version_info[0] >= 3: flags = build_args_stdio(settings) - pylint_executable = settings.get('executable', 'pylint') + pylint_executable = settings.get("executable", "pylint") return pylint_lint_stdin(pylint_executable, document, flags) flags = _build_pylint_flags(settings) return PylintLinter.lint(document, is_saved, flags=flags) @@ -231,7 +238,7 @@ def build_args_stdio(settings): :return: arguments to path to pylint :rtype: list """ - pylint_args = settings.get('args') + pylint_args = settings.get("args") if pylint_args is None: return [] return pylint_args @@ -275,14 +282,16 @@ def _run_pylint_stdio(pylint_executable, document, flags): try: cmd = [pylint_executable] cmd.extend(flags) - cmd.extend(['--from-stdin', document.path]) + cmd.extend(["--from-stdin", document.path]) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) except IOError: log.debug("Can't execute %s. Trying with 'python -m pylint'", pylint_executable) - cmd = ['python', '-m', 'pylint'] + cmd = ["python", "-m", "pylint"] cmd.extend(flags) - cmd.extend(['--from-stdin', document.path]) - p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) # pylint: disable=consider-using-with + cmd.extend(["--from-stdin", document.path]) + p = Popen( # pylint: disable=consider-using-with + cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE + ) (stdout, stderr) = p.communicate(document.source.encode()) if stderr: log.error("Error while running pylint '%s'", stderr.decode()) @@ -303,7 +312,7 @@ def _parse_pylint_stdio_result(document, stdout): diagnostics = [] lines = stdout.splitlines() for raw_line in lines: - parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*): (.*)', raw_line) + parsed_line = re.match(r"(.*):(\d*):(\d*): (\w*): (.*)", raw_line) if not parsed_line: log.debug("Pylint output parser can't parse line '%s'", raw_line) continue @@ -317,35 +326,32 @@ def _parse_pylint_stdio_result(document, stdout): line = int(line) - 1 character = int(character) severity_map = { - 'C': lsp.DiagnosticSeverity.Information, - 'E': lsp.DiagnosticSeverity.Error, - 'F': lsp.DiagnosticSeverity.Error, - 'I': lsp.DiagnosticSeverity.Information, - 'R': lsp.DiagnosticSeverity.Hint, - 'W': lsp.DiagnosticSeverity.Warning, + "C": lsp.DiagnosticSeverity.Information, + "E": lsp.DiagnosticSeverity.Error, + "F": lsp.DiagnosticSeverity.Error, + "I": lsp.DiagnosticSeverity.Information, + "R": lsp.DiagnosticSeverity.Hint, + "W": lsp.DiagnosticSeverity.Warning, } severity = severity_map[code[0]] diagnostic = { - 'source': 'pylint', - 'code': code, - 'range': { - 'start': { - 'line': line, - 'character': character - }, - 'end': { - 'line': line, + "source": "pylint", + "code": code, + "range": { + "start": {"line": line, "character": character}, + "end": { + "line": line, # no way to determine the column - 'character': len(document.lines[line]) - 1 - } + "character": len(document.lines[line]) - 1, + }, }, - 'message': msg, - 'severity': severity, + "message": msg, + "severity": severity, } if code in UNNECESSITY_CODES: - diagnostic['tags'] = [lsp.DiagnosticTag.Unnecessary] + diagnostic["tags"] = [lsp.DiagnosticTag.Unnecessary] if code in DEPRECATION_CODES: - diagnostic['tags'] = [lsp.DiagnosticTag.Deprecated] + diagnostic["tags"] = [lsp.DiagnosticTag.Deprecated] diagnostics.append(diagnostic) return diagnostics diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py index 4ef2072a..cfdf86b5 100644 --- a/pylsp/plugins/references.py +++ b/pylsp/plugins/references.py @@ -17,10 +17,16 @@ def pylsp_references(document, position, exclude_declaration=False): usages = [d for d in usages if not d.is_definition()] # Filter out builtin modules - return [{ - 'uri': uris.uri_with(document.uri, path=str(d.module_path)) if d.module_path else document.uri, - 'range': { - 'start': {'line': d.line - 1, 'character': d.column}, - 'end': {'line': d.line - 1, 'character': d.column + len(d.name)} + return [ + { + "uri": uris.uri_with(document.uri, path=str(d.module_path)) + if d.module_path + else document.uri, + "range": { + "start": {"line": d.line - 1, "character": d.column}, + "end": {"line": d.line - 1, "character": d.column + len(d.name)}, + }, } - } for d in usages if not d.in_builtin_module()] + for d in usages + if not d.in_builtin_module() + ] diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index dc61f566..be40fe41 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -30,7 +30,8 @@ def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: return {"plugins": {"rope_autoimport": {"enabled": False, "memory": False}}} -def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: # pylint: disable=too-many-return-statements +# pylint: disable=too-many-return-statements +def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: """ Check if we should insert the word_node on the given expr. @@ -60,7 +61,9 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: # pylint return _handle_first_child(first_child, expr, word_node) -def _handle_first_child(first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf) -> bool: +def _handle_first_child( + first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf +) -> bool: """Check if we suggest imports given the following first child.""" if isinstance(first_child, tree.Import): return False @@ -125,7 +128,9 @@ def _process_statements( start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"} - score = _get_score(suggestion.source, suggestion.import_statement, suggestion.name, word) + score = _get_score( + suggestion.source, suggestion.import_statement, suggestion.name, word + ) if score > _score_max: continue # TODO make this markdown @@ -147,7 +152,9 @@ def get_names(script: Script) -> Set[str]: @hookimpl -def pylsp_completions(config: Config, workspace: Workspace, document: Document, position): +def pylsp_completions( + config: Config, workspace: Workspace, document: Document, position +): """Get autoimport suggestions.""" line = document.lines[position["line"]] expr = parso.parse(line) @@ -175,7 +182,9 @@ def _document(import_statement: str) -> str: return """# Auto-Import\n""" + import_statement -def _get_score(source: int, full_statement: str, suggested_name: str, desired_name) -> int: +def _get_score( + source: int, full_statement: str, suggested_name: str, desired_name +) -> int: import_length = len("import") full_statement_score = len(full_statement) - import_length suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 @@ -191,13 +200,17 @@ def _sort_import(score: int) -> str: return "[z" + str(score).rjust(_score_pow, "0") -def _reload_cache(config: Config, workspace: Workspace, files: Optional[List[Document]] = None): +def _reload_cache( + config: Config, workspace: Workspace, files: Optional[List[Document]] = None +): memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) task_handle = PylspTaskHandle(workspace) resources: Optional[List[Resource]] = ( - None if files is None else [document._rope_resource(rope_config) for document in files] + None + if files is None + else [document._rope_resource(rope_config) for document in files] ) autoimport.generate_cache(task_handle=task_handle, resources=resources) autoimport.generate_modules_cache(task_handle=task_handle) diff --git a/pylsp/plugins/rope_completion.py b/pylsp/plugins/rope_completion.py index 5bb36a5f..ca0d4349 100644 --- a/pylsp/plugins/rope_completion.py +++ b/pylsp/plugins/rope_completion.py @@ -13,21 +13,18 @@ @hookimpl def pylsp_settings(): # Default rope_completion to disabled - return {'plugins': {'rope_completion': {'enabled': False, 'eager': False}}} + return {"plugins": {"rope_completion": {"enabled": False, "eager": False}}} def _resolve_completion(completion, data, markup_kind): # pylint: disable=broad-except try: - doc = _utils.format_docstring( - data.get_doc(), - markup_kind=markup_kind - ) + doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind) except Exception as e: log.debug("Failed to resolve Rope completion: %s", e) doc = "" - completion['detail'] = '{0} {1}'.format(data.scope or "", data.name) - completion['documentation'] = doc + completion["detail"] = "{0} {1}".format(data.scope or "", data.name) + completion["documentation"] = doc return completion @@ -35,30 +32,37 @@ def _resolve_completion(completion, data, markup_kind): 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) + 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 - # So we only skip import completions when the cursor is one space after `import` - 'line': position['line'], 'character': max(position['character'] - 1, 0), - }) - if word == 'import': + word = document.word_at_position( + { + # The -1 should really be trying to look at the previous word, but that might be quite expensive + # So we only skip import completions when the cursor is one space after `import` + "line": position["line"], + "character": max(position["character"] - 1, 0), + } + ) + if word == "import": return None offset = document.offset_at_position(position) - rope_config = config.settings(document_path=document.path).get('rope', {}) + rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) document_rope = document._rope_resource(rope_config) - completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) - item_capabilities = completion_capabilities.get('completionItem', {}) - supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + completion_capabilities = config.capabilities.get("textDocument", {}).get( + "completion", {} + ) + item_capabilities = completion_capabilities.get("completionItem", {}) + supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) try: - definitions = code_assist(rope_project, document.source, offset, document_rope, maxfixes=3) + definitions = code_assist( + rope_project, document.source, offset, document_rope, maxfixes=3 + ) except Exception as e: # pylint: disable=broad-except log.debug("Failed to run Rope code assist: %s", e) return [] @@ -67,21 +71,19 @@ def pylsp_completions(config, workspace, document, position): new_definitions = [] for d in definitions: item = { - 'label': d.name, - 'kind': _kind(d), - 'sortText': _sort_text(d), - 'data': { - 'doc_uri': document.uri - } + "label": d.name, + "kind": _kind(d), + "sortText": _sort_text(d), + "data": {"doc_uri": document.uri}, } if resolve_eagerly: item = _resolve_completion(item, d, preferred_markup_kind) new_definitions.append(item) # most recently retrieved completion items, used for resolution - document.shared_data['LAST_ROPE_COMPLETIONS'] = { + document.shared_data["LAST_ROPE_COMPLETIONS"] = { # label is the only required property; here it is assumed to be unique - completion['label']: (completion, data) + completion["label"]: (completion, data) for completion, data in zip(new_definitions, definitions) } @@ -93,11 +95,15 @@ def pylsp_completions(config, workspace, document, position): @hookimpl def pylsp_completion_item_resolve(config, completion_item, document): """Resolve formatted completion for given non-resolved completion""" - shared_data = document.shared_data['LAST_ROPE_COMPLETIONS'].get(completion_item['label']) - - completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) - item_capabilities = completion_capabilities.get('completionItem', {}) - supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + shared_data = document.shared_data["LAST_ROPE_COMPLETIONS"].get( + completion_item["label"] + ) + + completion_capabilities = config.capabilities.get("textDocument", {}).get( + "completion", {} + ) + item_capabilities = completion_capabilities.get("completionItem", {}) + supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) if shared_data: @@ -107,52 +113,52 @@ def pylsp_completion_item_resolve(config, completion_item, document): def _sort_text(definition): - """ Ensure builtins appear at the bottom. + """Ensure builtins appear at the bottom. Description is of format : . """ if definition.name.startswith("_"): # It's a 'hidden' func, put it next last - return 'z' + definition.name - if definition.scope == 'builtin': - return 'y' + definition.name + return "z" + definition.name + if definition.scope == "builtin": + return "y" + definition.name # Else put it at the front - return 'a' + definition.name + return "a" + definition.name def _kind(d): - """ Return the LSP type """ + """Return the LSP type""" MAP = { - 'none': lsp.CompletionItemKind.Value, - 'type': lsp.CompletionItemKind.Class, - 'tuple': lsp.CompletionItemKind.Class, - 'dict': lsp.CompletionItemKind.Class, - 'dictionary': lsp.CompletionItemKind.Class, - 'function': lsp.CompletionItemKind.Function, - 'lambda': lsp.CompletionItemKind.Function, - 'generator': lsp.CompletionItemKind.Function, - 'class': lsp.CompletionItemKind.Class, - 'instance': lsp.CompletionItemKind.Reference, - 'method': lsp.CompletionItemKind.Method, - 'builtin': lsp.CompletionItemKind.Class, - 'builtinfunction': lsp.CompletionItemKind.Function, - 'module': lsp.CompletionItemKind.Module, - 'file': lsp.CompletionItemKind.File, - 'xrange': lsp.CompletionItemKind.Class, - 'slice': lsp.CompletionItemKind.Class, - 'traceback': lsp.CompletionItemKind.Class, - 'frame': lsp.CompletionItemKind.Class, - 'buffer': lsp.CompletionItemKind.Class, - 'dictproxy': lsp.CompletionItemKind.Class, - 'funcdef': lsp.CompletionItemKind.Function, - 'property': lsp.CompletionItemKind.Property, - 'import': lsp.CompletionItemKind.Module, - 'keyword': lsp.CompletionItemKind.Keyword, - 'constant': lsp.CompletionItemKind.Variable, - 'variable': lsp.CompletionItemKind.Variable, - 'value': lsp.CompletionItemKind.Value, - 'param': lsp.CompletionItemKind.Variable, - 'statement': lsp.CompletionItemKind.Keyword, + "none": lsp.CompletionItemKind.Value, + "type": lsp.CompletionItemKind.Class, + "tuple": lsp.CompletionItemKind.Class, + "dict": lsp.CompletionItemKind.Class, + "dictionary": lsp.CompletionItemKind.Class, + "function": lsp.CompletionItemKind.Function, + "lambda": lsp.CompletionItemKind.Function, + "generator": lsp.CompletionItemKind.Function, + "class": lsp.CompletionItemKind.Class, + "instance": lsp.CompletionItemKind.Reference, + "method": lsp.CompletionItemKind.Method, + "builtin": lsp.CompletionItemKind.Class, + "builtinfunction": lsp.CompletionItemKind.Function, + "module": lsp.CompletionItemKind.Module, + "file": lsp.CompletionItemKind.File, + "xrange": lsp.CompletionItemKind.Class, + "slice": lsp.CompletionItemKind.Class, + "traceback": lsp.CompletionItemKind.Class, + "frame": lsp.CompletionItemKind.Class, + "buffer": lsp.CompletionItemKind.Class, + "dictproxy": lsp.CompletionItemKind.Class, + "funcdef": lsp.CompletionItemKind.Function, + "property": lsp.CompletionItemKind.Property, + "import": lsp.CompletionItemKind.Module, + "keyword": lsp.CompletionItemKind.Keyword, + "constant": lsp.CompletionItemKind.Variable, + "variable": lsp.CompletionItemKind.Variable, + "value": lsp.CompletionItemKind.Value, + "param": lsp.CompletionItemKind.Variable, + "statement": lsp.CompletionItemKind.Keyword, } return MAP.get(d.type) diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py index d9ebab5c..f59ba890 100644 --- a/pylsp/plugins/rope_rename.py +++ b/pylsp/plugins/rope_rename.py @@ -14,46 +14,47 @@ @hookimpl def pylsp_settings(): # Default rope_rename to disabled - return {'plugins': {'rope_rename': {'enabled': False}}} + return {"plugins": {"rope_rename": {"enabled": False}}} @hookimpl def pylsp_rename(config, workspace, document, position, new_name): - rope_config = config.settings(document_path=document.path).get('rope', {}) + rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) rename = Rename( rope_project, libutils.path_to_resource(rope_project, document.path), - document.offset_at_position(position) + document.offset_at_position(position), ) - log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name) + log.debug( + "Executing rename of %s to %s", document.word_at_position(position), new_name + ) changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True) log.debug("Finished rename: %s", changeset.changes) changes = [] for change in changeset.changes: uri = uris.from_fs_path(change.resource.path) doc = workspace.get_maybe_document(uri) - changes.append({ - 'textDocument': { - 'uri': uri, - 'version': doc.version if doc else None - }, - 'edits': [ - { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': { - 'line': _num_lines(change.resource), - 'character': 0, + changes.append( + { + "textDocument": {"uri": uri, "version": doc.version if doc else None}, + "edits": [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": { + "line": _num_lines(change.resource), + "character": 0, + }, }, - }, - 'newText': change.new_contents, - } - ] - }) - return {'documentChanges': changes} + "newText": change.new_contents, + } + ], + } + ) + return {"documentChanges": changes} def _num_lines(resource): diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py index 4907a6e3..4fc93dfb 100644 --- a/pylsp/plugins/signature.py +++ b/pylsp/plugins/signature.py @@ -20,11 +20,17 @@ def pylsp_signature_help(config, document, position): signatures = document.jedi_script().get_signatures(**code_position) if not signatures: - return {'signatures': []} - - signature_capabilities = config.capabilities.get('textDocument', {}).get('signatureHelp', {}) - signature_information_support = signature_capabilities.get('signatureInformation', {}) - supported_markup_kinds = signature_information_support.get('documentationFormat', ['markdown']) + return {"signatures": []} + + signature_capabilities = config.capabilities.get("textDocument", {}).get( + "signatureHelp", {} + ) + signature_information_support = signature_capabilities.get( + "signatureInformation", {} + ) + supported_markup_kinds = signature_information_support.get( + "documentationFormat", ["markdown"] + ) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) s = signatures[0] @@ -32,32 +38,33 @@ def pylsp_signature_help(config, document, position): docstring = s.docstring() # Docstring contains one or more lines of signature, followed by empty line, followed by docstring - function_sig_lines = (docstring.split('\n\n') or [''])[0].splitlines() - function_sig = ' '.join([line.strip() for line in function_sig_lines]) + function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines() + function_sig = " ".join([line.strip() for line in function_sig_lines]) sig = { - 'label': function_sig, - 'documentation': _utils.format_docstring( - s.docstring(raw=True), - markup_kind=preferred_markup_kind - ) + "label": function_sig, + "documentation": _utils.format_docstring( + s.docstring(raw=True), markup_kind=preferred_markup_kind + ), } # If there are params, add those if s.params: - sig['parameters'] = [{ - 'label': p.name, - 'documentation': _utils.format_docstring( - _param_docs(docstring, p.name), - markup_kind=preferred_markup_kind - ) - } for p in s.params] + sig["parameters"] = [ + { + "label": p.name, + "documentation": _utils.format_docstring( + _param_docs(docstring, p.name), markup_kind=preferred_markup_kind + ), + } + for p in s.params + ] # We only return a single signature because Python doesn't allow overloading - sig_info = {'signatures': [sig], 'activeSignature': 0} + sig_info = {"signatures": [sig], "activeSignature": 0} if s.index is not None and s.params: # Then we know which parameter we're looking at - sig_info['activeParameter'] = s.index + sig_info["activeParameter"] = s.index return sig_info @@ -68,6 +75,6 @@ def _param_docs(docstring, param_name): m = regex.match(line) if not m: continue - if m.group('param') != param_name: + if m.group("param") != param_name: continue - return m.group('doc') or "" + return m.group("doc") or "" diff --git a/pylsp/plugins/symbols.py b/pylsp/plugins/symbols.py index d5925dbf..e3c961c7 100644 --- a/pylsp/plugins/symbols.py +++ b/pylsp/plugins/symbols.py @@ -18,9 +18,9 @@ def pylsp_document_symbols(config, document): # pylint: disable=too-many-branches # pylint: disable=too-many-statements - symbols_settings = config.plugin_settings('jedi_symbols') - all_scopes = symbols_settings.get('all_scopes', True) - add_import_symbols = symbols_settings.get('include_import_symbols', True) + symbols_settings = config.plugin_settings("jedi_symbols") + all_scopes = symbols_settings.get("all_scopes", True) + add_import_symbols = symbols_settings.get("include_import_symbols", True) definitions = document.jedi_names(all_scopes=all_scopes) symbols = [] exclude = set({}) @@ -33,7 +33,7 @@ def pylsp_document_symbols(config, document): if not add_import_symbols: # Skip if there's an import in the code the symbol is defined. code = d.get_line_code() - if ' import ' in code or 'import ' in code: + if " import " in code or "import " in code: continue # Skip imported symbols comparing module names. @@ -47,15 +47,15 @@ def pylsp_document_symbols(config, document): # The last element of sym_full_name is the symbol itself, so # we need to discard it to do module comparisons below. - if '.' in sym_full_name: - sym_module_name = sym_full_name.rpartition('.')[0] + if "." in sym_full_name: + sym_module_name = sym_full_name.rpartition(".")[0] else: sym_module_name = sym_full_name # This is necessary to display symbols in init files (the checks # below fail without it). - if document_dot_path.endswith('__init__'): - document_dot_path = document_dot_path.rpartition('.')[0] + if document_dot_path.endswith("__init__"): + document_dot_path = document_dot_path.rpartition(".")[0] # document_dot_path is the module where the symbol is imported, # whereas sym_module_name is the one where it was declared. @@ -63,13 +63,13 @@ def pylsp_document_symbols(config, document): # If document_dot_path is in sym_module_name, we can safely assume # that the symbol was declared in the document. imported_symbol = False - elif sym_module_name.split('.')[0] in document_dot_path.split('.'): + elif sym_module_name.split(".")[0] in document_dot_path.split("."): # If the first module in sym_module_name is one of the modules in # document_dot_path, we need to check if sym_module_name starts # with the modules in document_dot_path. - document_mods = document_dot_path.split('.') + document_mods = document_dot_path.split(".") for i in range(1, len(document_mods) + 1): - submod = '.'.join(document_mods[-i:]) + submod = ".".join(document_mods[-i:]) if sym_module_name.startswith(submod): imported_symbol = False break @@ -79,7 +79,7 @@ def pylsp_document_symbols(config, document): # to tell if the symbol was declared in the same file: if # sym_module_name starts by __main__. if imported_symbol: - if not sym_module_name.startswith('__main__'): + if not sym_module_name.startswith("__main__"): continue else: # We need to skip symbols if their definition doesn't have `full_name` info, they @@ -100,29 +100,29 @@ def pylsp_document_symbols(config, document): if kind is not None: exclude |= {tuple_range} - if d.type == 'statement': - if d.description.startswith('self'): - kind = 'field' + if d.type == "statement": + if d.description.startswith("self"): + kind = "field" symbol = { - 'name': d.name, - 'containerName': _container(d), - 'location': { - 'uri': document.uri, - 'range': _range(d), + "name": d.name, + "containerName": _container(d), + "location": { + "uri": document.uri, + "range": _range(d), }, - 'kind': _kind(d) if kind is None else _SYMBOL_KIND_MAP[kind], + "kind": _kind(d) if kind is None else _SYMBOL_KIND_MAP[kind], } symbols.append(symbol) - if d.type == 'class': + if d.type == "class": try: defined_names = list(d.defined_names()) for method in defined_names: - if method.type == 'function': - redefinitions[_tuple_range(method)] = 'method' - elif method.type == 'statement': - redefinitions[_tuple_range(method)] = 'field' + if method.type == "function": + redefinitions[_tuple_range(method)] = "method" + elif method.type == "statement": + redefinitions[_tuple_range(method)] = "field" else: redefinitions[_tuple_range(method)] = method.type definitions = list(defined_names) + definitions @@ -134,10 +134,11 @@ def pylsp_document_symbols(config, document): def _include_def(definition): return ( # Don't tend to include parameters as symbols - definition.type != 'param' and + definition.type != "param" + and # Unused vars should also be skipped - definition.name != '_' and - _kind(definition) is not None + definition.name != "_" + and _kind(definition) is not None ) @@ -161,8 +162,8 @@ def _range(definition): (start_line, start_column) = definition.start_pos (end_line, end_column) = definition.end_pos return { - 'start': {'line': start_line - 1, 'character': start_column}, - 'end': {'line': end_line - 1, 'character': end_column} + "start": {"line": start_line - 1, "character": start_column}, + "end": {"line": end_line - 1, "character": end_column}, } @@ -172,48 +173,48 @@ def _tuple_range(definition): _SYMBOL_KIND_MAP = { - 'none': SymbolKind.Variable, - 'type': SymbolKind.Class, - 'tuple': SymbolKind.Class, - 'dict': SymbolKind.Class, - 'dictionary': SymbolKind.Class, - 'function': SymbolKind.Function, - 'lambda': SymbolKind.Function, - 'generator': SymbolKind.Function, - 'class': SymbolKind.Class, - 'instance': SymbolKind.Class, - 'method': SymbolKind.Method, - 'builtin': SymbolKind.Class, - 'builtinfunction': SymbolKind.Function, - 'module': SymbolKind.Module, - 'file': SymbolKind.File, - 'xrange': SymbolKind.Array, - 'slice': SymbolKind.Class, - 'traceback': SymbolKind.Class, - 'frame': SymbolKind.Class, - 'buffer': SymbolKind.Array, - 'dictproxy': SymbolKind.Class, - 'funcdef': SymbolKind.Function, - 'property': SymbolKind.Property, - 'import': SymbolKind.Module, - 'keyword': SymbolKind.Variable, - 'constant': SymbolKind.Constant, - 'variable': SymbolKind.Variable, - 'value': SymbolKind.Variable, - 'param': SymbolKind.Variable, - 'statement': SymbolKind.Variable, - 'boolean': SymbolKind.Boolean, - 'int': SymbolKind.Number, - 'longlean': SymbolKind.Number, - 'float': SymbolKind.Number, - 'complex': SymbolKind.Number, - 'string': SymbolKind.String, - 'unicode': SymbolKind.String, - 'list': SymbolKind.Array, - 'field': SymbolKind.Field + "none": SymbolKind.Variable, + "type": SymbolKind.Class, + "tuple": SymbolKind.Class, + "dict": SymbolKind.Class, + "dictionary": SymbolKind.Class, + "function": SymbolKind.Function, + "lambda": SymbolKind.Function, + "generator": SymbolKind.Function, + "class": SymbolKind.Class, + "instance": SymbolKind.Class, + "method": SymbolKind.Method, + "builtin": SymbolKind.Class, + "builtinfunction": SymbolKind.Function, + "module": SymbolKind.Module, + "file": SymbolKind.File, + "xrange": SymbolKind.Array, + "slice": SymbolKind.Class, + "traceback": SymbolKind.Class, + "frame": SymbolKind.Class, + "buffer": SymbolKind.Array, + "dictproxy": SymbolKind.Class, + "funcdef": SymbolKind.Function, + "property": SymbolKind.Property, + "import": SymbolKind.Module, + "keyword": SymbolKind.Variable, + "constant": SymbolKind.Constant, + "variable": SymbolKind.Variable, + "value": SymbolKind.Variable, + "param": SymbolKind.Variable, + "statement": SymbolKind.Variable, + "boolean": SymbolKind.Boolean, + "int": SymbolKind.Number, + "longlean": SymbolKind.Number, + "float": SymbolKind.Number, + "complex": SymbolKind.Number, + "string": SymbolKind.String, + "unicode": SymbolKind.String, + "list": SymbolKind.Array, + "field": SymbolKind.Field, } def _kind(d): - """ Return the VSCode Symbol Type """ + """Return the VSCode Symbol Type""" return _SYMBOL_KIND_MAP.get(d.type) diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 308fd5cf..4a8111be 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -26,9 +26,9 @@ def pylsp_format_document(workspace, document, options): def pylsp_format_range(document, range, options): # pylint: disable=redefined-builtin log.info("Formatting document %s in range %s with yapf", document, range) # First we 'round' the range up/down to full lines only - range['start']['character'] = 0 - range['end']['line'] += 1 - range['end']['character'] = 0 + range["start"]["character"] = 0 + range["end"]["line"] += 1 + range["end"]["character"] = 0 # From Yapf docs: # lines: (list of tuples of integers) A list of tuples of lines, [start, end], @@ -37,21 +37,21 @@ def pylsp_format_range(document, range, options): # pylint: disable=redefined-b # than a whole file. # Add 1 for 1-indexing vs LSP's 0-indexing - lines = [(range['start']['line'] + 1, range['end']['line'] + 1)] + lines = [(range["start"]["line"] + 1, range["end"]["line"] + 1)] return _format(document, lines=lines, options=options) def get_style_config(document_path, options=None): # Exclude file if it follows the patterns for that - exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir(os.getcwd()) + exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir( + os.getcwd() + ) if file_resources.IsIgnored(document_path, exclude_patterns_from_ignore_file): return [] # Get the default styles as a string # for a preset configuration, i.e. "pep8" - style_config = file_resources.GetDefaultStyleForDir( - os.path.dirname(document_path) - ) + style_config = file_resources.GetDefaultStyleForDir(os.path.dirname(document_path)) if options is None: return style_config @@ -61,24 +61,24 @@ def get_style_config(document_path, options=None): # to pass instead of a string so that we can modify it style_config = style.CreateStyleFromConfig(style_config) - use_tabs = style_config['USE_TABS'] - indent_width = style_config['INDENT_WIDTH'] + use_tabs = style_config["USE_TABS"] + indent_width = style_config["INDENT_WIDTH"] - if options.get('tabSize') is not None: - indent_width = max(int(options.get('tabSize')), 1) + if options.get("tabSize") is not None: + indent_width = max(int(options.get("tabSize")), 1) - if options.get('insertSpaces') is not None: + if options.get("insertSpaces") is not None: # TODO is it guaranteed to be a boolean, or can it be a string? - use_tabs = not options.get('insertSpaces') + use_tabs = not options.get("insertSpaces") if use_tabs: # Indent width doesn't make sense when using tabs # the specifications state: "Size of a tab in spaces" indent_width = 1 - style_config['USE_TABS'] = use_tabs - style_config['INDENT_WIDTH'] = indent_width - style_config['CONTINUATION_INDENT_WIDTH'] = indent_width + style_config["USE_TABS"] = use_tabs + style_config["INDENT_WIDTH"] = indent_width + style_config["CONTINUATION_INDENT_WIDTH"] = indent_width for style_option, value in options.items(): # Apply arbitrary options passed as formatter options @@ -109,39 +109,34 @@ def diff_to_text_edits(diff, eol_chars): prev_line_no = change.old - 1 elif change.new: # addition - text_edits.append({ - 'range': { - 'start': { - 'line': prev_line_no + 1, - 'character': 0 + text_edits.append( + { + "range": { + "start": {"line": prev_line_no + 1, "character": 0}, + "end": {"line": prev_line_no + 1, "character": 0}, }, - 'end': { - 'line': prev_line_no + 1, - 'character': 0 - } - }, - 'newText': change.line + eol_chars - }) + "newText": change.line + eol_chars, + } + ) elif change.old: # remove lsp_line_no = change.old - 1 - text_edits.append({ - 'range': { - 'start': { - 'line': lsp_line_no, - 'character': 0 + text_edits.append( + { + "range": { + "start": {"line": lsp_line_no, "character": 0}, + "end": { + # From LSP spec: + # If you want to specify a range that contains a line + # including the line ending character(s) then use an + # end position denoting the start of the next line. + "line": lsp_line_no + 1, + "character": 0, + }, }, - 'end': { - # From LSP spec: - # If you want to specify a range that contains a line - # including the line ending character(s) then use an - # end position denoting the start of the next line. - 'line': lsp_line_no + 1, - 'character': 0 - } - }, - 'newText': '' - }) + "newText": "", + } + ) prev_line_no = lsp_line_no return text_edits @@ -157,22 +152,18 @@ def ensure_eof_new_line(document, eol_chars, text_edits): lines = document.lines last_line_number = len(lines) - 1 - if text_edits and text_edits[-1]['range']['start']['line'] >= last_line_number: + if text_edits and text_edits[-1]["range"]["start"]["line"] >= last_line_number: return - text_edits.append({ - 'range': { - 'start': { - 'line': last_line_number, - 'character': 0 + text_edits.append( + { + "range": { + "start": {"line": last_line_number, "character": 0}, + "end": {"line": last_line_number + 1, "character": 0}, }, - 'end': { - 'line': last_line_number + 1, - 'character': 0 - } - }, - 'newText': lines[-1] + eol_chars - }) + "newText": lines[-1] + eol_chars, + } + ) def _format(document, lines=None, options=None): @@ -180,10 +171,10 @@ def _format(document, lines=None, options=None): # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' # and restore them below when adding new lines eol_chars = get_eol_chars(source) - if eol_chars in ['\r', '\r\n']: - source = source.replace(eol_chars, '\n') + if eol_chars in ["\r", "\r\n"]: + source = source.replace(eol_chars, "\n") else: - eol_chars = '\n' + eol_chars = "\n" style_config = get_style_config(document_path=document.path, options=options) @@ -192,7 +183,7 @@ def _format(document, lines=None, options=None): lines=lines, filename=document.filename, print_diff=True, - style_config=style_config + style_config=style_config, ) if not changed: diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 9302c4d2..faa93423 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -25,8 +25,8 @@ LINT_DEBOUNCE_S = 0.5 # 500 ms PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s MAX_WORKERS = 64 -PYTHON_FILE_EXTENSIONS = ('.py', '.pyi') -CONFIG_FILEs = ('pycodestyle.cfg', 'setup.cfg', 'tox.ini', '.flake8') +PYTHON_FILE_EXTENSIONS = (".py", ".pyi") +CONFIG_FILEs = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8") class _StreamHandlerWrapper(socketserver.StreamRequestHandler): @@ -42,7 +42,7 @@ def handle(self): try: self.delegate.start() except OSError as e: - if os.name == 'nt': + if os.name == "nt": # Catch and pass on ConnectionResetError when parent process # dies # pylint: disable=no-member, undefined-variable @@ -54,49 +54,54 @@ def handle(self): def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class): if not issubclass(handler_class, PythonLSPServer): - raise ValueError('Handler class must be an instance of PythonLSPServer') + raise ValueError("Handler class must be an instance of PythonLSPServer") def shutdown_server(check_parent_process, *args): # pylint: disable=unused-argument if check_parent_process: - log.debug('Shutting down server') + log.debug("Shutting down server") # Shutdown call must be done on a thread, to prevent deadlocks stop_thread = threading.Thread(target=server.shutdown) stop_thread.start() # Construct a custom wrapper class around the user's handler_class wrapper_class = type( - handler_class.__name__ + 'Handler', + handler_class.__name__ + "Handler", (_StreamHandlerWrapper,), - {'DELEGATE_CLASS': partial(handler_class, - check_parent_process=check_parent_process), - 'SHUTDOWN_CALL': partial(shutdown_server, check_parent_process)} + { + "DELEGATE_CLASS": partial( + handler_class, check_parent_process=check_parent_process + ), + "SHUTDOWN_CALL": partial(shutdown_server, check_parent_process), + }, ) - server = socketserver.TCPServer((bind_addr, port), wrapper_class, bind_and_activate=False) + server = socketserver.TCPServer( + (bind_addr, port), wrapper_class, bind_and_activate=False + ) server.allow_reuse_address = True try: server.server_bind() server.server_activate() - log.info('Serving %s on (%s, %s)', handler_class.__name__, bind_addr, port) + log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port) server.serve_forever() finally: - log.info('Shutting down') + log.info("Shutting down") server.server_close() def start_io_lang_server(rfile, wfile, check_parent_process, handler_class): if not issubclass(handler_class, PythonLSPServer): - raise ValueError('Handler class must be an instance of PythonLSPServer') - log.info('Starting %s IO language server', handler_class.__name__) + raise ValueError("Handler class must be an instance of PythonLSPServer") + log.info("Starting %s IO language server", handler_class.__name__) server = handler_class(rfile, wfile, check_parent_process) server.start() def start_ws_lang_server(port, check_parent_process, handler_class): if not issubclass(handler_class, PythonLSPServer): - raise ValueError('Handler class must be an instance of PythonLSPServer') + raise ValueError("Handler class must be an instance of PythonLSPServer") # pylint: disable=import-outside-toplevel @@ -106,9 +111,12 @@ def start_ws_lang_server(port, check_parent_process, handler_class): from concurrent.futures import ThreadPoolExecutor import websockets except ImportError as e: - raise ImportError("websocket modules missing. Please run pip install 'python-lsp-server[websockets]") from e + raise ImportError( + "websocket modules missing. Please run pip install 'python-lsp-server[websockets]" + ) from e with ThreadPoolExecutor(max_workers=10) as tpool: + async def pylsp_ws(websocket): log.debug("Creating LSP object") @@ -117,8 +125,12 @@ async def pylsp_ws(websocket): # Not using default stream reader and writer. # Instead using a consumer based approach to handle processed requests - pylsp_handler = handler_class(rx=None, tx=None, consumer=response_handler, - check_parent_process=check_parent_process) + pylsp_handler = handler_class( + rx=None, + tx=None, + consumer=response_handler, + check_parent_process=check_parent_process, + ) async for message in websocket: try: @@ -148,13 +160,15 @@ async def run_server(): class PythonLSPServer(MethodDispatcher): - """ Implementation of the Microsoft VSCode Language Server Protocol + """Implementation of the Microsoft VSCode Language Server Protocol https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md """ # pylint: disable=too-many-public-methods,redefined-builtin - def __init__(self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None): + def __init__( + self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None + ): self.workspace = None self.config = None self.root_uri = None @@ -178,7 +192,9 @@ def __init__(self, rx, tx, check_parent_process=False, consumer=None, *, endpoin # if consumer is None, it is assumed that the default streams-based approach is being used if consumer is None: - self._endpoint = endpoint_cls(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS) + self._endpoint = endpoint_cls( + self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS + ) else: self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS) @@ -196,7 +212,7 @@ def consume(self, message): def __getitem__(self, item): """Override getitem to fallback through multiple dispatchers.""" - if self._shutdown and item != 'exit': + if self._shutdown and item != "exit": # exit is the only allowed method during shutdown log.debug("Ignoring non-exit method during shutdown: %s", item) raise KeyError @@ -233,152 +249,180 @@ def _hook(self, hook_name, doc_uri=None, **kwargs): """Calls hook_name and returns a list of results from all registered handlers""" workspace = self._match_uri_to_workspace(doc_uri) doc = workspace.get_document(doc_uri) if doc_uri else None - hook_handlers = self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins) - return hook_handlers(config=self.config, workspace=workspace, document=doc, **kwargs) + hook_handlers = self.config.plugin_manager.subset_hook_caller( + hook_name, self.config.disabled_plugins + ) + return hook_handlers( + config=self.config, workspace=workspace, document=doc, **kwargs + ) def capabilities(self): server_capabilities = { - 'codeActionProvider': True, - 'codeLensProvider': { - 'resolveProvider': False, # We may need to make this configurable + "codeActionProvider": True, + "codeLensProvider": { + "resolveProvider": False, # We may need to make this configurable }, - 'completionProvider': { - 'resolveProvider': True, # We could know everything ahead of time, but this takes time to transfer - 'triggerCharacters': ['.'], + "completionProvider": { + "resolveProvider": True, # We could know everything ahead of time, but this takes time to transfer + "triggerCharacters": ["."], }, - 'documentFormattingProvider': True, - 'documentHighlightProvider': True, - 'documentRangeFormattingProvider': True, - 'documentSymbolProvider': True, - 'definitionProvider': True, - 'executeCommandProvider': { - 'commands': flatten(self._hook('pylsp_commands')) + "documentFormattingProvider": True, + "documentHighlightProvider": True, + "documentRangeFormattingProvider": True, + "documentSymbolProvider": True, + "definitionProvider": True, + "executeCommandProvider": { + "commands": flatten(self._hook("pylsp_commands")) }, - 'hoverProvider': True, - 'referencesProvider': True, - 'renameProvider': True, - 'foldingRangeProvider': True, - 'signatureHelpProvider': { - 'triggerCharacters': ['(', ',', '='] - }, - 'textDocumentSync': { - 'change': lsp.TextDocumentSyncKind.INCREMENTAL, - 'save': { - 'includeText': True, + "hoverProvider": True, + "referencesProvider": True, + "renameProvider": True, + "foldingRangeProvider": True, + "signatureHelpProvider": {"triggerCharacters": ["(", ",", "="]}, + "textDocumentSync": { + "change": lsp.TextDocumentSyncKind.INCREMENTAL, + "save": { + "includeText": True, }, - 'openClose': True, + "openClose": True, }, - 'notebookDocumentSync': { - 'notebookSelector': { - 'cells': [{'language': 'python'}] - } + "notebookDocumentSync": { + "notebookSelector": {"cells": [{"language": "python"}]} }, - 'workspace': { - 'workspaceFolders': { - 'supported': True, - 'changeNotifications': True - } + "workspace": { + "workspaceFolders": {"supported": True, "changeNotifications": True} }, - 'experimental': merge( - self._hook('pylsp_experimental_capabilities')) + "experimental": merge(self._hook("pylsp_experimental_capabilities")), } - log.info('Server capabilities: %s', server_capabilities) + log.info("Server capabilities: %s", server_capabilities) return server_capabilities - 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) + 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 '' + rootUri = uris.from_fs_path(rootPath) if rootPath is not None else "" self.workspaces.pop(self.root_uri, None) self.root_uri = rootUri - self.config = config.Config(rootUri, initializationOptions or {}, - processId, _kwargs.get('capabilities', {})) + self.config = config.Config( + rootUri, + initializationOptions or {}, + 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'] + 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) + 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.workspaces[uri] = Workspace(uri, self._endpoint, workspace_config) - self._dispatchers = self._hook('pylsp_dispatchers') - self._hook('pylsp_initialize') + self._dispatchers = self._hook("pylsp_dispatchers") + self._hook("pylsp_initialize") + + if ( + self._check_parent_process + and processId is not None + and self.watching_thread is None + ): - if self._check_parent_process and processId is not None and self.watching_thread is None: def watch_parent_process(pid): # exit when the given pid is not alive if not _utils.is_process_alive(pid): log.info("parent process %s is not alive, exiting!", pid) self.m_exit() else: - threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start() + threading.Timer( + PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid] + ).start() - self.watching_thread = threading.Thread(target=watch_parent_process, args=(processId,)) + self.watching_thread = threading.Thread( + target=watch_parent_process, args=(processId,) + ) self.watching_thread.daemon = True self.watching_thread.start() # Get our capabilities return { - 'capabilities': self.capabilities(), - 'serverInfo': { - 'name': 'pylsp', - 'version': __version__, + "capabilities": self.capabilities(), + "serverInfo": { + "name": "pylsp", + "version": __version__, }, } def m_initialized(self, **_kwargs): - self._hook('pylsp_initialized') + self._hook("pylsp_initialized") def code_actions(self, doc_uri, range, context): - return flatten(self._hook('pylsp_code_actions', doc_uri, range=range, context=context)) + return flatten( + self._hook("pylsp_code_actions", doc_uri, range=range, context=context) + ) def code_lens(self, doc_uri): - return flatten(self._hook('pylsp_code_lens', doc_uri)) + return flatten(self._hook("pylsp_code_lens", doc_uri)) def completions(self, doc_uri, position): - completions = self._hook('pylsp_completions', doc_uri, position=position) - return { - 'isIncomplete': False, - 'items': flatten(completions) - } + completions = self._hook("pylsp_completions", doc_uri, position=position) + return {"isIncomplete": False, "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) + 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)) + return flatten(self._hook("pylsp_definitions", doc_uri, position=position)) def document_symbols(self, doc_uri): - return flatten(self._hook('pylsp_document_symbols', doc_uri)) + return flatten(self._hook("pylsp_document_symbols", doc_uri)) def document_did_save(self, doc_uri): return self._hook("pylsp_document_did_save", doc_uri) def execute_command(self, command, arguments): - return self._hook('pylsp_execute_command', command=command, arguments=arguments) + return self._hook("pylsp_execute_command", command=command, arguments=arguments) def format_document(self, doc_uri, options): - return lambda: self._hook('pylsp_format_document', doc_uri, options=options) + return lambda: self._hook("pylsp_format_document", doc_uri, options=options) def format_range(self, doc_uri, range, options): - return self._hook('pylsp_format_range', doc_uri, range=range, options=options) + return self._hook("pylsp_format_range", doc_uri, range=range, options=options) def highlight(self, doc_uri, position): - return flatten(self._hook('pylsp_document_highlight', doc_uri, position=position)) or None + return ( + flatten(self._hook("pylsp_document_highlight", doc_uri, position=position)) + or None + ) def hover(self, doc_uri, position): - return self._hook('pylsp_hover', doc_uri, position=position) or {'contents': ''} + return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""} - @_utils.debounce(LINT_DEBOUNCE_S, keyed_by='doc_uri') + @_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri") def lint(self, doc_uri, is_saved): # Since we're debounced, the document may no longer be open workspace = self._match_uri_to_workspace(doc_uri) @@ -390,11 +434,12 @@ def lint(self, doc_uri, is_saved): def _lint_text_document(self, doc_uri, workspace, is_saved): workspace.publish_diagnostics( - doc_uri, - flatten(self._hook('pylsp_lint', doc_uri, is_saved=is_saved)) + doc_uri, flatten(self._hook("pylsp_lint", doc_uri, is_saved=is_saved)) ) - def _lint_notebook_document(self, notebook_document, workspace): # pylint: disable=too-many-locals + def _lint_notebook_document( + self, notebook_document, workspace + ): # pylint: disable=too-many-locals """ Lint a notebook document. @@ -413,88 +458,107 @@ def _lint_notebook_document(self, notebook_document, workspace): # pylint: disa offset = 0 total_source = "" for cell in notebook_document.cells: - cell_uri = cell['document'] + cell_uri = cell["document"] cell_document = workspace.get_cell_document(cell_uri) num_lines = cell_document.line_count data = { - 'uri': cell_uri, - 'line_start': offset, - 'line_end': offset + num_lines - 1, - 'source': cell_document.source + "uri": cell_uri, + "line_start": offset, + "line_end": offset + num_lines - 1, + "source": cell_document.source, } cell_list.append(data) if offset == 0: total_source = cell_document.source else: - total_source += ("\n" + cell_document.source) + total_source += "\n" + cell_document.source offset += num_lines workspace.put_document(random_uri, total_source) try: - document_diagnostics = flatten(self._hook('pylsp_lint', random_uri, is_saved=True)) + document_diagnostics = flatten( + self._hook("pylsp_lint", random_uri, is_saved=True) + ) # Now we need to map the diagnostics back to the correct cell and publish them. # Note: this is O(n*m) in the number of cells and diagnostics, respectively. for cell in cell_list: cell_diagnostics = [] for diagnostic in document_diagnostics: - start_line = diagnostic['range']['start']['line'] - end_line = diagnostic['range']['end']['line'] + start_line = diagnostic["range"]["start"]["line"] + end_line = diagnostic["range"]["end"]["line"] - if start_line > cell['line_end'] or end_line < cell['line_start']: + if start_line > cell["line_end"] or end_line < cell["line_start"]: continue - diagnostic['range']['start']['line'] = start_line - cell['line_start'] - diagnostic['range']['end']['line'] = end_line - cell['line_start'] + diagnostic["range"]["start"]["line"] = ( + start_line - cell["line_start"] + ) + diagnostic["range"]["end"]["line"] = end_line - cell["line_start"] cell_diagnostics.append(diagnostic) - workspace.publish_diagnostics(cell['uri'], cell_diagnostics) + workspace.publish_diagnostics(cell["uri"], cell_diagnostics) finally: workspace.rm_document(random_uri) def references(self, doc_uri, position, exclude_declaration): - return flatten(self._hook( - 'pylsp_references', doc_uri, position=position, - exclude_declaration=exclude_declaration - )) + return flatten( + self._hook( + "pylsp_references", + doc_uri, + position=position, + exclude_declaration=exclude_declaration, + ) + ) def rename(self, doc_uri, position, new_name): - return self._hook('pylsp_rename', doc_uri, position=position, new_name=new_name) + return self._hook("pylsp_rename", doc_uri, position=position, new_name=new_name) def signature_help(self, doc_uri, position): - return self._hook('pylsp_signature_help', doc_uri, position=position) + return self._hook("pylsp_signature_help", doc_uri, position=position) def folding(self, doc_uri): - return flatten(self._hook('pylsp_folding_range', 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_notebook_document__did_open(self, notebookDocument=None, cellTextDocuments=None, **_kwargs): - workspace = self._match_uri_to_workspace(notebookDocument['uri']) + def m_notebook_document__did_open( + self, notebookDocument=None, cellTextDocuments=None, **_kwargs + ): + workspace = self._match_uri_to_workspace(notebookDocument["uri"]) workspace.put_notebook_document( - notebookDocument['uri'], - notebookDocument['notebookType'], - cells=notebookDocument['cells'], - version=notebookDocument.get('version'), - metadata=notebookDocument.get('metadata') + notebookDocument["uri"], + notebookDocument["notebookType"], + cells=notebookDocument["cells"], + version=notebookDocument.get("version"), + metadata=notebookDocument.get("metadata"), ) - for cell in (cellTextDocuments or []): - workspace.put_cell_document(cell['uri'], cell['languageId'], cell['text'], version=cell.get('version')) - self.lint(notebookDocument['uri'], is_saved=True) - - def m_notebook_document__did_close(self, notebookDocument=None, cellTextDocuments=None, **_kwargs): - workspace = self._match_uri_to_workspace(notebookDocument['uri']) - for cell in (cellTextDocuments or []): - workspace.publish_diagnostics(cell['uri'], []) - workspace.rm_document(cell['uri']) - workspace.rm_document(notebookDocument['uri']) - - def m_notebook_document__did_change(self, notebookDocument=None, change=None, **_kwargs): + for cell in cellTextDocuments or []: + workspace.put_cell_document( + cell["uri"], + cell["languageId"], + cell["text"], + version=cell.get("version"), + ) + self.lint(notebookDocument["uri"], is_saved=True) + + def m_notebook_document__did_close( + self, notebookDocument=None, cellTextDocuments=None, **_kwargs + ): + workspace = self._match_uri_to_workspace(notebookDocument["uri"]) + for cell in cellTextDocuments or []: + workspace.publish_diagnostics(cell["uri"], []) + workspace.rm_document(cell["uri"]) + workspace.rm_document(notebookDocument["uri"]) + + def m_notebook_document__did_change( + self, notebookDocument=None, change=None, **_kwargs + ): """ Changes to the notebook document. @@ -506,157 +570,189 @@ def m_notebook_document__did_change(self, notebookDocument=None, change=None, ** 4.1 Cell metadata changed 4.2 Cell source changed """ - workspace = self._match_uri_to_workspace(notebookDocument['uri']) + workspace = self._match_uri_to_workspace(notebookDocument["uri"]) - if change.get('metadata'): + if change.get("metadata"): # Case 1 - workspace.update_notebook_metadata(notebookDocument['uri'], change.get('metadata')) + workspace.update_notebook_metadata( + notebookDocument["uri"], change.get("metadata") + ) - cells = change.get('cells') + cells = change.get("cells") if cells: # Change to cells - structure = cells.get('structure') + structure = cells.get("structure") if structure: # Case 2 or 3 - notebook_cell_array_change = structure['array'] - start = notebook_cell_array_change['start'] - cell_delete_count = notebook_cell_array_change['deleteCount'] + notebook_cell_array_change = structure["array"] + start = notebook_cell_array_change["start"] + cell_delete_count = notebook_cell_array_change["deleteCount"] if cell_delete_count == 0: # Case 2 # Cell documents - for cell_document in structure['didOpen']: + for cell_document in structure["didOpen"]: workspace.put_cell_document( - cell_document['uri'], - cell_document['languageId'], - cell_document['text'], - cell_document.get('version') + cell_document["uri"], + cell_document["languageId"], + cell_document["text"], + cell_document.get("version"), ) # Cell metadata which is added to Notebook - workspace.add_notebook_cells(notebookDocument['uri'], notebook_cell_array_change['cells'], start) + workspace.add_notebook_cells( + notebookDocument["uri"], + notebook_cell_array_change["cells"], + start, + ) else: # Case 3 # Cell documents - for cell_document in structure['didClose']: - workspace.rm_document(cell_document['uri']) - workspace.publish_diagnostics(cell_document['uri'], []) + for cell_document in structure["didClose"]: + workspace.rm_document(cell_document["uri"]) + workspace.publish_diagnostics(cell_document["uri"], []) # Cell metadata which is removed from Notebook - workspace.remove_notebook_cells(notebookDocument['uri'], start, cell_delete_count) + workspace.remove_notebook_cells( + notebookDocument["uri"], start, cell_delete_count + ) - data = cells.get('data') + data = cells.get("data") if data: # Case 4.1 for cell in data: # update NotebookDocument.cells properties pass - text_content = cells.get('textContent') + text_content = cells.get("textContent") if text_content: # Case 4.2 for cell in text_content: - cell_uri = cell['document']['uri'] + cell_uri = cell["document"]["uri"] # Even though the protocol says that `changes` is an array, we assume that it's always a single # element array that contains the last change to the cell source. - workspace.update_document(cell_uri, cell['changes'][0]) - self.lint(notebookDocument['uri'], is_saved=True) + workspace.update_document(cell_uri, cell["changes"][0]) + self.lint(notebookDocument["uri"], is_saved=True) def m_text_document__did_close(self, textDocument=None, **_kwargs): - workspace = self._match_uri_to_workspace(textDocument['uri']) - workspace.publish_diagnostics(textDocument['uri'], []) - workspace.rm_document(textDocument['uri']) + workspace = self._match_uri_to_workspace(textDocument["uri"]) + workspace.publish_diagnostics(textDocument["uri"], []) + workspace.rm_document(textDocument["uri"]) def m_text_document__did_open(self, textDocument=None, **_kwargs): - workspace = self._match_uri_to_workspace(textDocument['uri']) - workspace.put_document(textDocument['uri'], textDocument['text'], version=textDocument.get('version')) - self._hook('pylsp_document_did_open', textDocument['uri']) - self.lint(textDocument['uri'], is_saved=True) + workspace = self._match_uri_to_workspace(textDocument["uri"]) + workspace.put_document( + textDocument["uri"], + textDocument["text"], + version=textDocument.get("version"), + ) + self._hook("pylsp_document_did_open", textDocument["uri"]) + self.lint(textDocument["uri"], is_saved=True) - def m_text_document__did_change(self, contentChanges=None, textDocument=None, **_kwargs): - workspace = self._match_uri_to_workspace(textDocument['uri']) + def m_text_document__did_change( + self, contentChanges=None, textDocument=None, **_kwargs + ): + workspace = self._match_uri_to_workspace(textDocument["uri"]) for change in contentChanges: workspace.update_document( - textDocument['uri'], - change, - version=textDocument.get('version') + textDocument["uri"], change, version=textDocument.get("version") ) - self.lint(textDocument['uri'], is_saved=False) + self.lint(textDocument["uri"], is_saved=False) def m_text_document__did_save(self, textDocument=None, **_kwargs): - self.lint(textDocument['uri'], is_saved=True) - self.document_did_save(textDocument['uri']) + self.lint(textDocument["uri"], is_saved=True) + self.document_did_save(textDocument["uri"]) - def m_text_document__code_action(self, textDocument=None, range=None, context=None, **_kwargs): - return self.code_actions(textDocument['uri'], range, context) + def m_text_document__code_action( + self, textDocument=None, range=None, context=None, **_kwargs + ): + return self.code_actions(textDocument["uri"], range, context) def m_text_document__code_lens(self, textDocument=None, **_kwargs): - return self.code_lens(textDocument['uri']) + return self.code_lens(textDocument["uri"]) def m_text_document__completion(self, textDocument=None, position=None, **_kwargs): - return self.completions(textDocument['uri'], position) + return self.completions(textDocument["uri"], position) def m_text_document__definition(self, textDocument=None, position=None, **_kwargs): - return self.definitions(textDocument['uri'], position) + return self.definitions(textDocument["uri"], position) - def m_text_document__document_highlight(self, textDocument=None, position=None, **_kwargs): - return self.highlight(textDocument['uri'], position) + def m_text_document__document_highlight( + self, textDocument=None, position=None, **_kwargs + ): + return self.highlight(textDocument["uri"], position) def m_text_document__hover(self, textDocument=None, position=None, **_kwargs): - return self.hover(textDocument['uri'], position) + return self.hover(textDocument["uri"], position) def m_text_document__document_symbol(self, textDocument=None, **_kwargs): - return self.document_symbols(textDocument['uri']) + return self.document_symbols(textDocument["uri"]) def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs): - return self.format_document(textDocument['uri'], options) + return self.format_document(textDocument["uri"], options) - def m_text_document__rename(self, textDocument=None, position=None, newName=None, **_kwargs): - return self.rename(textDocument['uri'], position, newName) + def m_text_document__rename( + self, textDocument=None, position=None, newName=None, **_kwargs + ): + return self.rename(textDocument["uri"], position, newName) def m_text_document__folding_range(self, textDocument=None, **_kwargs): - return self.folding(textDocument['uri']) + return self.folding(textDocument["uri"]) - def m_text_document__range_formatting(self, textDocument=None, range=None, options=None, **_kwargs): - return self.format_range(textDocument['uri'], range, options) + def m_text_document__range_formatting( + self, textDocument=None, range=None, options=None, **_kwargs + ): + return self.format_range(textDocument["uri"], range, options) - def m_text_document__references(self, textDocument=None, position=None, context=None, **_kwargs): - exclude_declaration = not context['includeDeclaration'] - return self.references(textDocument['uri'], position, exclude_declaration) + def m_text_document__references( + self, textDocument=None, position=None, context=None, **_kwargs + ): + exclude_declaration = not context["includeDeclaration"] + return self.references(textDocument["uri"], position, exclude_declaration) - def m_text_document__signature_help(self, textDocument=None, position=None, **_kwargs): - return self.signature_help(textDocument['uri'], position) + def m_text_document__signature_help( + self, textDocument=None, position=None, **_kwargs + ): + return self.signature_help(textDocument["uri"], position) def m_workspace__did_change_configuration(self, settings=None): if self.config is not None: - self.config.update((settings or {}).get('pylsp', {})) + self.config.update((settings or {}).get("pylsp", {})) for workspace in self.workspaces.values(): workspace.update_config(settings) for doc_uri in workspace.documents: self.lint(doc_uri, is_saved=False) - def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs): # pylint: disable=too-many-locals + def m_workspace__did_change_workspace_folders( + self, event=None, **_kwargs + ): # pylint: disable=too-many-locals if event is None: return - added = event.get('added', []) - removed = event.get('removed', []) + added = event.get("added", []) + removed = event.get("removed", []) for removed_info in removed: - if 'uri' in removed_info: - removed_uri = removed_info['uri'] + if "uri" in removed_info: + removed_uri = removed_info["uri"] self.workspaces.pop(removed_uri, None) for added_info in added: - if 'uri' in added_info: - added_uri = added_info['uri'] + if "uri" in added_info: + added_uri = added_info["uri"] workspace_config = config.Config( - added_uri, self.config._init_opts, - self.config._process_id, self.config._capabilities) + added_uri, + self.config._init_opts, + self.config._process_id, + self.config._capabilities, + ) workspace_config.update(self.config._settings) self.workspaces[added_uri] = Workspace( - added_uri, self._endpoint, workspace_config) + added_uri, self._endpoint, workspace_config + ) - root_workspace_removed = any(removed_info['uri'] == self.root_uri for removed_info in removed) - workspace_added = len(added) > 0 and 'uri' in added[0] + root_workspace_removed = any( + removed_info["uri"] == self.root_uri for removed_info in removed + ) + workspace_added = len(added) > 0 and "uri" in added[0] if root_workspace_removed and workspace_added: - added_uri = added[0]['uri'] + added_uri = added[0]["uri"] self.root_uri = added_uri new_root_workspace = self.workspaces[added_uri] self.config = new_root_workspace._config @@ -665,7 +761,7 @@ def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs): # p # NOTE: Removing the root workspace can only happen when the server # is closed, thus the else condition of this if can never happen. if self.workspaces: - log.debug('Root workspace deleted!') + log.debug("Root workspace deleted!") available_workspaces = sorted(self.workspaces) first_workspace = available_workspaces[0] new_root_workspace = self.workspaces[first_workspace] @@ -684,10 +780,10 @@ def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs): # p def m_workspace__did_change_watched_files(self, changes=None, **_kwargs): changed_py_files = set() config_changed = False - for d in (changes or []): - if d['uri'].endswith(PYTHON_FILE_EXTENSIONS): - changed_py_files.add(d['uri']) - elif d['uri'].endswith(CONFIG_FILEs): + for d in changes or []: + if d["uri"].endswith(PYTHON_FILE_EXTENSIONS): + changed_py_files.add(d["uri"]) + elif d["uri"].endswith(CONFIG_FILEs): config_changed = True if config_changed: diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py index 24d74eeb..07be2aa4 100644 --- a/pylsp/text_edit.py +++ b/pylsp/text_edit.py @@ -1,28 +1,31 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. + def get_well_formatted_range(lsp_range): - start = lsp_range['start'] - end = lsp_range['end'] + start = lsp_range["start"] + end = lsp_range["end"] - if start['line'] > end['line'] or (start['line'] == end['line'] and start['character'] > end['character']): - return {'start': end, 'end': start} + if start["line"] > end["line"] or ( + start["line"] == end["line"] and start["character"] > end["character"] + ): + return {"start": end, "end": start} return lsp_range def get_well_formatted_edit(text_edit): - lsp_range = get_well_formatted_range(text_edit['range']) - if lsp_range != text_edit['range']: - return {'newText': text_edit['newText'], 'range': lsp_range} + lsp_range = get_well_formatted_range(text_edit["range"]) + if lsp_range != text_edit["range"]: + return {"newText": text_edit["newText"], "range": lsp_range} return text_edit def compare_text_edits(a, b): - diff = a['range']['start']['line'] - b['range']['start']['line'] + diff = a["range"]["start"]["line"] - b["range"]["start"]["line"] if diff == 0: - return a['range']['start']['character'] - b['range']['start']['character'] + return a["range"]["start"]["character"] - b["range"]["start"]["character"] return diff @@ -79,16 +82,16 @@ def apply_text_edits(doc, text_edits): last_modified_offset = 0 spans = [] for e in sorted_edits: - start_offset = doc.offset_at_position(e['range']['start']) + start_offset = doc.offset_at_position(e["range"]["start"]) if start_offset < last_modified_offset: - raise OverLappingTextEditException('overlapping edit') + raise OverLappingTextEditException("overlapping edit") if start_offset > last_modified_offset: spans.append(text[last_modified_offset:start_offset]) - if len(e['newText']): - spans.append(e['newText']) - last_modified_offset = doc.offset_at_position(e['range']['end']) + if len(e["newText"]): + spans.append(e["newText"]) + last_modified_offset = doc.offset_at_position(e["range"]["end"]) spans.append(text[last_modified_offset:]) - return ''.join(spans) + return "".join(spans) diff --git a/pylsp/uris.py b/pylsp/uris.py index 552761fc..7e5c4d05 100644 --- a/pylsp/uris.py +++ b/pylsp/uris.py @@ -9,7 +9,7 @@ from urllib import parse from pylsp import IS_WIN -RE_DRIVE_LETTER_PATH = re.compile(r'^\/[a-zA-Z]:') +RE_DRIVE_LETTER_PATH = re.compile(r"^\/[a-zA-Z]:") def urlparse(uri): @@ -21,7 +21,7 @@ def urlparse(uri): parse.unquote(path), parse.unquote(params), parse.unquote(query), - parse.unquote(fragment) + parse.unquote(fragment), ) @@ -35,14 +35,16 @@ def urlunparse(parts): else: quoted_path = parse.quote(path) - return parse.urlunparse(( - parse.quote(scheme), - parse.quote(netloc), - quoted_path, - parse.quote(params), - parse.quote(query), - parse.quote(fragment) - )) + return parse.urlunparse( + ( + parse.quote(scheme), + parse.quote(netloc), + quoted_path, + parse.quote(params), + parse.quote(query), + parse.quote(fragment), + ) + ) def to_fs_path(uri): @@ -55,7 +57,7 @@ def to_fs_path(uri): # scheme://netloc/path;parameters?query#fragment scheme, netloc, path, _params, _query, _fragment = urlparse(uri) - if netloc and path and scheme == 'file': + if netloc and path and scheme == "file": # unc path: file://shares/c$/far/boo value = "//{}{}".format(netloc, path) @@ -68,49 +70,55 @@ def to_fs_path(uri): value = path if IS_WIN: - value = value.replace('/', '\\') + value = value.replace("/", "\\") return value def from_fs_path(path): """Returns a URI for the given filesystem path.""" - scheme = 'file' - params, query, fragment = '', '', '' + scheme = "file" + params, query, fragment = "", "", "" path, netloc = _normalize_win_path(path) return urlunparse((scheme, netloc, path, params, query, fragment)) -def uri_with(uri, scheme=None, netloc=None, path=None, params=None, query=None, fragment=None): +def uri_with( + uri, scheme=None, netloc=None, path=None, params=None, query=None, fragment=None +): """Return a URI with the given part(s) replaced. Parts are decoded / encoded. """ - old_scheme, old_netloc, old_path, old_params, old_query, old_fragment = urlparse(uri) + old_scheme, old_netloc, old_path, old_params, old_query, old_fragment = urlparse( + uri + ) path, _netloc = _normalize_win_path(path) - return urlunparse(( - scheme or old_scheme, - netloc or old_netloc, - path or old_path, - params or old_params, - query or old_query, - fragment or old_fragment - )) + return urlunparse( + ( + scheme or old_scheme, + netloc or old_netloc, + path or old_path, + params or old_params, + query or old_query, + fragment or old_fragment, + ) + ) def _normalize_win_path(path): - netloc = '' + netloc = "" # normalize to fwd-slashes on windows, # on other systems bwd-slaches are valid # filename character, eg /f\oo/ba\r.txt if IS_WIN: - path = path.replace('\\', '/') + path = path.replace("\\", "/") # check for authority as used in UNC shares # or use the path as given - if path[:2] == '//': - idx = path.index('/', 2) + if path[:2] == "//": + idx = path.index("/", 2) if idx == -1: netloc = path[2:] else: @@ -119,8 +127,8 @@ def _normalize_win_path(path): # Ensure that path starts with a slash # or that it is at least a slash - if not path.startswith('/'): - path = '/' + path + if not path.startswith("/"): + path = "/" + path # Normalize drive paths to lower case if RE_DRIVE_LETTER_PATH.match(path): diff --git a/pylsp/workspace.py b/pylsp/workspace.py index e5fccc9c..d9868846 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -20,28 +20,29 @@ DEFAULT_AUTO_IMPORT_MODULES = ["numpy"] # TODO: this is not the best e.g. we capture numbers -RE_START_WORD = re.compile('[A-Za-z_0-9]*$') -RE_END_WORD = re.compile('^[A-Za-z_0-9]*') +RE_START_WORD = re.compile("[A-Za-z_0-9]*$") +RE_END_WORD = re.compile("^[A-Za-z_0-9]*") def lock(method): """Define an atomic region over a method.""" + @functools.wraps(method) def wrapper(self, *args, **kwargs): with self._lock: return method(self, *args, **kwargs) + return wrapper class Workspace: - # pylint: disable=too-many-public-methods - M_PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics' - M_PROGRESS = '$/progress' - M_INITIALIZE_PROGRESS = 'window/workDoneProgress/create' - M_APPLY_EDIT = 'workspace/applyEdit' - M_SHOW_MESSAGE = 'window/showMessage' + M_PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" + M_PROGRESS = "$/progress" + M_INITIALIZE_PROGRESS = "window/workDoneProgress/create" + M_APPLY_EDIT = "workspace/applyEdit" + M_SHOW_MESSAGE = "window/showMessage" def __init__(self, root_uri, endpoint, config=None): self._config = config @@ -62,6 +63,7 @@ def __init__(self, root_uri, endpoint, config=None): def _rope_autoimport(self, rope_config: Optional, memory: bool = False): # pylint: disable=import-outside-toplevel from rope.contrib.autoimport.sqlite import AutoImport + if self.__rope_autoimport is None: project = self._rope_project_builder(rope_config) self.__rope_autoimport = AutoImport(project, memory=memory) @@ -73,15 +75,16 @@ def _rope_project_builder(self, rope_config): # TODO: we could keep track of dirty files and validate only those if self.__rope is None or self.__rope_config != rope_config: - rope_folder = rope_config.get('ropeFolder') + rope_folder = rope_config.get("ropeFolder") if rope_folder: self.__rope = Project(self._root_path, ropefolder=rope_folder) else: self.__rope = Project(self._root_path) - self.__rope.prefs.set('extension_modules', - rope_config.get('extensionModules', [])) - self.__rope.prefs.set('ignore_syntax_errors', True) - self.__rope.prefs.set('ignore_bad_imports', True) + self.__rope.prefs.set( + "extension_modules", rope_config.get("extensionModules", []) + ) + self.__rope.prefs.set("ignore_syntax_errors", True) + self.__rope.prefs.set("ignore_bad_imports", True) self.__rope.validate() return self.__rope @@ -98,7 +101,9 @@ def root_uri(self): return self._root_uri def is_local(self): - return (self._root_uri_scheme in ['', 'file']) and os.path.exists(self._root_path) + return (self._root_uri_scheme in ["", "file"]) and os.path.exists( + self._root_path + ) def get_document(self, doc_uri): """Return a managed document if-present, else create one pointing at disk. @@ -114,10 +119,16 @@ def get_maybe_document(self, doc_uri): return self._docs.get(doc_uri) def put_document(self, doc_uri, source, version=None): - self._docs[doc_uri] = self._create_document(doc_uri, source=source, version=version) + self._docs[doc_uri] = self._create_document( + doc_uri, source=source, version=version + ) - def put_notebook_document(self, doc_uri, notebook_type, cells, version=None, metadata=None): - self._docs[doc_uri] = self._create_notebook_document(doc_uri, notebook_type, cells, version, metadata) + def put_notebook_document( + self, doc_uri, notebook_type, cells, version=None, metadata=None + ): + self._docs[doc_uri] = self._create_notebook_document( + doc_uri, notebook_type, cells, version, metadata + ) def add_notebook_cells(self, doc_uri, cells, start): self._docs[doc_uri].add_cells(cells, start) @@ -129,7 +140,9 @@ def update_notebook_metadata(self, doc_uri, metadata): self._docs[doc_uri].metadata = metadata def put_cell_document(self, doc_uri, language_id, source, version=None): - self._docs[doc_uri] = self._create_cell_document(doc_uri, language_id, source, version) + self._docs[doc_uri] = self._create_cell_document( + doc_uri, language_id, source, version + ) def rm_document(self, doc_uri): self._docs.pop(doc_uri) @@ -139,15 +152,18 @@ def update_document(self, doc_uri, change, version=None): self._docs[doc_uri].version = version def update_config(self, settings): - self._config.update((settings or {}).get('pylsp', {})) + self._config.update((settings or {}).get("pylsp", {})) for doc_uri in self.documents: self.get_document(doc_uri).update_config(settings) def apply_edit(self, edit): - return self._endpoint.request(self.M_APPLY_EDIT, {'edit': edit}) + return self._endpoint.request(self.M_APPLY_EDIT, {"edit": edit}) def publish_diagnostics(self, doc_uri, diagnostics): - self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, params={'uri': doc_uri, 'diagnostics': diagnostics}) + self._endpoint.notify( + self.M_PUBLISH_DIAGNOSTICS, + params={"uri": doc_uri, "diagnostics": diagnostics}, + ) @contextmanager def report_progress( @@ -170,16 +186,20 @@ def report_progress( of the progress token. """ if self._config: - client_supports_progress_reporting = ( - self._config.capabilities.get("window", {}).get("workDoneProgress", False) - ) + client_supports_progress_reporting = self._config.capabilities.get( + "window", {} + ).get("workDoneProgress", False) else: client_supports_progress_reporting = False if client_supports_progress_reporting: - token = self._progress_begin(title, message, percentage, skip_token_initialization) + token = self._progress_begin( + title, message, percentage, skip_token_initialization + ) - def progress_message(message: str, percentage: Optional[int] = None) -> None: + def progress_message( + message: str, percentage: Optional[int] = None + ) -> None: self._progress_report(token, message, percentage) try: @@ -192,7 +212,9 @@ def progress_message(message: str, percentage: Optional[int] = None) -> None: # FALLBACK: # If the client doesn't support progress reporting, we have a dummy method # for the caller to use. - def dummy_progress_message(message: str, percentage: Optional[int] = None) -> None: + def dummy_progress_message( + message: str, percentage: Optional[int] = None + ) -> None: # pylint: disable=unused-argument pass @@ -209,7 +231,9 @@ def _progress_begin( if not skip_token_initialization: try: - self._endpoint.request(self.M_INITIALIZE_PROGRESS, {'token': token}).result(timeout=1.0) + self._endpoint.request( + self.M_INITIALIZE_PROGRESS, {"token": token} + ).result(timeout=1.0) except Exception: # pylint: disable=broad-exception-caught log.warning( "There was an error while trying to initialize progress reporting." @@ -218,7 +242,7 @@ def _progress_begin( "To prevent waiting for the timeout you can set " "`skip_token_initialization=True`. " "Not every editor will show progress then, but many will.", - exc_info=True + exc_info=True, ) value = { @@ -277,12 +301,21 @@ def _progress_end(self, token: str, message: Optional[str] = None) -> None: ) def show_message(self, message, msg_type=lsp.MessageType.Info): - self._endpoint.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message}) + self._endpoint.notify( + self.M_SHOW_MESSAGE, params={"type": msg_type, "message": message} + ) def source_roots(self, document_path): """Return the source roots for the given document.""" - files = _utils.find_parents(self._root_path, document_path, ['setup.py', 'pyproject.toml']) or [] - return list({os.path.dirname(project_file) for project_file in files}) or [self._root_path] + files = ( + _utils.find_parents( + self._root_path, document_path, ["setup.py", "pyproject.toml"] + ) + or [] + ) + return list({os.path.dirname(project_file) for project_file in files}) or [ + self._root_path + ] def _create_document(self, doc_uri, source=None, version=None): path = uris.to_fs_path(doc_uri) @@ -295,14 +328,16 @@ def _create_document(self, doc_uri, source=None, version=None): rope_project_builder=self._rope_project_builder, ) - def _create_notebook_document(self, doc_uri, notebook_type, cells, version=None, metadata=None): + def _create_notebook_document( + self, doc_uri, notebook_type, cells, version=None, metadata=None + ): return Notebook( doc_uri, notebook_type, self, cells=cells, version=version, - metadata=metadata + metadata=metadata, ) def _create_cell_document(self, doc_uri, language_id, source=None, version=None): @@ -324,9 +359,16 @@ def close(self): class Document: - - def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, - rope_project_builder=None): + def __init__( + self, + uri, + workspace, + source=None, + version=None, + local=True, + extra_sys_path=None, + rope_project_builder=None, + ): self.uri = uri self.version = version self.path = uris.to_fs_path(uri) @@ -348,7 +390,10 @@ def __str__(self): def _rope_resource(self, rope_config): # pylint: disable=import-outside-toplevel from rope.base import libutils - return libutils.path_to_resource(self._rope_project_builder(rope_config), self.path) + + return libutils.path_to_resource( + self._rope_project_builder(rope_config), self.path + ) @property @lock @@ -359,28 +404,28 @@ def lines(self): @lock def source(self): if self._source is None: - with io.open(self.path, 'r', encoding='utf-8') as f: + with io.open(self.path, "r", encoding="utf-8") as f: return f.read() return self._source def update_config(self, settings): - self._config.update((settings or {}).get('pylsp', {})) + self._config.update((settings or {}).get("pylsp", {})) @lock def apply_change(self, change): """Apply a change to the document.""" - text = change['text'] - change_range = change.get('range') + text = change["text"] + change_range = change.get("range") if not change_range: # The whole file has changed self._source = text return - start_line = change_range['start']['line'] - start_col = change_range['start']['character'] - end_line = change_range['end']['line'] - end_col = change_range['end']['character'] + start_line = change_range["start"]["line"] + start_col = change_range["start"]["character"] + end_line = change_range["end"]["line"] + end_col = change_range["end"]["character"] # Check for an edit occuring at the very end of the file if start_line == len(self.lines): @@ -412,15 +457,15 @@ def apply_change(self, change): def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" - return position['character'] + len(''.join(self.lines[:position['line']])) + return position["character"] + len("".join(self.lines[: position["line"]])) def word_at_position(self, position): """Get the word under the cursor returning the start and end positions.""" - if position['line'] >= len(self.lines): - return '' + if position["line"] >= len(self.lines): + return "" - line = self.lines[position['line']] - i = position['character'] + line = self.lines[position["line"]] + i = position["character"] # Split word in two start = line[:i] end = line[i:] @@ -435,8 +480,9 @@ def word_at_position(self, position): @lock def jedi_names(self, all_scopes=False, definitions=True, references=False): script = self.jedi_script() - return script.get_names(all_scopes=all_scopes, definitions=definitions, - references=references) + return script.get_names( + all_scopes=all_scopes, definitions=definitions, references=references + ) @lock def jedi_script(self, position=None, use_document_path=False): @@ -445,25 +491,32 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = None if self._config: - jedi_settings = self._config.plugin_settings('jedi', document_path=self.path) - jedi.settings.auto_import_modules = jedi_settings.get('auto_import_modules', - DEFAULT_AUTO_IMPORT_MODULES) - environment_path = jedi_settings.get('environment') + jedi_settings = self._config.plugin_settings( + "jedi", document_path=self.path + ) + jedi.settings.auto_import_modules = jedi_settings.get( + "auto_import_modules", DEFAULT_AUTO_IMPORT_MODULES + ) + environment_path = jedi_settings.get("environment") # Jedi itself cannot deal with homedir-relative paths. # On systems, where it is expected, expand the home directory. - if environment_path and os.name != 'nt': + if environment_path and os.name != "nt": environment_path = os.path.expanduser(environment_path) - extra_paths = jedi_settings.get('extra_paths') or [] - env_vars = jedi_settings.get('env_vars') + extra_paths = jedi_settings.get("extra_paths") or [] + env_vars = jedi_settings.get("env_vars") # Drop PYTHONPATH from env_vars before creating the environment because that makes # Jedi throw an error. if env_vars is None: env_vars = os.environ.copy() - env_vars.pop('PYTHONPATH', None) + env_vars.pop("PYTHONPATH", None) - environment = self.get_enviroment(environment_path, env_vars=env_vars) if environment_path else None + environment = ( + self.get_enviroment(environment_path, env_vars=env_vars) + if environment_path + else None + ) sys_path = self.sys_path(environment_path, env_vars=env_vars) + extra_paths project_path = self._workspace.root_path @@ -472,10 +525,10 @@ def jedi_script(self, position=None, use_document_path=False): sys_path += [os.path.normpath(os.path.dirname(self.path))] kwargs = { - 'code': self.source, - 'path': self.path, - 'environment': environment, - 'project': jedi.Project(path=project_path, sys_path=sys_path), + "code": self.source, + "path": self.path, + "environment": environment, + "project": jedi.Project(path=project_path, sys_path=sys_path), } if position: @@ -492,9 +545,9 @@ def get_enviroment(self, environment_path=None, env_vars=None): if environment_path in self._workspace._environments: environment = self._workspace._environments[environment_path] else: - environment = jedi.api.environment.create_environment(path=environment_path, - safe=False, - env_vars=env_vars) + environment = jedi.api.environment.create_environment( + path=environment_path, safe=False, env_vars=env_vars + ) self._workspace._environments[environment_path] = environment return environment @@ -503,14 +556,19 @@ def sys_path(self, environment_path=None, env_vars=None): # Copy our extra sys path # TODO: when safe to break API, use env_vars explicitly to pass to create_environment path = list(self._extra_sys_path) - environment = self.get_enviroment(environment_path=environment_path, env_vars=env_vars) + environment = self.get_enviroment( + environment_path=environment_path, env_vars=env_vars + ) path.extend(environment.get_sys_path()) return path class Notebook: """Represents a notebook.""" - def __init__(self, uri, notebook_type, workspace, cells=None, version=None, metadata=None): + + def __init__( + self, uri, notebook_type, workspace, cells=None, version=None, metadata=None + ): self.uri = uri self.notebook_type = notebook_type self.workspace = workspace @@ -525,7 +583,7 @@ def add_cells(self, new_cells: List, start: int) -> None: self.cells[start:start] = new_cells def remove_cells(self, start: int, delete_count: int) -> None: - del self.cells[start:start+delete_count] + del self.cells[start : start + delete_count] class Cell(Document): @@ -538,13 +596,24 @@ class Cell(Document): they have a language id. """ - def __init__(self, uri, language_id, workspace, source=None, version=None, local=True, extra_sys_path=None, - rope_project_builder=None): - super().__init__(uri, workspace, source, version, local, extra_sys_path, rope_project_builder) + def __init__( + self, + uri, + language_id, + workspace, + source=None, + version=None, + local=True, + extra_sys_path=None, + rope_project_builder=None, + ): + super().__init__( + uri, workspace, source, version, local, extra_sys_path, rope_project_builder + ) self.language_id = language_id @property @lock def line_count(self): - """"Return the number of lines in the cell document.""" - return len(self.source.split('\n')) + """ "Return the number of lines in the cell document.""" + return len(self.source.split("\n")) diff --git a/scripts/jsonschema2md.py b/scripts/jsonschema2md.py index 3707e00d..10b7f855 100644 --- a/scripts/jsonschema2md.py +++ b/scripts/jsonschema2md.py @@ -38,7 +38,7 @@ def describe_type(prop: dict) -> str: prop_type = types[0] parts = [f"`{prop_type}`"] for option in types: - if option in EXTRA_DESCRIPTORS: + if option in EXTRA_DESCRIPTORS: parts.append(EXTRA_DESCRIPTORS[option](prop)) if "enum" in prop: allowed_values = [f"`{value!r}`" for value in prop["enum"]] diff --git a/setup.cfg b/setup.cfg index 933ff6a4..b606d41a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,6 @@ # Copyright 2021- Python Language Server Contributors. [pycodestyle] -ignore = E226, E722, W504 +ignore = E203, E226, E722, W503, W504 max-line-length = 120 exclude = test/plugins/.ropeproject,test/.ropeproject diff --git a/test/conftest.py b/test/conftest.py index 839fd126..332866c5 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,6 +8,4 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) -pytest_plugins = [ - 'test.fixtures' -] +pytest_plugins = ["test.fixtures"] diff --git a/test/fixtures.py b/test/fixtures.py index 7c7bcf62..dde37ff4 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -28,6 +28,7 @@ class FakeEditorMethodsMixin: """ Represents the methods to be added to a dispatcher class when faking an editor. """ + def m_window__work_done_progress__create(self, *_args, **_kwargs): """ Fake editor method `window/workDoneProgress/create`. @@ -52,6 +53,7 @@ class FakeEndpoint(Endpoint): Fake methods in the `dispatcher` should raise `JsonRpcException` for any error. """ + def request(self, method, params=None): request_future = super().request(method, params) try: @@ -64,13 +66,11 @@ def request(self, method, params=None): @pytest.fixture def pylsp(tmpdir): - """ Return an initialized python LS """ + """Return an initialized python LS""" ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint) ls.m_initialize( - processId=1, - rootUri=uris.from_fs_path(str(tmpdir)), - initializationOptions={} + processId=1, rootUri=uris.from_fs_path(str(tmpdir)), initializationOptions={} ) return ls @@ -78,26 +78,20 @@ def pylsp(tmpdir): @pytest.fixture def pylsp_w_workspace_folders(tmpdir): - """ Return an initialized python LS """ + """Return an initialized python LS""" ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint) - folder1 = tmpdir.mkdir('folder1') - folder2 = tmpdir.mkdir('folder2') + 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' - } - ] + {"uri": uris.from_fs_path(str(folder1)), "name": "folder1"}, + {"uri": uris.from_fs_path(str(folder2)), "name": "folder2"}, + ], ) workspace_folders = [folder1, folder2] @@ -129,7 +123,7 @@ def workspace(tmpdir, endpoint): # pylint: disable=redefined-outer-name @pytest.fixture def workspace_other_root_path(tmpdir, endpoint): # pylint: disable=redefined-outer-name """Return a workspace with a root_path other than tmpdir.""" - ws_path = str(tmpdir.mkdir('test123').mkdir('test456')) + ws_path = str(tmpdir.mkdir("test123").mkdir("test456")) ws = Workspace(uris.from_fs_path(ws_path), endpoint) ws._config = Config(ws.root_uri, {}, 0, {}) return ws @@ -139,7 +133,9 @@ def workspace_other_root_path(tmpdir, endpoint): # pylint: disable=redefined-ou def config(workspace): # pylint: disable=redefined-outer-name """Return a config object.""" cfg = Config(workspace.root_uri, {}, 0, {}) - cfg._plugin_settings = {'plugins': {'pylint': {'enabled': False, 'args': [], 'executable': None}}} + cfg._plugin_settings = { + "plugins": {"pylint": {"enabled": False, "args": [], "executable": None}} + } return cfg @@ -150,14 +146,15 @@ def doc(workspace): # pylint: disable=redefined-outer-name @pytest.fixture def temp_workspace_factory(workspace): # pylint: disable=redefined-outer-name - ''' + """ Returns a function that creates a temporary workspace from the files dict. The dict is in the format {"file_name": "file_contents"} - ''' + """ + def fn(files): def create_file(name, content): fn = os.path.join(workspace.root_path, name) - with open(fn, 'w', encoding='utf-8') as f: + with open(fn, "w", encoding="utf-8") as f: f.write(content) workspace.put_document(uris.from_fs_path(fn), content) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index a1ddd779..dbb6f7a4 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -10,8 +10,9 @@ from pylsp import lsp, uris from pylsp.config.config import Config from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names -from pylsp.plugins.rope_autoimport import \ - pylsp_completions as pylsp_autoimport_completions +from pylsp.plugins.rope_autoimport import ( + pylsp_completions as pylsp_autoimport_completions, +) from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace @@ -21,7 +22,9 @@ @pytest.fixture(scope="session") def autoimport_workspace(tmp_path_factory) -> Workspace: "Special autoimport workspace. Persists across sessions to make in-memory sqlite3 database fast." - workspace = Workspace(uris.from_fs_path(str(tmp_path_factory.mktemp("pylsp"))), Mock()) + workspace = Workspace( + uris.from_fs_path(str(tmp_path_factory.mktemp("pylsp"))), Mock() + ) workspace._config = Config(workspace.root_uri, {}, 0, {}) workspace._config.update({"rope_autoimport": {"memory": True, "enabled": True}}) pylsp_initialize(workspace._config, workspace) @@ -57,10 +60,8 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: def test_autoimport_completion(completions): assert completions assert check_dict( - { - "label": "pathlib", - "kind": lsp.CompletionItemKind.Module - }, completions) + {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, completions + ) @pytest.mark.parametrize("completions", [("""import """, 7)], indirect=True) @@ -74,29 +75,22 @@ def test_autoimport_pathlib(completions): start = {"line": 0, "character": 0} edit_range = {"start": start, "end": start} - assert completions[0]["additionalTextEdits"] == [{ - "range": - edit_range, - "newText": - "import pathlib\n" - }] + assert completions[0]["additionalTextEdits"] == [ + {"range": edit_range, "newText": "import pathlib\n"} + ] -@pytest.mark.parametrize("completions", [("""import test\n""", 10)], - indirect=True) +@pytest.mark.parametrize("completions", [("""import test\n""", 10)], indirect=True) def test_autoimport_import_with_name(completions): assert len(completions) == 0 -@pytest.mark.parametrize("completions", [("""def func(s""", 10)], - indirect=True) +@pytest.mark.parametrize("completions", [("""def func(s""", 10)], indirect=True) def test_autoimport_function(completions): - assert len(completions) == 0 -@pytest.mark.parametrize("completions", [("""class Test""", 10)], - indirect=True) +@pytest.mark.parametrize("completions", [("""class Test""", 10)], indirect=True) def test_autoimport_class(completions): assert len(completions) == 0 @@ -106,35 +100,37 @@ def test_autoimport_empty_line(completions): assert len(completions) == 0 -@pytest.mark.parametrize("completions", [("""class Test(NamedTupl):""", 20)], - indirect=True) +@pytest.mark.parametrize( + "completions", [("""class Test(NamedTupl):""", 20)], indirect=True +) def test_autoimport_class_complete(completions): assert len(completions) > 0 -@pytest.mark.parametrize("completions", [("""class Test(NamedTupl""", 20)], - indirect=True) +@pytest.mark.parametrize( + "completions", [("""class Test(NamedTupl""", 20)], indirect=True +) def test_autoimport_class_incomplete(completions): assert len(completions) > 0 -@pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], - indirect=True) +@pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], indirect=True) def test_autoimport_function_typing(completions): assert len(completions) > 0 assert check_dict({"label": "List"}, completions) -@pytest.mark.parametrize("completions", [("""def func(s : Lis ):""", 16)], - indirect=True) +@pytest.mark.parametrize( + "completions", [("""def func(s : Lis ):""", 16)], indirect=True +) def test_autoimport_function_typing_complete(completions): assert len(completions) > 0 assert check_dict({"label": "List"}, completions) -@pytest.mark.parametrize("completions", - [("""def func(s : Lis ) -> Generat:""", 29)], - indirect=True) +@pytest.mark.parametrize( + "completions", [("""def func(s : Lis ) -> Generat:""", 29)], indirect=True +) def test_autoimport_function_typing_return(completions): assert len(completions) > 0 assert check_dict({"label": "Generator"}, completions) @@ -145,8 +141,7 @@ def test_autoimport_defined_name(config, workspace): com_position = {"line": 1, "character": 3} workspace.put_document(DOC_URI, source=document) doc = workspace.get_document(DOC_URI) - completions = pylsp_autoimport_completions(config, workspace, doc, - com_position) + completions = pylsp_autoimport_completions(config, workspace, doc, com_position) workspace.rm_document(DOC_URI) assert not check_dict({"label": "List"}, completions) @@ -186,20 +181,16 @@ def test_autoimport_defined_name(config, workspace): class TestShouldInsert: - def test_dot(self): - assert not should_insert("""str.""", 4) def test_dot_partial(self): - assert not should_insert("""str.metho\n""", 9) def test_comment(self): assert not should_insert("""#""", 1) def test_comment_indent(self): - assert not should_insert(""" # """, 5) def test_from(self): @@ -214,15 +205,17 @@ def test_sort_sources(): def test_sort_statements(): - result1 = _get_score(2, "from importlib_metadata import pathlib", - "pathlib", "pathli") + result1 = _get_score( + 2, "from importlib_metadata import pathlib", "pathlib", "pathli" + ) result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 def test_sort_both(): - result1 = _get_score(3, "from importlib_metadata import pathlib", - "pathlib", "pathli") + result1 = _get_score( + 3, "from importlib_metadata import pathlib", "pathlib", "pathli" + ) result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 @@ -239,5 +232,4 @@ class sfa: sfiosifo """ results = get_names(jedi.Script(code=source)) - assert results == set( - ["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) + assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) diff --git a/test/plugins/test_autopep8_format.py b/test/plugins/test_autopep8_format.py index 6fac7cf1..ecdf2419 100644 --- a/test/plugins/test_autopep8_format.py +++ b/test/plugins/test_autopep8_format.py @@ -44,22 +44,22 @@ def test_format(config, workspace): res = pylsp_format_document(config, workspace, doc, options=None) assert len(res) == 1 - assert res[0]['newText'] == "a = 123\n\n\ndef func():\n pass\n" + assert res[0]["newText"] == "a = 123\n\n\ndef func():\n pass\n" def test_range_format(config, workspace): doc = Document(DOC_URI, workspace, DOC) def_range = { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 2, 'character': 0} + "start": {"line": 0, "character": 0}, + "end": {"line": 2, "character": 0}, } res = pylsp_format_range(config, workspace, doc, def_range, options=None) assert len(res) == 1 # Make sure the func is still badly formatted - assert res[0]['newText'] == "a = 123\n\n\n\n\ndef func():\n pass\n" + assert res[0]["newText"] == "a = 123\n\n\n\n\ndef func():\n pass\n" def test_no_change(config, workspace): @@ -72,12 +72,15 @@ def test_hanging_indentation(config, workspace): res = pylsp_format_document(config, workspace, doc, options=None) assert len(res) == 1 - assert res[0]['newText'] == CORRECT_INDENTED_DOC + assert res[0]["newText"] == CORRECT_INDENTED_DOC -@pytest.mark.parametrize('newline', ['\r\n', '\r']) +@pytest.mark.parametrize("newline", ["\r\n", "\r"]) def test_line_endings(config, workspace, newline): - doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') + doc = Document(DOC_URI, workspace, f"import os;import sys{2 * newline}dict(a=1)") res = pylsp_format_document(config, workspace, doc, options=None) - assert res[0]['newText'] == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' + assert ( + res[0]["newText"] + == f"import os{newline}import sys{2 * newline}dict(a=1){newline}" + ) diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py index fc22c34f..00a54eb7 100644 --- a/test/plugins/test_completion.py +++ b/test/plugins/test_completion.py @@ -13,17 +13,17 @@ 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.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 -PY2 = sys.version[0] == '2' -LINUX = sys.platform.startswith('linux') -CI = os.environ.get('CI') -LOCATION = os.path.realpath( - os.path.join(os.getcwd(), os.path.dirname(__file__)) -) +PY2 = sys.version[0] == "2" +LINUX = sys.platform.startswith("linux") +CI = os.environ.get("CI") +LOCATION = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) DOC_URI = uris.from_fs_path(__file__) DOC = """import os print os.path.isabs("/tmp") @@ -54,7 +54,7 @@ def documented_hello(): def test_rope_import_completion(config, workspace): - com_position = {'line': 0, 'character': 7} + com_position = {"line": 0, "character": 7} doc = Document(DOC_URI, workspace, DOC) items = pylsp_rope_completions(config, workspace, doc, com_position) assert items is None @@ -68,213 +68,225 @@ class TypeCase(NamedTuple): TYPE_CASES: Dict[str, TypeCase] = { - 'variable': TypeCase( - document='test = 1\ntes', - position={'line': 1, 'character': 3}, - label='test', - expected=lsp.CompletionItemKind.Variable + "variable": TypeCase( + document="test = 1\ntes", + position={"line": 1, "character": 3}, + label="test", + expected=lsp.CompletionItemKind.Variable, ), - 'function': TypeCase( - document='def test():\n pass\ntes', - position={'line': 2, 'character': 3}, - label='test()', - expected=lsp.CompletionItemKind.Function + "function": TypeCase( + document="def test():\n pass\ntes", + position={"line": 2, "character": 3}, + label="test()", + expected=lsp.CompletionItemKind.Function, ), - 'keyword': TypeCase( - document='fro', - position={'line': 0, 'character': 3}, - label='from', - expected=lsp.CompletionItemKind.Keyword + "keyword": TypeCase( + document="fro", + position={"line": 0, "character": 3}, + label="from", + expected=lsp.CompletionItemKind.Keyword, ), - 'file': TypeCase( + "file": TypeCase( document='"' + __file__[:-2].replace('"', '\\"') + '"', - position={'line': 0, 'character': len(__file__) - 2}, + position={"line": 0, "character": len(__file__) - 2}, label=Path(__file__).name + '"', - expected=lsp.CompletionItemKind.File + expected=lsp.CompletionItemKind.File, ), - 'module': TypeCase( - document='import statis', - position={'line': 0, 'character': 13}, - label='statistics', - expected=lsp.CompletionItemKind.Module + "module": TypeCase( + document="import statis", + position={"line": 0, "character": 13}, + label="statistics", + expected=lsp.CompletionItemKind.Module, ), - 'class': TypeCase( - document='KeyErr', - position={'line': 0, 'character': 6}, - label='KeyError', - expected=lsp.CompletionItemKind.Class + "class": TypeCase( + document="KeyErr", + position={"line": 0, "character": 6}, + label="KeyError", + expected=lsp.CompletionItemKind.Class, ), - 'property': TypeCase( + "property": TypeCase( document=( - 'class A:\n' - ' @property\n' - ' def test(self):\n' - ' pass\n' - 'A().tes' + "class A:\n" + " @property\n" + " def test(self):\n" + " pass\n" + "A().tes" ), - position={'line': 4, 'character': 5}, - label='test', - expected=lsp.CompletionItemKind.Property - ) + position={"line": 4, "character": 5}, + label="test", + expected=lsp.CompletionItemKind.Property, + ), } -@pytest.mark.parametrize('case', list(TYPE_CASES.values()), ids=list(TYPE_CASES.keys())) +@pytest.mark.parametrize("case", list(TYPE_CASES.values()), ids=list(TYPE_CASES.keys())) def test_jedi_completion_type(case, config, workspace): # property support was introduced in 0.18 - if case.expected == lsp.CompletionItemKind.Property and JEDI_VERSION.startswith('0.17'): + if case.expected == lsp.CompletionItemKind.Property and JEDI_VERSION.startswith( + "0.17" + ): return doc = Document(DOC_URI, workspace, case.document) items = pylsp_jedi_completions(config, doc, case.position) - items = {i['label']: i for i in items} - assert items[case.label]['kind'] == case.expected + items = {i["label"]: i for i in items} + assert items[case.label]["kind"] == case.expected def test_jedi_completion(config, workspace): # Over 'i' in os.path.isabs(...) - com_position = {'line': 1, 'character': 15} + com_position = {"line": 1, "character": 15} doc = Document(DOC_URI, workspace, DOC) items = pylsp_jedi_completions(config, doc, com_position) assert items - labels = [i['label'] for i in items] - assert 'isfile(path)' in labels + labels = [i["label"] for i in items] + assert "isfile(path)" in labels # Test we don't throw with big character - pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000}) + 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} + com_position = {"line": 8, "character": 0} doc = Document(DOC_URI, workspace, DOC) - config.update({'plugins': {'jedi_completion': {'resolve_at_most': math.inf}}}) + config.update({"plugins": {"jedi_completion": {"resolve_at_most": math.inf}}}) completions = pylsp_jedi_completions(config, doc, com_position) - items = {c['label']: c for c in completions} + items = {c["label"]: c for c in completions} - documented_hello_item = items['documented_hello()'] + documented_hello_item = items["documented_hello()"] - assert 'documentation' not in documented_hello_item - assert 'detail' not in documented_hello_item + assert "documentation" not in documented_hello_item + assert "detail" not in documented_hello_item resolved_documented_hello = pylsp_jedi_completion_item_resolve( - doc._config, - completion_item=documented_hello_item, - document=doc + doc._config, completion_item=documented_hello_item, document=doc ) expected_doc = { - 'kind': 'markdown', - 'value': '```python\ndocumented_hello()\n```\n\n\nSends a polite greeting' + "kind": "markdown", + "value": "```python\ndocumented_hello()\n```\n\n\nSends a polite greeting", } - assert resolved_documented_hello['documentation'] == expected_doc + assert resolved_documented_hello["documentation"] == expected_doc def test_jedi_completion_with_fuzzy_enabled(config, workspace): # Over 'i' in os.path.isabs(...) - config.update({'plugins': {'jedi_completion': {'fuzzy': True}}}) - com_position = {'line': 1, 'character': 15} + config.update({"plugins": {"jedi_completion": {"fuzzy": True}}}) + com_position = {"line": 1, "character": 15} doc = Document(DOC_URI, workspace, DOC) items = pylsp_jedi_completions(config, doc, com_position) assert items - expected = 'commonprefix(m)' - if JEDI_VERSION == '0.18.0': - expected = 'commonprefix(list)' - assert items[0]['label'] == expected + expected = "commonprefix(m)" + if JEDI_VERSION == "0.18.0": + expected = "commonprefix(list)" + assert items[0]["label"] == expected # Test we don't throw with big character - pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000}) + 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} + 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': 0}}}) + config.update({"plugins": {"jedi_completion": {"resolve_at_most": 0}}}) items = pylsp_jedi_completions(config, doc, com_position) - labels = {i['label'] for i in items} - assert 'isabs' in labels + labels = {i["label"] for i in items} + assert "isabs" in labels # Resolve all items - config.update({'plugins': {'jedi_completion': {'resolve_at_most': math.inf}}}) + config.update({"plugins": {"jedi_completion": {"resolve_at_most": math.inf}}}) items = pylsp_jedi_completions(config, doc, com_position) - labels = {i['label'] for i in items} - assert 'isfile(path)' in labels + labels = {i["label"] for i in items} + assert "isfile(path)" in labels def test_rope_completion(config, workspace): # Over 'i' in os.path.isabs(...) - com_position = {'line': 1, 'character': 15} + com_position = {"line": 1, "character": 15} workspace.put_document(DOC_URI, source=DOC) doc = workspace.get_document(DOC_URI) items = pylsp_rope_completions(config, workspace, doc, com_position) assert items - assert items[0]['label'] == 'isabs' + assert items[0]["label"] == "isabs" def test_jedi_completion_ordering(config, workspace): # Over the blank line - com_position = {'line': 8, 'character': 0} + com_position = {"line": 8, "character": 0} doc = Document(DOC_URI, workspace, DOC) - config.update({'plugins': {'jedi_completion': {'resolve_at_most': math.inf}}}) + config.update({"plugins": {"jedi_completion": {"resolve_at_most": math.inf}}}) completions = pylsp_jedi_completions(config, doc, com_position) - items = {c['label']: c['sortText'] for c in completions} + items = {c["label"]: c["sortText"] for c in completions} # And that 'hidden' functions come after unhidden ones - assert items['hello()'] < items['_a_hello()'] + assert items["hello()"] < items["_a_hello()"] def test_jedi_property_completion(config, workspace): # Over the 'w' in 'print Hello().world' - com_position = {'line': 18, 'character': 15} + com_position = {"line": 18, "character": 15} doc = Document(DOC_URI, workspace, DOC) completions = pylsp_jedi_completions(config, doc, com_position) - items = {c['label']: c['sortText'] for c in completions} + items = {c["label"]: c["sortText"] for c in completions} # Ensure we can complete the 'world' property - assert 'world' in list(items.keys())[0] + assert "world" in list(items.keys())[0] def test_jedi_method_completion(config, workspace): # Over the 'y' in 'print Hello().every' - com_position = {'line': 20, 'character': 19} + com_position = {"line": 20, "character": 19} doc = Document(DOC_URI, workspace, DOC) - config.capabilities['textDocument'] = {'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) completions = pylsp_jedi_completions(config, doc, com_position) - everyone_method = [completion for completion in completions if completion['label'] == 'everyone(a, b, c, d)'][0] + everyone_method = [ + completion + for completion in completions + if completion["label"] == "everyone(a, b, c, d)" + ][0] # Ensure we only generate snippets for positional args - assert everyone_method['insertTextFormat'] == lsp.InsertTextFormat.Snippet - assert everyone_method['insertText'] == 'everyone(${1:a}, ${2:b})$0' + assert everyone_method["insertTextFormat"] == lsp.InsertTextFormat.Snippet + assert everyone_method["insertText"] == "everyone(${1:a}, ${2:b})$0" # Disable param snippets - config.update({'plugins': {'jedi_completion': {'include_params': False}}}) + config.update({"plugins": {"jedi_completion": {"include_params": False}}}) completions = pylsp_jedi_completions(config, doc, com_position) - everyone_method = [completion for completion in completions if completion['label'] == 'everyone(a, b, c, d)'][0] + everyone_method = [ + completion + for completion in completions + if completion["label"] == "everyone(a, b, c, d)" + ][0] - assert 'insertTextFormat' not in everyone_method - assert everyone_method['insertText'] == 'everyone' + assert "insertTextFormat" not in everyone_method + assert everyone_method["insertText"] == "everyone" -@pytest.mark.skipif(PY2 or (sys.platform.startswith('linux') and os.environ.get('CI') is not None), - reason="Test in Python 3 and not on CIs on Linux because wheels don't work on them.") +@pytest.mark.skipif( + PY2 or (sys.platform.startswith("linux") and os.environ.get("CI") is not None), + reason="Test in Python 3 and not on CIs on Linux because wheels don't work on them.", +) def test_pyqt_completion(config, workspace): # Over 'QA' in 'from PyQt5.QtWidgets import QApplication' doc_pyqt = "from PyQt5.QtWidgets import QA" - com_position = {'line': 0, 'character': len(doc_pyqt)} + com_position = {"line": 0, "character": len(doc_pyqt)} doc = Document(DOC_URI, workspace, doc_pyqt) completions = pylsp_jedi_completions(config, doc, com_position) @@ -283,175 +295,195 @@ def test_pyqt_completion(config, workspace): def test_numpy_completions(config, workspace): doc_numpy = "import numpy as np; np." - com_position = {'line': 0, 'character': len(doc_numpy)} + com_position = {"line": 0, "character": len(doc_numpy)} doc = Document(DOC_URI, workspace, doc_numpy) items = pylsp_jedi_completions(config, doc, com_position) assert items - assert any('array' in i['label'] for i in items) + assert any("array" in i["label"] for i in items) def test_pandas_completions(config, workspace): doc_pandas = "import pandas as pd; pd." - com_position = {'line': 0, 'character': len(doc_pandas)} + com_position = {"line": 0, "character": len(doc_pandas)} doc = Document(DOC_URI, workspace, doc_pandas) items = pylsp_jedi_completions(config, doc, com_position) assert items - assert any('DataFrame' in i['label'] for i in items) + assert any("DataFrame" in i["label"] for i in items) def test_matplotlib_completions(config, workspace): doc_mpl = "import matplotlib.pyplot as plt; plt." - com_position = {'line': 0, 'character': len(doc_mpl)} + com_position = {"line": 0, "character": len(doc_mpl)} doc = Document(DOC_URI, workspace, doc_mpl) items = pylsp_jedi_completions(config, doc, com_position) assert items - assert any('plot' in i['label'] for i in items) + assert any("plot" in i["label"] for i in items) def test_snippets_completion(config, workspace): - doc_snippets = 'from collections import defaultdict \na=defaultdict' - com_position = {'line': 0, 'character': 35} + doc_snippets = "from collections import defaultdict \na=defaultdict" + com_position = {"line": 0, "character": 35} doc = Document(DOC_URI, workspace, doc_snippets) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) completions = pylsp_jedi_completions(config, doc, com_position) - assert completions[0]['insertText'] == 'defaultdict' + assert completions[0]["insertText"] == "defaultdict" - com_position = {'line': 1, 'character': len(doc_snippets)} + com_position = {"line": 1, "character": len(doc_snippets)} completions = pylsp_jedi_completions(config, doc, com_position) - assert completions[0]['insertText'] == 'defaultdict($0)' - assert completions[0]['insertTextFormat'] == lsp.InsertTextFormat.Snippet + assert completions[0]["insertText"] == "defaultdict($0)" + assert completions[0]["insertTextFormat"] == lsp.InsertTextFormat.Snippet def test_snippets_completion_at_most(config, workspace): - doc_snippets = 'from collections import defaultdict \na=defaultdict' + doc_snippets = "from collections import defaultdict \na=defaultdict" doc = Document(DOC_URI, workspace, doc_snippets) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) - config.update({'plugins': {'jedi_completion': {'resolve_at_most': 0}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) + config.update({"plugins": {"jedi_completion": {"resolve_at_most": 0}}}) - com_position = {'line': 1, 'character': len(doc_snippets)} + com_position = {"line": 1, "character": len(doc_snippets)} completions = pylsp_jedi_completions(config, doc, com_position) - assert completions[0]['insertText'] == 'defaultdict' - assert not completions[0].get('insertTextFormat', None) + assert completions[0]["insertText"] == "defaultdict" + assert not completions[0].get("insertTextFormat", None) def test_completion_with_class_objects(config, workspace): - doc_text = 'class FOOBAR(Object): pass\nFOOB' - com_position = {'line': 1, 'character': 4} + doc_text = "class FOOBAR(Object): pass\nFOOB" + com_position = {"line": 1, "character": 4} doc = Document(DOC_URI, workspace, doc_text) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': { - 'include_params': True, - 'include_class_objects': True, - }}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update( + { + "plugins": { + "jedi_completion": { + "include_params": True, + "include_class_objects": True, + } + } + } + ) completions = pylsp_jedi_completions(config, doc, com_position) assert len(completions) == 2 - assert completions[0]['label'] == 'FOOBAR' - assert completions[0]['kind'] == lsp.CompletionItemKind.Class + assert completions[0]["label"] == "FOOBAR" + assert completions[0]["kind"] == lsp.CompletionItemKind.Class - assert completions[1]['label'] == 'FOOBAR object' - assert completions[1]['kind'] == lsp.CompletionItemKind.TypeParameter + assert completions[1]["label"] == "FOOBAR object" + assert completions[1]["kind"] == lsp.CompletionItemKind.TypeParameter def test_completion_with_function_objects(config, workspace): - doc_text = 'def foobar(): pass\nfoob' - com_position = {'line': 1, 'character': 4} + doc_text = "def foobar(): pass\nfoob" + com_position = {"line": 1, "character": 4} doc = Document(DOC_URI, workspace, doc_text) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': { - 'include_params': True, - 'include_function_objects': True, - }}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update( + { + "plugins": { + "jedi_completion": { + "include_params": True, + "include_function_objects": True, + } + } + } + ) completions = pylsp_jedi_completions(config, doc, com_position) assert len(completions) == 2 - assert completions[0]['label'] == 'foobar()' - assert completions[0]['kind'] == lsp.CompletionItemKind.Function + assert completions[0]["label"] == "foobar()" + assert completions[0]["kind"] == lsp.CompletionItemKind.Function - assert completions[1]['label'] == 'foobar() object' - assert completions[1]['kind'] == lsp.CompletionItemKind.TypeParameter + assert completions[1]["label"] == "foobar() object" + assert completions[1]["kind"] == lsp.CompletionItemKind.TypeParameter def test_snippet_parsing(config, workspace): - doc = 'divmod' - completion_position = {'line': 0, 'character': 6} + doc = "divmod" + completion_position = {"line": 0, "character": 6} doc = Document(DOC_URI, workspace, doc) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) completions = pylsp_jedi_completions(config, doc, completion_position) - out = 'divmod(${1:x}, ${2:y})$0' - if JEDI_VERSION == '0.18.0': - out = 'divmod(${1:a}, ${2:b})$0' - assert completions[0]['insertText'] == out + out = "divmod(${1:x}, ${2:y})$0" + if JEDI_VERSION == "0.18.0": + out = "divmod(${1:a}, ${2:b})$0" + assert completions[0]["insertText"] == out def test_multiline_import_snippets(config, workspace): - document = 'from datetime import(\n date,\n datetime)\na=date' + document = "from datetime import(\n date,\n datetime)\na=date" doc = Document(DOC_URI, workspace, document) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) - position = {'line': 1, 'character': 5} + position = {"line": 1, "character": 5} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'date' + assert completions[0]["insertText"] == "date" - position = {'line': 2, 'character': 9} + position = {"line": 2, "character": 9} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'datetime' + assert completions[0]["insertText"] == "datetime" def test_multiline_snippets(config, workspace): - document = 'from datetime import\\\n date,\\\n datetime \na=date' + document = "from datetime import\\\n date,\\\n datetime \na=date" doc = Document(DOC_URI, workspace, document) - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) - position = {'line': 1, 'character': 5} + position = {"line": 1, "character": 5} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'date' + assert completions[0]["insertText"] == "date" - position = {'line': 2, 'character': 9} + position = {"line": 2, "character": 9} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'datetime' + assert completions[0]["insertText"] == "datetime" def test_multistatement_snippet(config, workspace): - config.capabilities['textDocument'] = { - 'completion': {'completionItem': {'snippetSupport': True}}} - config.update({'plugins': {'jedi_completion': {'include_params': True}}}) + config.capabilities["textDocument"] = { + "completion": {"completionItem": {"snippetSupport": True}} + } + config.update({"plugins": {"jedi_completion": {"include_params": True}}}) - document = 'a = 1; from datetime import date' + document = "a = 1; from datetime import date" doc = Document(DOC_URI, workspace, document) - position = {'line': 0, 'character': len(document)} + position = {"line": 0, "character": len(document)} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'date' + assert completions[0]["insertText"] == "date" - document = 'from math import fmod; a = fmod' + document = "from math import fmod; a = fmod" doc = Document(DOC_URI, workspace, document) - position = {'line': 0, 'character': len(document)} + position = {"line": 0, "character": len(document)} completions = pylsp_jedi_completions(config, doc, position) - assert completions[0]['insertText'] == 'fmod(${1:x}, ${2:y})$0' + assert completions[0]["insertText"] == "fmod(${1:x}, ${2:y})$0" def test_jedi_completion_extra_paths(tmpdir, workspace): # Create a tempfile with some content and pass to extra_paths - temp_doc_content = ''' + temp_doc_content = """ def spam(): pass -''' +""" p = tmpdir.mkdir("extra_path") extra_paths = [str(p)] p = p.join("foo.py") @@ -463,57 +495,59 @@ def spam(): doc = Document(DOC_URI, workspace, doc_content) # After 'foo.s' without extra paths - com_position = {'line': 1, 'character': 5} + com_position = {"line": 1, "character": 5} completions = pylsp_jedi_completions(doc._config, doc, com_position) assert completions is None # Update config extra paths - settings = {'pylsp': {'plugins': {'jedi': {'extra_paths': extra_paths}}}} + settings = {"pylsp": {"plugins": {"jedi": {"extra_paths": extra_paths}}}} doc.update_config(settings) # After 'foo.s' with extra paths - com_position = {'line': 1, 'character': 5} + com_position = {"line": 1, "character": 5} completions = pylsp_jedi_completions(doc._config, doc, com_position) - assert completions[0]['label'] == 'spam()' + assert completions[0]["label"] == "spam()" -@pytest.mark.skipif(PY2 or not LINUX or not CI, reason="tested on linux and python 3 only") +@pytest.mark.skipif( + PY2 or not LINUX or not CI, reason="tested on linux and python 3 only" +) def test_jedi_completion_environment(workspace): # Content of doc to test completion - doc_content = '''import logh -''' + doc_content = """import logh +""" doc = Document(DOC_URI, workspace, doc_content) # After 'import logh' with default environment - com_position = {'line': 0, 'character': 11} + com_position = {"line": 0, "character": 11} - assert os.path.isdir('/tmp/pyenv/') + assert os.path.isdir("/tmp/pyenv/") - settings = {'pylsp': {'plugins': {'jedi': {'environment': None}}}} + settings = {"pylsp": {"plugins": {"jedi": {"environment": None}}}} doc.update_config(settings) completions = pylsp_jedi_completions(doc._config, doc, com_position) assert completions is None # Update config extra environment - env_path = '/tmp/pyenv/bin/python' - settings = {'pylsp': {'plugins': {'jedi': {'environment': env_path}}}} + env_path = "/tmp/pyenv/bin/python" + settings = {"pylsp": {"plugins": {"jedi": {"environment": env_path}}}} doc.update_config(settings) # After 'import logh' with new environment completions = pylsp_jedi_completions(doc._config, doc, com_position) - assert completions[0]['label'] == 'loghub' + assert completions[0]["label"] == "loghub" resolved = pylsp_jedi_completion_item_resolve(doc._config, completions[0], doc) - assert 'changelog generator' in resolved['documentation']['value'].lower() + assert "changelog generator" in resolved["documentation"]["value"].lower() def test_document_path_completions(tmpdir, workspace_other_root_path): # Create a dummy module out of the workspace's root_path and try to get # completions for it in another file placed next to it. - module_content = ''' + module_content = """ def foo(): pass -''' +""" p = tmpdir.join("mymodule.py") p.write(module_content) @@ -521,33 +555,37 @@ def foo(): # Content of doc to test completion doc_content = """import mymodule mymodule.f""" - doc_path = str(tmpdir) + os.path.sep + 'myfile.py' + doc_path = str(tmpdir) + os.path.sep + "myfile.py" doc_uri = uris.from_fs_path(doc_path) doc = Document(doc_uri, workspace_other_root_path, doc_content) - com_position = {'line': 1, 'character': 10} + com_position = {"line": 1, "character": 10} completions = pylsp_jedi_completions(doc._config, doc, com_position) - assert completions[0]['label'] == 'foo()' + assert completions[0]["label"] == "foo()" def test_file_completions(workspace, tmpdir): # Create directory and a file to get completions for them. # Note: `tmpdir`` is the root dir of the `workspace` fixture. That's why we use # it here. - tmpdir.mkdir('bar') - file = tmpdir.join('foo.txt') - file.write('baz') + tmpdir.mkdir("bar") + file = tmpdir.join("foo.txt") + file.write("baz") # Content of doc to test completion doc_content = '"' doc = Document(DOC_URI, workspace, doc_content) # Request for completions - com_position = {'line': 0, 'character': 1} + com_position = {"line": 0, "character": 1} completions = pylsp_jedi_completions(doc._config, doc, com_position) # Check completions assert len(completions) == 2 - assert [c['kind'] == lsp.CompletionItemKind.File for c in completions] - assert completions[0]['insertText'] == ('bar' + '\\\\') if os.name == 'nt' else ('bar' + '\\/') - assert completions[1]['insertText'] == 'foo.txt"' + assert [c["kind"] == lsp.CompletionItemKind.File for c in completions] + assert ( + completions[0]["insertText"] == ("bar" + "\\\\") + if os.name == "nt" + else ("bar" + "\\/") + ) + assert completions[1]["insertText"] == 'foo.txt"' diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py index 4a8855c6..34acc6a9 100644 --- a/test/plugins/test_definitions.py +++ b/test/plugins/test_definitions.py @@ -26,35 +26,37 @@ def add_member(self, id, name): def test_definitions(config, workspace): # Over 'a' in print a - cursor_pos = {'line': 3, 'character': 6} + cursor_pos = {"line": 3, "character": 6} # The definition of 'a' def_range = { - 'start': {'line': 0, 'character': 4}, - 'end': {'line': 0, 'character': 5} + "start": {"line": 0, "character": 4}, + "end": {"line": 0, "character": 5}, } doc = Document(DOC_URI, workspace, DOC) - assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, doc, cursor_pos) + assert [{"uri": DOC_URI, "range": def_range}] == pylsp_definitions( + config, doc, cursor_pos + ) def test_builtin_definition(config, workspace): # Over 'i' in dict - cursor_pos = {'line': 8, 'character': 24} + cursor_pos = {"line": 8, "character": 24} doc = Document(DOC_URI, workspace, DOC) orig_settings = config.settings() # Check definition for `dict` goes to `builtins.pyi::dict` - follow_defns_setting = {'follow_builtin_definitions': True} - settings = {'plugins': {'jedi_definition': follow_defns_setting}} + follow_defns_setting = {"follow_builtin_definitions": True} + settings = {"plugins": {"jedi_definition": follow_defns_setting}} config.update(settings) defns = pylsp_definitions(config, doc, cursor_pos) assert len(defns) == 1 assert defns[0]["uri"].endswith("builtins.pyi") # Check no definitions for `dict` - follow_defns_setting['follow_builtin_definitions'] = False + follow_defns_setting["follow_builtin_definitions"] = False config.update(settings) defns = pylsp_definitions(config, doc, cursor_pos) assert not defns @@ -64,43 +66,45 @@ def test_builtin_definition(config, workspace): def test_assignment(config, workspace): # Over 's' in self.members[id] - cursor_pos = {'line': 11, 'character': 19} + cursor_pos = {"line": 11, "character": 19} # The assignment of 'self.members' def_range = { - 'start': {'line': 8, 'character': 13}, - 'end': {'line': 8, 'character': 20} + "start": {"line": 8, "character": 13}, + "end": {"line": 8, "character": 20}, } doc = Document(DOC_URI, workspace, DOC) - assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, doc, cursor_pos) + assert [{"uri": DOC_URI, "range": def_range}] == pylsp_definitions( + config, doc, cursor_pos + ) def test_document_path_definitions(config, workspace_other_root_path, tmpdir): # Create a dummy module out of the workspace's root_path and try to get # a definition on it in another file placed next to it. - module_content = ''' + module_content = """ def foo(): pass -''' +""" p = tmpdir.join("mymodule.py") p.write(module_content) # Content of doc to test definition doc_content = """from mymodule import foo""" - doc_path = str(tmpdir) + os.path.sep + 'myfile.py' + doc_path = str(tmpdir) + os.path.sep + "myfile.py" doc_uri = uris.from_fs_path(doc_path) doc = Document(doc_uri, workspace_other_root_path, doc_content) # The range where is defined in mymodule.py def_range = { - 'start': {'line': 1, 'character': 4}, - 'end': {'line': 1, 'character': 7} + "start": {"line": 1, "character": 4}, + "end": {"line": 1, "character": 7}, } # The position where foo is called in myfile.py - cursor_pos = {'line': 0, 'character': 24} + cursor_pos = {"line": 0, "character": 24} # The uri for mymodule.py module_path = str(p) diff --git a/test/plugins/test_flake8_lint.py b/test/plugins/test_flake8_lint.py index 6a84f696..882bc996 100644 --- a/test/plugins/test_flake8_lint.py +++ b/test/plugins/test_flake8_lint.py @@ -22,7 +22,7 @@ def using_const(): def temp_document(doc_text, workspace): - with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file: name = temp_file.name temp_file.write(doc_text) doc = Document(uris.from_fs_path(name), workspace) @@ -31,31 +31,31 @@ def temp_document(doc_text, workspace): def test_flake8_unsaved(workspace): - doc = Document('', workspace, DOC) + doc = Document("", workspace, DOC) diags = flake8_lint.pylsp_lint(workspace, doc) - msg = 'F841 local variable \'a\' is assigned to but never used' - unused_var = [d for d in diags if d['message'] == msg][0] + msg = "F841 local variable 'a' is assigned to but never used" + unused_var = [d for d in diags if d["message"] == msg][0] - assert unused_var['source'] == 'flake8' - assert unused_var['code'] == 'F841' - assert unused_var['range']['start'] == {'line': 5, 'character': 1} - assert unused_var['range']['end'] == {'line': 5, 'character': 11} - assert unused_var['severity'] == lsp.DiagnosticSeverity.Error - assert unused_var['tags'] == [lsp.DiagnosticTag.Unnecessary] + assert unused_var["source"] == "flake8" + assert unused_var["code"] == "F841" + assert unused_var["range"]["start"] == {"line": 5, "character": 1} + assert unused_var["range"]["end"] == {"line": 5, "character": 11} + assert unused_var["severity"] == lsp.DiagnosticSeverity.Error + assert unused_var["tags"] == [lsp.DiagnosticTag.Unnecessary] def test_flake8_lint(workspace): name, doc = temp_document(DOC, workspace) try: diags = flake8_lint.pylsp_lint(workspace, doc) - msg = 'F841 local variable \'a\' is assigned to but never used' - unused_var = [d for d in diags if d['message'] == msg][0] - - assert unused_var['source'] == 'flake8' - assert unused_var['code'] == 'F841' - assert unused_var['range']['start'] == {'line': 5, 'character': 1} - assert unused_var['range']['end'] == {'line': 5, 'character': 11} - assert unused_var['severity'] == lsp.DiagnosticSeverity.Error + msg = "F841 local variable 'a' is assigned to but never used" + unused_var = [d for d in diags if d["message"] == msg][0] + + assert unused_var["source"] == "flake8" + assert unused_var["code"] == "F841" + assert unused_var["range"]["start"] == {"line": 5, "character": 1} + assert unused_var["range"]["end"] == {"line": 5, "character": 11} + assert unused_var["severity"] == lsp.DiagnosticSeverity.Error finally: os.remove(name) @@ -65,13 +65,18 @@ def test_flake8_respecting_configuration(workspace): ("src/__init__.py", ""), ("src/a.py", DOC), ("src/b.py", "import os"), - ("setup.cfg", dedent(""" + ( + "setup.cfg", + dedent( + """ [flake8] ignore = E302,W191 per-file-ignores = src/a.py:F401 src/b.py:W292 - """)) + """ + ), + ), ] made = {} @@ -118,25 +123,27 @@ def test_flake8_respecting_configuration(workspace): def test_flake8_config_param(workspace): - with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock: + with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value mock_instance.communicate.return_value = [bytes(), bytes()] - flake8_conf = '/tmp/some.cfg' - workspace._config.update({'plugins': {'flake8': {'config': flake8_conf}}}) + flake8_conf = "/tmp/some.cfg" + workspace._config.update({"plugins": {"flake8": {"config": flake8_conf}}}) _name, doc = temp_document(DOC, workspace) flake8_lint.pylsp_lint(workspace, doc) (call_args,) = popen_mock.call_args[0] - assert 'flake8' in call_args - assert '--config={}'.format(flake8_conf) in call_args + assert "flake8" in call_args + assert "--config={}".format(flake8_conf) in call_args def test_flake8_executable_param(workspace): - with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock: + with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value mock_instance.communicate.return_value = [bytes(), bytes()] - flake8_executable = '/tmp/flake8' - workspace._config.update({'plugins': {'flake8': {'executable': flake8_executable}}}) + flake8_executable = "/tmp/flake8" + workspace._config.update( + {"plugins": {"flake8": {"executable": flake8_executable}}} + ) _name, doc = temp_document(DOC, workspace) flake8_lint.pylsp_lint(workspace, doc) @@ -151,7 +158,9 @@ def get_flake8_cfg_settings(workspace, config_str): This function creates a ``setup.cfg``; you'll have to delete it yourself. """ - with open(os.path.join(workspace.root_path, "setup.cfg"), "w+", encoding='utf-8') as f: + with open( + os.path.join(workspace.root_path, "setup.cfg"), "w+", encoding="utf-8" + ) as f: f.write(config_str) workspace.update_config({"pylsp": {"configurationSources": ["flake8"]}}) @@ -176,7 +185,7 @@ def test_flake8_multiline(workspace): assert "exclude" in flake8_settings assert len(flake8_settings["exclude"]) == 2 - with patch('pylsp.plugins.flake8_lint.Popen') as popen_mock: + with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value mock_instance.communicate.return_value = [bytes(), bytes()] @@ -186,7 +195,13 @@ def test_flake8_multiline(workspace): call_args = popen_mock.call_args[0][0] init_file = os.path.join("blah", "__init__.py") - assert call_args == ["flake8", "-", "--exclude=blah/,file_2.py", "--stdin-display-name", init_file] + assert call_args == [ + "flake8", + "-", + "--exclude=blah/,file_2.py", + "--stdin-display-name", + init_file, + ] os.unlink(os.path.join(workspace.root_path, "setup.cfg")) diff --git a/test/plugins/test_folding.py b/test/plugins/test_folding.py index 57d6e2e9..733f3993 100644 --- a/test/plugins/test_folding.py +++ b/test/plugins/test_folding.py @@ -9,7 +9,8 @@ from pylsp.workspace import Document DOC_URI = uris.from_fs_path(__file__) -DOC = dedent(""" +DOC = dedent( + """ def func(arg1, arg2, arg3, arg4, arg5, default=func( 2, 3, 4 @@ -78,9 +79,11 @@ def inner(): def testC(): pass -""") +""" +) -SYNTAX_ERR = dedent(""" +SYNTAX_ERR = dedent( + """ def func(arg1, arg2, arg3, arg4, arg5, default=func( 2, 3, 4 @@ -109,47 +112,50 @@ class A(: for j in range(0, i): if i % 2 == 1: pass -""") +""" +) def test_folding(workspace): doc = Document(DOC_URI, workspace, DOC) ranges = pylsp_folding_range(doc) - expected = [{'startLine': 1, 'endLine': 6}, - {'startLine': 2, 'endLine': 3}, - {'startLine': 5, 'endLine': 6}, - {'startLine': 8, 'endLine': 11}, - {'startLine': 12, 'endLine': 20}, - {'startLine': 13, 'endLine': 14}, - {'startLine': 15, 'endLine': 16}, - {'startLine': 17, 'endLine': 18}, - {'startLine': 19, 'endLine': 20}, - {'startLine': 22, 'endLine': 35}, - {'startLine': 23, 'endLine': 35}, - {'startLine': 24, 'endLine': 25}, - {'startLine': 27, 'endLine': 29}, - {'startLine': 28, 'endLine': 29}, - {'startLine': 30, 'endLine': 31}, - {'startLine': 32, 'endLine': 34}, - {'startLine': 33, 'endLine': 34}, - {'startLine': 38, 'endLine': 39}, - {'startLine': 41, 'endLine': 43}, - {'startLine': 42, 'endLine': 43}, - {'startLine': 45, 'endLine': 54}, - {'startLine': 47, 'endLine': 51}, - {'startLine': 49, 'endLine': 51}, - {'startLine': 50, 'endLine': 51}, - {'startLine': 52, 'endLine': 54}, - {'startLine': 53, 'endLine': 54}, - {'startLine': 56, 'endLine': 57}, - {'startLine': 59, 'endLine': 65}, - {'startLine': 60, 'endLine': 61}, - {'startLine': 62, 'endLine': 63}, - {'startLine': 64, 'endLine': 65}, - {'startLine': 67, 'endLine': 68}] + expected = [ + {"startLine": 1, "endLine": 6}, + {"startLine": 2, "endLine": 3}, + {"startLine": 5, "endLine": 6}, + {"startLine": 8, "endLine": 11}, + {"startLine": 12, "endLine": 20}, + {"startLine": 13, "endLine": 14}, + {"startLine": 15, "endLine": 16}, + {"startLine": 17, "endLine": 18}, + {"startLine": 19, "endLine": 20}, + {"startLine": 22, "endLine": 35}, + {"startLine": 23, "endLine": 35}, + {"startLine": 24, "endLine": 25}, + {"startLine": 27, "endLine": 29}, + {"startLine": 28, "endLine": 29}, + {"startLine": 30, "endLine": 31}, + {"startLine": 32, "endLine": 34}, + {"startLine": 33, "endLine": 34}, + {"startLine": 38, "endLine": 39}, + {"startLine": 41, "endLine": 43}, + {"startLine": 42, "endLine": 43}, + {"startLine": 45, "endLine": 54}, + {"startLine": 47, "endLine": 51}, + {"startLine": 49, "endLine": 51}, + {"startLine": 50, "endLine": 51}, + {"startLine": 52, "endLine": 54}, + {"startLine": 53, "endLine": 54}, + {"startLine": 56, "endLine": 57}, + {"startLine": 59, "endLine": 65}, + {"startLine": 60, "endLine": 61}, + {"startLine": 62, "endLine": 63}, + {"startLine": 64, "endLine": 65}, + {"startLine": 67, "endLine": 68}, + ] if sys.version_info[:2] >= (3, 9): # the argument list of the decorator is also folded in Python >= 3.9 - expected.insert(4, {'startLine': 9, 'endLine': 10}) + expected.insert(4, {"startLine": 9, "endLine": 10}) assert ranges == expected @@ -157,17 +163,19 @@ def test_folding(workspace): def test_folding_syntax_error(workspace): doc = Document(DOC_URI, workspace, SYNTAX_ERR) ranges = pylsp_folding_range(doc) - expected = [{'startLine': 1, 'endLine': 6}, - {'startLine': 2, 'endLine': 3}, - {'startLine': 5, 'endLine': 6}, - {'startLine': 8, 'endLine': 9}, - {'startLine': 12, 'endLine': 13}, - {'startLine': 15, 'endLine': 17}, - {'startLine': 16, 'endLine': 17}, - {'startLine': 19, 'endLine': 28}, - {'startLine': 21, 'endLine': 25}, - {'startLine': 23, 'endLine': 25}, - {'startLine': 24, 'endLine': 25}, - {'startLine': 26, 'endLine': 28}, - {'startLine': 27, 'endLine': 28}] + expected = [ + {"startLine": 1, "endLine": 6}, + {"startLine": 2, "endLine": 3}, + {"startLine": 5, "endLine": 6}, + {"startLine": 8, "endLine": 9}, + {"startLine": 12, "endLine": 13}, + {"startLine": 15, "endLine": 17}, + {"startLine": 16, "endLine": 17}, + {"startLine": 19, "endLine": 28}, + {"startLine": 21, "endLine": 25}, + {"startLine": 23, "endLine": 25}, + {"startLine": 24, "endLine": 25}, + {"startLine": 26, "endLine": 28}, + {"startLine": 27, "endLine": 28}, + ] assert ranges == expected diff --git a/test/plugins/test_highlight.py b/test/plugins/test_highlight.py index b1baa008..e7bd5075 100644 --- a/test/plugins/test_highlight.py +++ b/test/plugins/test_highlight.py @@ -14,45 +14,51 @@ def test_highlight(workspace): # Over 'a' in a.startswith - cursor_pos = {'line': 1, 'character': 0} + cursor_pos = {"line": 1, "character": 0} doc = Document(DOC_URI, workspace, DOC) - assert pylsp_document_highlight(doc, cursor_pos) == [{ - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 1}, + assert pylsp_document_highlight(doc, cursor_pos) == [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 1}, + }, + # The first usage is Write + "kind": lsp.DocumentHighlightKind.Write, }, - # The first usage is Write - 'kind': lsp.DocumentHighlightKind.Write - }, { - 'range': { - 'start': {'line': 1, 'character': 0}, - 'end': {'line': 1, 'character': 1}, + { + "range": { + "start": {"line": 1, "character": 0}, + "end": {"line": 1, "character": 1}, + }, + # The second usage is Read + "kind": lsp.DocumentHighlightKind.Read, }, - # The second usage is Read - 'kind': lsp.DocumentHighlightKind.Read - }] + ] -SYS_DOC = '''import sys +SYS_DOC = """import sys print sys.path -''' +""" def test_sys_highlight(workspace): - cursor_pos = {'line': 0, 'character': 8} + cursor_pos = {"line": 0, "character": 8} doc = Document(DOC_URI, workspace, SYS_DOC) - assert pylsp_document_highlight(doc, cursor_pos) == [{ - 'range': { - 'start': {'line': 0, 'character': 7}, - 'end': {'line': 0, 'character': 10} + assert pylsp_document_highlight(doc, cursor_pos) == [ + { + "range": { + "start": {"line": 0, "character": 7}, + "end": {"line": 0, "character": 10}, + }, + "kind": lsp.DocumentHighlightKind.Write, }, - 'kind': lsp.DocumentHighlightKind.Write - }, { - 'range': { - 'start': {'line': 1, 'character': 6}, - 'end': {'line': 1, 'character': 9} + { + "range": { + "start": {"line": 1, "character": 6}, + "end": {"line": 1, "character": 9}, + }, + "kind": lsp.DocumentHighlightKind.Read, }, - 'kind': lsp.DocumentHighlightKind.Read - }] + ] diff --git a/test/plugins/test_hover.py b/test/plugins/test_hover.py index 89040247..7049becc 100644 --- a/test/plugins/test_hover.py +++ b/test/plugins/test_hover.py @@ -25,55 +25,66 @@ def main(): def test_numpy_hover(workspace): # Over the blank line - no_hov_position = {'line': 1, 'character': 0} + no_hov_position = {"line": 1, "character": 0} # Over 'numpy' in import numpy as np - numpy_hov_position_1 = {'line': 2, 'character': 8} + numpy_hov_position_1 = {"line": 2, "character": 8} # Over 'np' in import numpy as np - numpy_hov_position_2 = {'line': 2, 'character': 17} + numpy_hov_position_2 = {"line": 2, "character": 17} # Over 'np' in np.sin - numpy_hov_position_3 = {'line': 3, 'character': 1} + numpy_hov_position_3 = {"line": 3, "character": 1} # Over 'sin' in np.sin - numpy_sin_hov_position = {'line': 3, 'character': 4} + numpy_sin_hov_position = {"line": 3, "character": 4} doc = Document(DOC_URI, workspace, NUMPY_DOC) - contents = '' - assert contents in pylsp_hover(doc._config, doc, no_hov_position)['contents'] + contents = "" + assert contents in pylsp_hover(doc._config, doc, no_hov_position)["contents"] - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_1)['contents']['value'] + contents = "NumPy\n=====\n\nProvides\n" + assert ( + contents + in pylsp_hover(doc._config, doc, numpy_hov_position_1)["contents"]["value"] + ) - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_2)['contents']['value'] + contents = "NumPy\n=====\n\nProvides\n" + assert ( + contents + in pylsp_hover(doc._config, doc, numpy_hov_position_2)["contents"]["value"] + ) - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_3)['contents']['value'] + contents = "NumPy\n=====\n\nProvides\n" + assert ( + contents + in pylsp_hover(doc._config, doc, numpy_hov_position_3)["contents"]["value"] + ) # https://github.com/davidhalter/jedi/issues/1746 # pylint: disable=import-outside-toplevel import numpy as np - if np.lib.NumpyVersion(np.__version__) < '1.20.0': - contents = 'Trigonometric sine, element-wise.\n\n' - assert contents in pylsp_hover(doc._config, - doc, numpy_sin_hov_position)['contents']['value'] + if np.lib.NumpyVersion(np.__version__) < "1.20.0": + contents = "Trigonometric sine, element-wise.\n\n" + assert ( + contents + in pylsp_hover(doc._config, doc, numpy_sin_hov_position)["contents"][ + "value" + ] + ) def test_hover(workspace): # Over 'main' in def main(): - hov_position = {'line': 2, 'character': 6} + hov_position = {"line": 2, "character": 6} # Over the blank second line - no_hov_position = {'line': 1, 'character': 0} + no_hov_position = {"line": 1, "character": 0} doc = Document(DOC_URI, workspace, DOC) - contents = {'kind': 'markdown', 'value': '```python\nmain()\n```\n\n\nhello world'} + contents = {"kind": "markdown", "value": "```python\nmain()\n```\n\n\nhello world"} - assert { - 'contents': contents - } == pylsp_hover(doc._config, doc, hov_position) + assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position) - assert {'contents': ''} == pylsp_hover(doc._config, doc, no_hov_position) + assert {"contents": ""} == pylsp_hover(doc._config, doc, no_hov_position) def test_document_path_hover(workspace_other_root_path, tmpdir): @@ -91,11 +102,11 @@ def foo(): # Content of doc to test definition doc_content = """from mymodule import foo foo""" - doc_path = str(tmpdir) + os.path.sep + 'myfile.py' + doc_path = str(tmpdir) + os.path.sep + "myfile.py" doc_uri = uris.from_fs_path(doc_path) doc = Document(doc_uri, workspace_other_root_path, doc_content) - cursor_pos = {'line': 1, 'character': 3} - contents = pylsp_hover(doc._config, doc, cursor_pos)['contents'] + cursor_pos = {"line": 1, "character": 3} + contents = pylsp_hover(doc._config, doc, cursor_pos)["contents"] - assert 'A docstring for foo.' in contents['value'] + assert "A docstring for foo." in contents["value"] diff --git a/test/plugins/test_jedi_rename.py b/test/plugins/test_jedi_rename.py index fb2f97f1..c3a1e485 100644 --- a/test/plugins/test_jedi_rename.py +++ b/test/plugins/test_jedi_rename.py @@ -9,71 +9,71 @@ from pylsp.plugins.jedi_rename import pylsp_rename from pylsp.workspace import Document -LT_PY36 = sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 6) +LT_PY36 = sys.version_info.major < 3 or ( + sys.version_info.major == 3 and sys.version_info.minor < 6 +) -DOC_NAME = 'test1.py' -DOC = '''class Test1(): +DOC_NAME = "test1.py" +DOC = """class Test1(): pass class Test2(Test1): pass -''' +""" -DOC_NAME_EXTRA = 'test2.py' -DOC_EXTRA = '''from test1 import Test1 +DOC_NAME_EXTRA = "test2.py" +DOC_EXTRA = """from test1 import Test1 x = Test1() -''' +""" @pytest.fixture def tmp_workspace(temp_workspace_factory): - return temp_workspace_factory({ - DOC_NAME: DOC, - DOC_NAME_EXTRA: DOC_EXTRA - }) + return temp_workspace_factory({DOC_NAME: DOC, DOC_NAME_EXTRA: DOC_EXTRA}) -@pytest.mark.skipif(LT_PY36, reason='Jedi refactoring isnt supported on Python 2.x/3.5') +@pytest.mark.skipif(LT_PY36, reason="Jedi refactoring isnt supported on Python 2.x/3.5") def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name # rename the `Test1` class - position = {'line': 0, 'character': 6} + position = {"line": 0, "character": 6} DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME)) doc = Document(DOC_URI, tmp_workspace) - result = pylsp_rename(config, tmp_workspace, doc, position, 'ShouldBeRenamed') + result = pylsp_rename(config, tmp_workspace, doc, position, "ShouldBeRenamed") assert len(result.keys()) == 1 - changes = result.get('documentChanges') + changes = result.get("documentChanges") assert len(changes) == 2 - assert changes[0]['textDocument']['uri'] == doc.uri - assert changes[0]['textDocument']['version'] == doc.version - assert changes[0].get('edits') == [ + assert changes[0]["textDocument"]["uri"] == doc.uri + assert changes[0]["textDocument"]["version"] == doc.version + assert changes[0].get("edits") == [ { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 5, 'character': 0}, + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 5, "character": 0}, }, - 'newText': 'class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n', + "newText": "class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n", } ] path = os.path.join(tmp_workspace.root_path, DOC_NAME_EXTRA) uri_extra = uris.from_fs_path(path) - assert changes[1]['textDocument']['uri'] == uri_extra + assert changes[1]["textDocument"]["uri"] == uri_extra # This also checks whether documents not yet added via textDocument/didOpen # but that do need to be renamed in the project have a `null` version # number. - assert changes[1]['textDocument']['version'] is None - expected = 'from test1 import ShouldBeRenamed\nx = ShouldBeRenamed()\n' - if os.name == 'nt': + assert changes[1]["textDocument"]["version"] is None + expected = "from test1 import ShouldBeRenamed\nx = ShouldBeRenamed()\n" + if os.name == "nt": # The .write method in the temp_workspace_factory functions writes # Windows-style line-endings. - expected = expected.replace('\n', '\r\n') - assert changes[1].get('edits') == [ + expected = expected.replace("\n", "\r\n") + assert changes[1].get("edits") == [ { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 2, 'character': 0}}, - 'newText': expected + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 2, "character": 0}, + }, + "newText": expected, } ] diff --git a/test/plugins/test_mccabe_lint.py b/test/plugins/test_mccabe_lint.py index 975415e1..009d8cfc 100644 --- a/test/plugins/test_mccabe_lint.py +++ b/test/plugins/test_mccabe_lint.py @@ -17,19 +17,19 @@ def test_mccabe(config, workspace): old_settings = config.settings try: - config.update({'plugins': {'mccabe': {'threshold': 1}}}) + config.update({"plugins": {"mccabe": {"threshold": 1}}}) doc = Document(DOC_URI, workspace, DOC) diags = mccabe_lint.pylsp_lint(config, workspace, doc) - assert all(d['source'] == 'mccabe' for d in diags) + assert all(d["source"] == "mccabe" for d in diags) # One we're expecting is: - msg = 'Cyclomatic complexity too high: 1 (threshold 1)' - mod_import = [d for d in diags if d['message'] == msg][0] + msg = "Cyclomatic complexity too high: 1 (threshold 1)" + mod_import = [d for d in diags if d["message"] == msg][0] - assert mod_import['severity'] == lsp.DiagnosticSeverity.Warning - assert mod_import['range']['start'] == {'line': 0, 'character': 0} - assert mod_import['range']['end'] == {'line': 0, 'character': 6} + assert mod_import["severity"] == lsp.DiagnosticSeverity.Warning + assert mod_import["range"]["start"] == {"line": 0, "character": 0} + assert mod_import["range"]["end"] == {"line": 0, "character": 6} finally: config._settings = old_settings diff --git a/test/plugins/test_pycodestyle_lint.py b/test/plugins/test_pycodestyle_lint.py index 03233569..dd24daac 100644 --- a/test/plugins/test_pycodestyle_lint.py +++ b/test/plugins/test_pycodestyle_lint.py @@ -28,44 +28,44 @@ def test_pycodestyle(workspace): doc = Document(DOC_URI, workspace, DOC) diags = pycodestyle_lint.pylsp_lint(workspace, doc) - assert all(d['source'] == 'pycodestyle' for d in diags) + assert all(d["source"] == "pycodestyle" for d in diags) # One we're expecting is: - msg = 'W191 indentation contains tabs' - mod_import = [d for d in diags if d['message'] == msg][0] + msg = "W191 indentation contains tabs" + mod_import = [d for d in diags if d["message"] == msg][0] - assert mod_import['code'] == 'W191' - assert mod_import['severity'] == lsp.DiagnosticSeverity.Warning - assert mod_import['range']['start'] == {'line': 3, 'character': 0} - assert mod_import['range']['end'] == {'line': 3, 'character': 6} + assert mod_import["code"] == "W191" + assert mod_import["severity"] == lsp.DiagnosticSeverity.Warning + assert mod_import["range"]["start"] == {"line": 3, "character": 0} + assert mod_import["range"]["end"] == {"line": 3, "character": 6} - msg = 'W391 blank line at end of file' - mod_import = [d for d in diags if d['message'] == msg][0] + msg = "W391 blank line at end of file" + mod_import = [d for d in diags if d["message"] == msg][0] - assert mod_import['code'] == 'W391' - assert mod_import['severity'] == lsp.DiagnosticSeverity.Warning - assert mod_import['range']['start'] == {'line': 10, 'character': 0} - assert mod_import['range']['end'] == {'line': 10, 'character': 1} + assert mod_import["code"] == "W391" + assert mod_import["severity"] == lsp.DiagnosticSeverity.Warning + assert mod_import["range"]["start"] == {"line": 10, "character": 0} + assert mod_import["range"]["end"] == {"line": 10, "character": 1} msg = "E201 whitespace after '('" - mod_import = [d for d in diags if d['message'] == msg][0] + mod_import = [d for d in diags if d["message"] == msg][0] - assert mod_import['code'] == 'E201' - assert mod_import['severity'] == lsp.DiagnosticSeverity.Warning - assert mod_import['range']['start'] == {'line': 2, 'character': 10} - assert mod_import['range']['end'] == {'line': 2, 'character': 14} + assert mod_import["code"] == "E201" + assert mod_import["severity"] == lsp.DiagnosticSeverity.Warning + assert mod_import["range"]["start"] == {"line": 2, "character": 10} + assert mod_import["range"]["end"] == {"line": 2, "character": 14} msg = "E128 continuation line under-indented for visual indent" - mod_import = [d for d in diags if d['message'] == msg][0] + mod_import = [d for d in diags if d["message"] == msg][0] - assert mod_import['code'] == 'E128' - assert mod_import['severity'] == lsp.DiagnosticSeverity.Warning - assert mod_import['range']['start'] == {'line': 5, 'character': 1} - assert mod_import['range']['end'] == {'line': 5, 'character': 10} + assert mod_import["code"] == "E128" + assert mod_import["severity"] == lsp.DiagnosticSeverity.Warning + assert mod_import["range"]["start"] == {"line": 5, "character": 1} + assert mod_import["range"]["end"] == {"line": 5, "character": 10} def test_pycodestyle_config(workspace): - """ Test that we load config files properly. + """Test that we load config files properly. Config files are loaded in the following order: tox.ini pep8.cfg setup.cfg pycodestyle.cfg @@ -79,43 +79,45 @@ def test_pycodestyle_config(workspace): If any section called 'pycodestyle' exists that will be solely used and any config in a 'pep8' section will be ignored """ - doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, 'test.py')) + doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "test.py")) workspace.put_document(doc_uri, DOC) doc = workspace.get_document(doc_uri) # Make sure we get a warning for 'indentation contains tabs' diags = pycodestyle_lint.pylsp_lint(workspace, doc) - assert [d for d in diags if d['code'] == 'W191'] + assert [d for d in diags if d["code"] == "W191"] content = { - 'setup.cfg': ('[pycodestyle]\nignore = W191, E201, E128', True), - 'tox.ini': ('', False) + "setup.cfg": ("[pycodestyle]\nignore = W191, E201, E128", True), + "tox.ini": ("", False), } for conf_file, (content, working) in list(content.items()): # Now we'll add config file to ignore it - with open(os.path.join(workspace.root_path, conf_file), 'w+', encoding='utf-8') as f: + with open( + os.path.join(workspace.root_path, conf_file), "w+", encoding="utf-8" + ) as f: f.write(content) workspace._config.settings.cache_clear() # And make sure we don't get any warnings diags = pycodestyle_lint.pylsp_lint(workspace, doc) - assert len([d for d in diags if d['code'] == 'W191']) == (0 if working else 1) - assert len([d for d in diags if d['code'] == 'E201']) == (0 if working else 1) - assert [d for d in diags if d['code'] == 'W391'] + assert len([d for d in diags if d["code"] == "W191"]) == (0 if working else 1) + assert len([d for d in diags if d["code"] == "E201"]) == (0 if working else 1) + assert [d for d in diags if d["code"] == "W391"] os.unlink(os.path.join(workspace.root_path, conf_file)) # Make sure we can ignore via the PYLS config as well - workspace._config.update({'plugins': {'pycodestyle': {'ignore': ['W191', 'E201']}}}) + workspace._config.update({"plugins": {"pycodestyle": {"ignore": ["W191", "E201"]}}}) # And make sure we only get one warning diags = pycodestyle_lint.pylsp_lint(workspace, doc) - assert not [d for d in diags if d['code'] == 'W191'] - assert not [d for d in diags if d['code'] == 'E201'] - assert [d for d in diags if d['code'] == 'W391'] + assert not [d for d in diags if d["code"] == "W191"] + assert not [d for d in diags if d["code"] == "E201"] + assert [d for d in diags if d["code"] == "W391"] -@pytest.mark.parametrize('newline', ['\r\n', '\r']) +@pytest.mark.parametrize("newline", ["\r\n", "\r"]) def test_line_endings(workspace, newline): """ Check that Pycodestyle doesn't generate false positives with line endings diff --git a/test/plugins/test_pydocstyle_lint.py b/test/plugins/test_pydocstyle_lint.py index c3232d20..df352da8 100644 --- a/test/plugins/test_pydocstyle_lint.py +++ b/test/plugins/test_pydocstyle_lint.py @@ -22,18 +22,18 @@ def test_pydocstyle(config, workspace): doc = Document(DOC_URI, workspace, DOC) diags = pydocstyle_lint.pylsp_lint(config, workspace, doc) - assert all(d['source'] == 'pydocstyle' for d in diags) + assert all(d["source"] == "pydocstyle" for d in diags) # One we're expecting is: assert diags[0] == { - 'code': 'D100', - 'message': 'D100: Missing docstring in public module', - 'severity': lsp.DiagnosticSeverity.Warning, - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 11}, + "code": "D100", + "message": "D100: Missing docstring in public module", + "severity": lsp.DiagnosticSeverity.Warning, + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 11}, }, - 'source': 'pydocstyle' + "source": "pydocstyle", } @@ -47,7 +47,7 @@ def test_pydocstyle_test_document(config, workspace): def test_pydocstyle_empty_source(config, workspace): doc = Document(DOC_URI, workspace, "") diags = pydocstyle_lint.pylsp_lint(config, workspace, doc) - assert diags[0]['message'] == 'D100: Missing docstring in public module' + assert diags[0]["message"] == "D100: Missing docstring in public module" assert len(diags) == 1 diff --git a/test/plugins/test_pyflakes_lint.py b/test/plugins/test_pyflakes_lint.py index ce8713d0..e33457f2 100644 --- a/test/plugins/test_pyflakes_lint.py +++ b/test/plugins/test_pyflakes_lint.py @@ -33,11 +33,11 @@ def test_pyflakes(workspace): diags = pyflakes_lint.pylsp_lint(workspace, doc) # One we're expecting is: - msg = '\'sys\' imported but unused' - unused_import = [d for d in diags if d['message'] == msg][0] + msg = "'sys' imported but unused" + unused_import = [d for d in diags if d["message"] == msg][0] - assert unused_import['range']['start'] == {'line': 0, 'character': 0} - assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning + assert unused_import["range"]["start"] == {"line": 0, "character": 0} + assert unused_import["severity"] == lsp.DiagnosticSeverity.Warning def test_syntax_error_pyflakes(workspace): @@ -45,20 +45,20 @@ def test_syntax_error_pyflakes(workspace): diag = pyflakes_lint.pylsp_lint(workspace, doc)[0] if sys.version_info[:2] >= (3, 10): - assert diag['message'] == "expected ':'" + assert diag["message"] == "expected ':'" else: - assert diag['message'] == 'invalid syntax' - assert diag['range']['start'] == {'line': 0, 'character': 12} - assert diag['severity'] == lsp.DiagnosticSeverity.Error + assert diag["message"] == "invalid syntax" + assert diag["range"]["start"] == {"line": 0, "character": 12} + assert diag["severity"] == lsp.DiagnosticSeverity.Error def test_undefined_name_pyflakes(workspace): doc = Document(DOC_URI, workspace, DOC_UNDEFINED_NAME_ERR) diag = pyflakes_lint.pylsp_lint(workspace, doc)[0] - assert diag['message'] == 'undefined name \'b\'' - assert diag['range']['start'] == {'line': 0, 'character': 4} - assert diag['severity'] == lsp.DiagnosticSeverity.Error + assert diag["message"] == "undefined name 'b'" + assert diag["range"]["start"] == {"line": 0, "character": 4} + assert diag["severity"] == lsp.DiagnosticSeverity.Error def test_unicode_encoding(workspace): @@ -66,4 +66,4 @@ def test_unicode_encoding(workspace): diags = pyflakes_lint.pylsp_lint(workspace, doc) assert len(diags) == 1 - assert diags[0]['message'] == '\'sys\' imported but unused' + assert diags[0]["message"] == "'sys' imported but unused" diff --git a/test/plugins/test_pylint_lint.py b/test/plugins/test_pylint_lint.py index 3eae8914..210e1cff 100644 --- a/test/plugins/test_pylint_lint.py +++ b/test/plugins/test_pylint_lint.py @@ -28,7 +28,7 @@ def hello(): @contextlib.contextmanager def temp_document(doc_text, workspace): try: - with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file: name = temp_file.name temp_file.write(doc_text) yield Document(uris.from_fs_path(name), workspace) @@ -37,7 +37,7 @@ def temp_document(doc_text, workspace): def write_temp_doc(document, contents): - with open(document.path, 'w', encoding='utf-8') as temp_file: + with open(document.path, "w", encoding="utf-8") as temp_file: temp_file.write(contents) @@ -45,46 +45,50 @@ def test_pylint(config, workspace): with temp_document(DOC, workspace) as doc: diags = pylint_lint.pylsp_lint(config, workspace, doc, True) - msg = '[unused-import] Unused import sys' - unused_import = [d for d in diags if d['message'] == msg][0] + msg = "[unused-import] Unused import sys" + unused_import = [d for d in diags if d["message"] == msg][0] - assert unused_import['range']['start'] == {'line': 0, 'character': 0} - assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning - assert unused_import['tags'] == [lsp.DiagnosticTag.Unnecessary] + assert unused_import["range"]["start"] == {"line": 0, "character": 0} + assert unused_import["severity"] == lsp.DiagnosticSeverity.Warning + assert unused_import["tags"] == [lsp.DiagnosticTag.Unnecessary] # test running pylint in stdin - config.plugin_settings('pylint')['executable'] = 'pylint' + config.plugin_settings("pylint")["executable"] = "pylint" diags = pylint_lint.pylsp_lint(config, workspace, doc, True) - msg = 'Unused import sys (unused-import)' - unused_import = [d for d in diags if d['message'] == msg][0] + msg = "Unused import sys (unused-import)" + unused_import = [d for d in diags if d["message"] == msg][0] - assert unused_import['range']['start'] == { - 'line': 0, - 'character': 0, + assert unused_import["range"]["start"] == { + "line": 0, + "character": 0, } - assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning + assert unused_import["severity"] == lsp.DiagnosticSeverity.Warning def test_syntax_error_pylint(config, workspace): with temp_document(DOC_SYNTAX_ERR, workspace) as doc: diag = pylint_lint.pylsp_lint(config, workspace, doc, True)[0] - assert diag['message'].startswith("[syntax-error]") - assert diag['message'].count("expected ':'") or diag['message'].count('invalid syntax') + assert diag["message"].startswith("[syntax-error]") + assert diag["message"].count("expected ':'") or diag["message"].count( + "invalid syntax" + ) # Pylint doesn't give column numbers for invalid syntax. - assert diag['range']['start'] == {'line': 0, 'character': 12} - assert diag['severity'] == lsp.DiagnosticSeverity.Error - assert 'tags' not in diag + assert diag["range"]["start"] == {"line": 0, "character": 12} + assert diag["severity"] == lsp.DiagnosticSeverity.Error + assert "tags" not in diag # test running pylint in stdin - config.plugin_settings('pylint')['executable'] = 'pylint' + config.plugin_settings("pylint")["executable"] = "pylint" diag = pylint_lint.pylsp_lint(config, workspace, doc, True)[0] - assert diag['message'].count("expected ':'") or diag['message'].count('invalid syntax') + assert diag["message"].count("expected ':'") or diag["message"].count( + "invalid syntax" + ) # Pylint doesn't give column numbers for invalid syntax. - assert diag['range']['start'] == {'line': 0, 'character': 12} - assert diag['severity'] == lsp.DiagnosticSeverity.Error + assert diag["range"]["start"] == {"line": 0, "character": 12} + assert diag["severity"] == lsp.DiagnosticSeverity.Error def test_lint_free_pylint(config, workspace): @@ -93,7 +97,8 @@ def test_lint_free_pylint(config, workspace): # though, so it works for a test of an empty lint. ws = Workspace(str(Path(__file__).absolute().parents[2]), workspace._endpoint) assert not pylint_lint.pylsp_lint( - config, ws, Document(uris.from_fs_path(__file__), ws), True) + config, ws, Document(uris.from_fs_path(__file__), ws), True + ) def test_lint_caching(workspace): @@ -106,7 +111,7 @@ def test_lint_caching(workspace): # need to ensure that pylint doesn't give us invalid-name when our temp # file has capital letters in its name. - flags = '--disable=invalid-name' + flags = "--disable=invalid-name" with temp_document(DOC, workspace) as doc: # Start with a file with errors. diags = pylint_lint.PylintLinter.lint(doc, True, flags) @@ -114,7 +119,7 @@ def test_lint_caching(workspace): # Fix lint errors and write the changes to disk. Run the linter in the # in-memory mode to check the cached diagnostic behavior. - write_temp_doc(doc, '') + write_temp_doc(doc, "") assert pylint_lint.PylintLinter.lint(doc, False, flags) == diags # Now check the on-disk behavior. @@ -130,4 +135,5 @@ def test_per_file_caching(config, workspace): assert pylint_lint.pylsp_lint(config, workspace, doc, True) assert not pylint_lint.pylsp_lint( - config, workspace, Document(uris.from_fs_path(__file__), workspace), False) + config, workspace, Document(uris.from_fs_path(__file__), workspace), False + ) diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py index c1df037b..20906dff 100644 --- a/test/plugins/test_references.py +++ b/test/plugins/test_references.py @@ -10,8 +10,8 @@ from pylsp.plugins.references import pylsp_references -DOC1_NAME = 'test1.py' -DOC2_NAME = 'test2.py' +DOC1_NAME = "test1.py" +DOC2_NAME = "test2.py" DOC1 = """class Test1(): pass @@ -28,15 +28,17 @@ @pytest.fixture def tmp_workspace(temp_workspace_factory): - return temp_workspace_factory({ - DOC1_NAME: DOC1, - DOC2_NAME: DOC2, - }) + return temp_workspace_factory( + { + DOC1_NAME: DOC1, + DOC2_NAME: DOC2, + } + ) def test_references(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'Test1' in class Test1(): - position = {'line': 0, 'character': 8} + position = {"line": 0, "character": 8} DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME)) doc1 = Document(DOC1_URI, tmp_workspace) @@ -50,30 +52,32 @@ def test_references(tmp_workspace): # pylint: disable=redefined-outer-name assert len(no_def_refs) == 1 # Make sure our definition is correctly located - doc1_ref = [u for u in refs if u['uri'] == DOC1_URI][0] - assert doc1_ref['range']['start'] == {'line': 0, 'character': 6} - assert doc1_ref['range']['end'] == {'line': 0, 'character': 11} + doc1_ref = [u for u in refs if u["uri"] == DOC1_URI][0] + assert doc1_ref["range"]["start"] == {"line": 0, "character": 6} + assert doc1_ref["range"]["end"] == {"line": 0, "character": 11} # Make sure our import is correctly located - doc2_import_ref = [u for u in refs if u['uri'] != DOC1_URI][0] - assert doc2_import_ref['range']['start'] == {'line': 0, 'character': 18} - assert doc2_import_ref['range']['end'] == {'line': 0, 'character': 23} + doc2_import_ref = [u for u in refs if u["uri"] != DOC1_URI][0] + assert doc2_import_ref["range"]["start"] == {"line": 0, "character": 18} + assert doc2_import_ref["range"]["end"] == {"line": 0, "character": 23} - doc2_usage_ref = [u for u in refs if u['uri'] != DOC1_URI][1] - assert doc2_usage_ref['range']['start'] == {'line': 3, 'character': 4} - assert doc2_usage_ref['range']['end'] == {'line': 3, 'character': 9} + doc2_usage_ref = [u for u in refs if u["uri"] != DOC1_URI][1] + assert doc2_usage_ref["range"]["start"] == {"line": 3, "character": 4} + assert doc2_usage_ref["range"]["end"] == {"line": 3, "character": 9} def test_references_builtin(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'UnicodeError': - position = {'line': 4, 'character': 7} + position = {"line": 4, "character": 7} doc2_uri = uris.from_fs_path(os.path.join(str(tmp_workspace.root_path), DOC2_NAME)) doc2 = Document(doc2_uri, tmp_workspace) refs = pylsp_references(doc2, position) assert len(refs) >= 1 - expected = {'start': {'line': 4, 'character': 7}, - 'end': {'line': 4, 'character': 19}} - ranges = [r['range'] for r in refs] + expected = { + "start": {"line": 4, "character": 7}, + "end": {"line": 4, "character": 19}, + } + ranges = [r["range"] for r in refs] assert expected in ranges diff --git a/test/plugins/test_signature.py b/test/plugins/test_signature.py index d9dbb8d2..0ba28ac5 100644 --- a/test/plugins/test_signature.py +++ b/test/plugins/test_signature.py @@ -43,54 +43,63 @@ def main(param1=None, def test_no_signature(workspace): # Over blank line - sig_position = {'line': 9, 'character': 0} + sig_position = {"line": 9, "character": 0} doc = Document(DOC_URI, workspace, DOC) - sigs = signature.pylsp_signature_help(doc._config, doc, sig_position)['signatures'] + sigs = signature.pylsp_signature_help(doc._config, doc, sig_position)["signatures"] assert not sigs def test_signature(workspace): # Over '( ' in main( - sig_position = {'line': 10, 'character': 5} + sig_position = {"line": 10, "character": 5} doc = Document(DOC_URI, workspace, DOC) sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position) - sigs = sig_info['signatures'] + sigs = sig_info["signatures"] assert len(sigs) == 1 - assert sigs[0]['label'] == 'main(param1, param2)' - assert sigs[0]['parameters'][0]['label'] == 'param1' - assert sigs[0]['parameters'][0]['documentation'] == {'kind': 'markdown', 'value': 'Docs for param1'} + assert sigs[0]["label"] == "main(param1, param2)" + assert sigs[0]["parameters"][0]["label"] == "param1" + assert sigs[0]["parameters"][0]["documentation"] == { + "kind": "markdown", + "value": "Docs for param1", + } - assert sig_info['activeParameter'] == 0 + assert sig_info["activeParameter"] == 0 def test_multi_line_signature(workspace): # Over '( ' in main( - sig_position = {'line': 17, 'character': 5} + sig_position = {"line": 17, "character": 5} doc = Document(DOC_URI, workspace, MULTI_LINE_DOC) sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position) - sigs = sig_info['signatures'] + sigs = sig_info["signatures"] assert len(sigs) == 1 - assert sigs[0]['label'] == ( - 'main(param1=None, param2=None, param3=None, param4=None, ' - 'param5=None, param6=None, param7=None, param8=None)' + assert sigs[0]["label"] == ( + "main(param1=None, param2=None, param3=None, param4=None, " + "param5=None, param6=None, param7=None, param8=None)" ) - assert sigs[0]['parameters'][0]['label'] == 'param1' - assert sigs[0]['parameters'][0]['documentation'] == {'kind': 'markdown', 'value': 'Docs for param1'} - - assert sig_info['activeParameter'] == 0 - - -@pytest.mark.parametrize('regex,doc', [ - (signature.SPHINX, " :param test: parameter docstring"), - (signature.EPYDOC, " @param test: parameter docstring"), - (signature.GOOGLE, " test (str): parameter docstring") -]) + assert sigs[0]["parameters"][0]["label"] == "param1" + assert sigs[0]["parameters"][0]["documentation"] == { + "kind": "markdown", + "value": "Docs for param1", + } + + assert sig_info["activeParameter"] == 0 + + +@pytest.mark.parametrize( + "regex,doc", + [ + (signature.SPHINX, " :param test: parameter docstring"), + (signature.EPYDOC, " @param test: parameter docstring"), + (signature.GOOGLE, " test (str): parameter docstring"), + ], +) def test_docstring_params(regex, doc): m = regex.match(doc) - assert m.group('param') == "test" - assert m.group('doc') == "parameter docstring" + assert m.group("param") == "test" + assert m.group("doc") == "parameter docstring" diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py index 40e3e1e3..0f54e9db 100644 --- a/test/plugins/test_symbols.py +++ b/test/plugins/test_symbols.py @@ -12,9 +12,9 @@ from pylsp.workspace import Document -PY2 = sys.version[0] == '2' -LINUX = sys.platform.startswith('linux') -CI = os.environ.get('CI') +PY2 = sys.version[0] == "2" +LINUX = sys.platform.startswith("linux") +CI = os.environ.get("CI") DOC_URI = uris.from_fs_path(__file__) DOC = """import sys @@ -37,21 +37,21 @@ def helper_check_symbols_all_scope(symbols): assert len(symbols) == 8 def sym(name): - return [s for s in symbols if s['name'] == name][0] + return [s for s in symbols if s["name"] == name][0] # Check we have some sane mappings to VSCode constants - assert sym('a')['kind'] == SymbolKind.Variable - assert sym('B')['kind'] == SymbolKind.Class - assert sym('__init__')['kind'] == SymbolKind.Method - assert sym('main')['kind'] == SymbolKind.Function + assert sym("a")["kind"] == SymbolKind.Variable + assert sym("B")["kind"] == SymbolKind.Class + assert sym("__init__")["kind"] == SymbolKind.Method + assert sym("main")["kind"] == SymbolKind.Function # Not going to get too in-depth here else we're just testing Jedi - assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0} + assert sym("a")["location"]["range"]["start"] == {"line": 2, "character": 0} def test_symbols(config, workspace): doc = Document(DOC_URI, workspace, DOC) - config.update({'plugins': {'jedi_symbols': {'all_scopes': False}}}) + config.update({"plugins": {"jedi_symbols": {"all_scopes": False}}}) symbols = pylsp_document_symbols(config, doc) # All four symbols (import sys, a, B, main) @@ -59,19 +59,19 @@ def test_symbols(config, workspace): assert len(symbols) == 5 def sym(name): - return [s for s in symbols if s['name'] == name][0] + return [s for s in symbols if s["name"] == name][0] # Check we have some sane mappings to VSCode constants - assert sym('a')['kind'] == SymbolKind.Variable - assert sym('B')['kind'] == SymbolKind.Class - assert sym('main')['kind'] == SymbolKind.Function + assert sym("a")["kind"] == SymbolKind.Variable + assert sym("B")["kind"] == SymbolKind.Class + assert sym("main")["kind"] == SymbolKind.Function # Not going to get too in-depth here else we're just testing Jedi - assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0} + assert sym("a")["location"]["range"]["start"] == {"line": 2, "character": 0} # Ensure that the symbol range spans the whole definition - assert sym('main')['location']['range']['start'] == {'line': 9, 'character': 0} - assert sym('main')['location']['range']['end'] == {'line': 12, 'character': 0} + assert sym("main")["location"]["range"]["start"] == {"line": 9, "character": 0} + assert sym("main")["location"]["range"]["end"] == {"line": 12, "character": 0} def test_symbols_all_scopes(config, workspace): @@ -90,13 +90,15 @@ def test_symbols_non_existing_file(config, workspace, tmpdir): helper_check_symbols_all_scope(symbols) -@pytest.mark.skipif(PY2 or not LINUX or not CI, reason="tested on linux and python 3 only") +@pytest.mark.skipif( + PY2 or not LINUX or not CI, reason="tested on linux and python 3 only" +) def test_symbols_all_scopes_with_jedi_environment(workspace): doc = Document(DOC_URI, workspace, DOC) # Update config extra environment - env_path = '/tmp/pyenv/bin/python' - settings = {'pylsp': {'plugins': {'jedi': {'environment': env_path}}}} + env_path = "/tmp/pyenv/bin/python" + settings = {"pylsp": {"plugins": {"jedi": {"environment": env_path}}}} doc.update_config(settings) symbols = pylsp_document_symbols(doc._config, doc) helper_check_symbols_all_scope(symbols) diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 62808e6a..d89eb37d 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -38,8 +38,8 @@ def test_range_format(workspace): doc = Document(DOC_URI, workspace, DOC) def_range = { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 4, 'character': 10} + "start": {"line": 0, "character": 0}, + "end": {"line": 4, "character": 10}, } res = pylsp_format_range(doc, def_range, None) @@ -54,23 +54,29 @@ def test_no_change(workspace): def test_config_file(tmpdir, workspace): # a config file in the same directory as the source file will be used - conf = tmpdir.join('.style.yapf') - conf.write('[style]\ncolumn_limit = 14') - src = tmpdir.join('test.py') + conf = tmpdir.join(".style.yapf") + conf.write("[style]\ncolumn_limit = 14") + src = tmpdir.join("test.py") doc = Document(uris.from_fs_path(src.strpath), workspace, DOC) res = pylsp_format_document(workspace, doc, options=None) # A was split on multiple lines because of column_limit from config file - assert apply_text_edits(doc, res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" + assert ( + apply_text_edits(doc, res) + == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" + ) -@pytest.mark.parametrize('newline', ['\r\n']) +@pytest.mark.parametrize("newline", ["\r\n"]) def test_line_endings(workspace, newline): - doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') + doc = Document(DOC_URI, workspace, f"import os;import sys{2 * newline}dict(a=1)") res = pylsp_format_document(workspace, doc, options=None) - assert apply_text_edits(doc, res) == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' + assert ( + apply_text_edits(doc, res) + == f"import os{newline}import sys{2 * newline}dict(a=1){newline}" + ) def test_format_with_tab_size_option(workspace): @@ -103,7 +109,7 @@ def test_format_returns_text_edit_per_line(workspace): # two removes and two adds assert len(res) == 4 - assert res[0]['newText'] == "" - assert res[1]['newText'] == "" - assert res[2]['newText'] == " log(\"x\")\n" - assert res[3]['newText'] == " log(\"hi\")\n" + assert res[0]["newText"] == "" + assert res[1]["newText"] == "" + assert res[2]["newText"] == ' log("x")\n' + assert res[3]["newText"] == ' log("hi")\n' diff --git a/test/test_document.py b/test/test_document.py index 3dcabb68..1acb1611 100644 --- a/test/test_document.py +++ b/test/test_document.py @@ -12,88 +12,92 @@ def test_document_props(doc): def test_document_lines(doc): assert len(doc.lines) == 4 - assert doc.lines[0] == 'import sys\n' + assert doc.lines[0] == "import sys\n" def test_document_source_unicode(workspace): - document_mem = Document(DOC_URI, workspace, 'my source') + document_mem = Document(DOC_URI, workspace, "my source") document_disk = Document(DOC_URI, workspace) assert isinstance(document_mem.source, type(document_disk.source)) def test_offset_at_position(doc): - assert doc.offset_at_position({'line': 0, 'character': 8}) == 8 - assert doc.offset_at_position({'line': 1, 'character': 5}) == 16 - assert doc.offset_at_position({'line': 2, 'character': 0}) == 12 - assert doc.offset_at_position({'line': 2, 'character': 4}) == 16 - assert doc.offset_at_position({'line': 4, 'character': 0}) == 51 + assert doc.offset_at_position({"line": 0, "character": 8}) == 8 + assert doc.offset_at_position({"line": 1, "character": 5}) == 16 + assert doc.offset_at_position({"line": 2, "character": 0}) == 12 + assert doc.offset_at_position({"line": 2, "character": 4}) == 16 + assert doc.offset_at_position({"line": 4, "character": 0}) == 51 def test_word_at_position(doc): - """ Return the position under the cursor (or last in line if past the end) """ + """Return the position under the cursor (or last in line if past the end)""" # import sys - assert doc.word_at_position({'line': 0, 'character': 8}) == 'sys' + assert doc.word_at_position({"line": 0, "character": 8}) == "sys" # Past end of import sys - assert doc.word_at_position({'line': 0, 'character': 1000}) == 'sys' + assert doc.word_at_position({"line": 0, "character": 1000}) == "sys" # Empty line - assert doc.word_at_position({'line': 1, 'character': 5}) == '' + assert doc.word_at_position({"line": 1, "character": 5}) == "" # def main(): - assert doc.word_at_position({'line': 2, 'character': 0}) == 'def' + assert doc.word_at_position({"line": 2, "character": 0}) == "def" # Past end of file - assert doc.word_at_position({'line': 4, 'character': 0}) == '' + assert doc.word_at_position({"line": 4, "character": 0}) == "" def test_document_empty_edit(workspace): - doc = Document('file:///uri', workspace, '') - doc.apply_change({ - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 0} - }, - 'text': 'f' - }) - assert doc.source == 'f' + doc = Document("file:///uri", workspace, "") + doc.apply_change( + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "text": "f", + } + ) + assert doc.source == "f" def test_document_line_edit(workspace): - doc = Document('file:///uri', workspace, 'itshelloworld') - doc.apply_change({ - 'text': 'goodbye', - 'range': { - 'start': {'line': 0, 'character': 3}, - 'end': {'line': 0, 'character': 8} + doc = Document("file:///uri", workspace, "itshelloworld") + doc.apply_change( + { + "text": "goodbye", + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 8}, + }, } - }) - assert doc.source == 'itsgoodbyeworld' + ) + assert doc.source == "itsgoodbyeworld" def test_document_multiline_edit(workspace): - old = [ - "def hello(a, b):\n", - " print a\n", - " print b\n" - ] - doc = Document('file:///uri', workspace, ''.join(old)) - doc.apply_change({'text': 'print a, b', 'range': { - 'start': {'line': 1, 'character': 4}, - 'end': {'line': 2, 'character': 11} - }}) - assert doc.lines == [ - "def hello(a, b):\n", - " print a, b\n" - ] + old = ["def hello(a, b):\n", " print a\n", " print b\n"] + doc = Document("file:///uri", workspace, "".join(old)) + doc.apply_change( + { + "text": "print a, b", + "range": { + "start": {"line": 1, "character": 4}, + "end": {"line": 2, "character": 11}, + }, + } + ) + assert doc.lines == ["def hello(a, b):\n", " print a, b\n"] def test_document_end_of_file_edit(workspace): - old = [ - "print 'a'\n", - "print 'b'\n" - ] - doc = Document('file:///uri', workspace, ''.join(old)) - doc.apply_change({'text': 'o', 'range': { - 'start': {'line': 2, 'character': 0}, - 'end': {'line': 2, 'character': 0} - }}) + old = ["print 'a'\n", "print 'b'\n"] + doc = Document("file:///uri", workspace, "".join(old)) + doc.apply_change( + { + "text": "o", + "range": { + "start": {"line": 2, "character": 0}, + "end": {"line": 2, "character": 0}, + }, + } + ) assert doc.lines == [ "print 'a'\n", "print 'b'\n", diff --git a/test/test_language_server.py b/test/test_language_server.py index 92d1ea84..8aa8d2c5 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -14,7 +14,7 @@ from pylsp.python_lsp import start_io_lang_server, PythonLSPServer CALL_TIMEOUT = 10 -RUNNING_IN_CI = bool(os.environ.get('CI')) +RUNNING_IN_CI = bool(os.environ.get("CI")) def start_client(client): @@ -22,14 +22,15 @@ def start_client(client): class _ClientServer: - """ A class to setup a client/server pair """ + """A class to setup a client/server pair""" + def __init__(self, check_parent_process=False): # Client to Server pipe csr, csw = os.pipe() # Server to client pipe scr, scw = os.pipe() - if os.name == 'nt': + if os.name == "nt": ParallelKind = Thread else: if sys.version_info[:2] >= (3, 8): @@ -37,12 +38,20 @@ def __init__(self, check_parent_process=False): else: ParallelKind = multiprocessing.Process - self.process = ParallelKind(target=start_io_lang_server, args=( - os.fdopen(csr, 'rb'), os.fdopen(scw, 'wb'), check_parent_process, PythonLSPServer - )) + self.process = ParallelKind( + target=start_io_lang_server, + args=( + os.fdopen(csr, "rb"), + os.fdopen(scw, "wb"), + check_parent_process, + PythonLSPServer, + ), + ) self.process.start() - self.client = PythonLSPServer(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb'), start_io_lang_server) + self.client = PythonLSPServer( + os.fdopen(scr, "rb"), os.fdopen(csw, "wb"), start_io_lang_server + ) self.client_thread = Thread(target=start_client, args=[self.client]) self.client_thread.daemon = True self.client_thread.start() @@ -50,21 +59,23 @@ def __init__(self, check_parent_process=False): @pytest.fixture def client_server(): - """ A fixture that sets up a client/server pair and shuts down the server + """A fixture that sets up a client/server pair and shuts down the server This client/server pair does not support checking parent process aliveness """ client_server_pair = _ClientServer() yield client_server_pair.client - shutdown_response = client_server_pair.client._endpoint.request('shutdown').result(timeout=CALL_TIMEOUT) + shutdown_response = client_server_pair.client._endpoint.request("shutdown").result( + timeout=CALL_TIMEOUT + ) assert shutdown_response is None - client_server_pair.client._endpoint.notify('exit') + client_server_pair.client._endpoint.notify("exit") @pytest.fixture def client_exited_server(): - """ A fixture that sets up a client/server pair that support checking parent process aliveness + """A fixture that sets up a client/server pair that support checking parent process aliveness and assert the server has already exited """ client_server_pair = _ClientServer(True) @@ -76,26 +87,33 @@ def client_exited_server(): @flaky(max_runs=10, min_passes=1) -@pytest.mark.skipif(sys.platform == 'darwin', reason='Too flaky on Mac') +@pytest.mark.skipif(sys.platform == "darwin", reason="Too flaky on Mac") def test_initialize(client_server): # pylint: disable=redefined-outer-name - response = client_server._endpoint.request('initialize', { - 'rootPath': os.path.dirname(__file__), - 'initializationOptions': {} - }).result(timeout=CALL_TIMEOUT) - assert 'capabilities' in response + response = client_server._endpoint.request( + "initialize", + {"rootPath": os.path.dirname(__file__), "initializationOptions": {}}, + ).result(timeout=CALL_TIMEOUT) + assert "capabilities" in response @flaky(max_runs=10, min_passes=1) -@pytest.mark.skipif(not sys.platform.startswith('Linux'), reason='Skipped on win and flaky on mac') -def test_exit_with_parent_process_died(client_exited_server): # pylint: disable=redefined-outer-name +@pytest.mark.skipif( + not sys.platform.startswith("Linux"), reason="Skipped on win and flaky on mac" +) +def test_exit_with_parent_process_died( + client_exited_server, +): # pylint: disable=redefined-outer-name # language server should have already exited before responding lsp_server, mock_process = client_exited_server.client, client_exited_server.process # with pytest.raises(Exception): - lsp_server._endpoint.request('initialize', { - 'processId': mock_process.pid, - 'rootPath': os.path.dirname(__file__), - 'initializationOptions': {} - }).result(timeout=CALL_TIMEOUT) + lsp_server._endpoint.request( + "initialize", + { + "processId": mock_process.pid, + "rootPath": os.path.dirname(__file__), + "initializationOptions": {}, + }, + ).result(timeout=CALL_TIMEOUT) mock_process.terminate() time.sleep(CALL_TIMEOUT) @@ -103,18 +121,23 @@ def test_exit_with_parent_process_died(client_exited_server): # pylint: disable @flaky(max_runs=10, min_passes=1) -@pytest.mark.skipif(sys.platform.startswith('linux'), reason='Fails on linux') -def test_not_exit_without_check_parent_process_flag(client_server): # pylint: disable=redefined-outer-name - response = client_server._endpoint.request('initialize', { - 'processId': 1234, - 'rootPath': os.path.dirname(__file__), - 'initializationOptions': {} - }).result(timeout=CALL_TIMEOUT) - assert 'capabilities' in response +@pytest.mark.skipif(sys.platform.startswith("linux"), reason="Fails on linux") +def test_not_exit_without_check_parent_process_flag( + client_server, +): # pylint: disable=redefined-outer-name + response = client_server._endpoint.request( + "initialize", + { + "processId": 1234, + "rootPath": os.path.dirname(__file__), + "initializationOptions": {}, + }, + ).result(timeout=CALL_TIMEOUT) + assert "capabilities" in response @flaky(max_runs=10, min_passes=1) -@pytest.mark.skipif(RUNNING_IN_CI, reason='This test is hanging on CI') +@pytest.mark.skipif(RUNNING_IN_CI, reason="This test is hanging on CI") def test_missing_message(client_server): # pylint: disable=redefined-outer-name with pytest.raises(JsonRpcMethodNotFound): - client_server._endpoint.request('unknown_method').result(timeout=CALL_TIMEOUT) + client_server._endpoint.request("unknown_method").result(timeout=CALL_TIMEOUT) diff --git a/test/test_text_edit.py b/test/test_text_edit.py index 3e4cce11..63d0c904 100644 --- a/test/test_text_edit.py +++ b/test/test_text_edit.py @@ -8,273 +8,240 @@ def test_apply_text_edits_insert(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + pylsp.workspace.put_document(DOC_URI, "012345678901234567890123456789") test_doc = pylsp.workspace.get_document(DOC_URI) - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 0, - "character": 0 - } - }, - "newText": "Hello" - }]) == 'Hello012345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }]) == '0Hello12345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "World" - }]) == '0HelloWorld12345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "One" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "Two" - }, { - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "Three" - }]) == '0HelloWorld1OneTwoThree2345678901234567890123456789' + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "newText": "Hello", + } + ], + ) + == "Hello012345678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 1}, + "end": {"line": 0, "character": 1}, + }, + "newText": "Hello", + } + ], + ) + == "0Hello12345678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 1}, + "end": {"line": 0, "character": 1}, + }, + "newText": "Hello", + }, + { + "range": { + "start": {"line": 0, "character": 1}, + "end": {"line": 0, "character": 1}, + }, + "newText": "World", + }, + ], + ) + == "0HelloWorld12345678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 2}, + "end": {"line": 0, "character": 2}, + }, + "newText": "One", + }, + { + "range": { + "start": {"line": 0, "character": 1}, + "end": {"line": 0, "character": 1}, + }, + "newText": "Hello", + }, + { + "range": { + "start": {"line": 0, "character": 1}, + "end": {"line": 0, "character": 1}, + }, + "newText": "World", + }, + { + "range": { + "start": {"line": 0, "character": 2}, + "end": {"line": 0, "character": 2}, + }, + "newText": "Two", + }, + { + "range": { + "start": {"line": 0, "character": 2}, + "end": {"line": 0, "character": 2}, + }, + "newText": "Three", + }, + ], + ) + == "0HelloWorld1OneTwoThree2345678901234567890123456789" + ) def test_apply_text_edits_replace(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + pylsp.workspace.put_document(DOC_URI, "012345678901234567890123456789") test_doc = pylsp.workspace.get_document(DOC_URI) - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012Hello678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 9 - } - }, - "newText": "World" - }]) == '012HelloWorld901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "World" - }]) == '012HelloWorld678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012HelloWorld678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 3 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012WorldHello678901234567890123456789' + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", + } + ], + ) + == "012Hello678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", + }, + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 9}, + }, + "newText": "World", + }, + ], + ) + == "012HelloWorld901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", + }, + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 6}, + }, + "newText": "World", + }, + ], + ) + == "012HelloWorld678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 6}, + "end": {"line": 0, "character": 6}, + }, + "newText": "World", + }, + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", + }, + ], + ) + == "012HelloWorld678901234567890123456789" + ) + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 3}, + }, + "newText": "World", + }, + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", + }, + ], + ) + == "012WorldHello678901234567890123456789" + ) def test_apply_text_edits_overlap(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + pylsp.workspace.put_document(DOC_URI, "012345678901234567890123456789") test_doc = pylsp.workspace.get_document(DOC_URI) did_throw = False try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 3 + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 3}, + }, + "newText": "World", }, - "end": { - "line": 0, - "character": 3 - } - }, - "newText": "World" - }]) + ], + ) except OverLappingTextEditException: did_throw = True @@ -283,31 +250,25 @@ def test_apply_text_edits_overlap(pylsp): did_throw = False try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 0, "character": 3}, + "end": {"line": 0, "character": 6}, + }, + "newText": "Hello", }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 4 + { + "range": { + "start": {"line": 0, "character": 4}, + "end": {"line": 0, "character": 4}, + }, + "newText": "World", }, - "end": { - "line": 0, - "character": 4 - } - }, - "newText": "World" - }]) + ], + ) except OverLappingTextEditException: did_throw = True @@ -315,31 +276,28 @@ def test_apply_text_edits_overlap(pylsp): def test_apply_text_edits_multiline(pylsp): - pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') + pylsp.workspace.put_document(DOC_URI, "0\n1\n2\n3\n4") test_doc = pylsp.workspace.get_document(DOC_URI) - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 2, - "character": 0 - }, - "end": { - "line": 3, - "character": 0 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 1, - "character": 1 - }, - "end": { - "line": 1, - "character": 1 - } - }, - "newText": "World" - }]) == '0\n1World\nHello3\n4' + assert ( + apply_text_edits( + test_doc, + [ + { + "range": { + "start": {"line": 2, "character": 0}, + "end": {"line": 3, "character": 0}, + }, + "newText": "Hello", + }, + { + "range": { + "start": {"line": 1, "character": 1}, + "end": {"line": 1, "character": 1}, + }, + "newText": "World", + }, + ], + ) + == "0\n1World\nHello3\n4" + ) diff --git a/test/test_uris.py b/test/test_uris.py index ad83c090..59fb2094 100644 --- a/test/test_uris.py +++ b/test/test_uris.py @@ -7,46 +7,65 @@ @unix_only -@pytest.mark.parametrize('uri,path', [ - ('file:///foo/bar#frag', '/foo/bar'), - ('file:/foo/bar#frag', '/foo/bar'), - ('file:/foo/space%20%3Fbar#frag', '/foo/space ?bar'), -]) +@pytest.mark.parametrize( + "uri,path", + [ + ("file:///foo/bar#frag", "/foo/bar"), + ("file:/foo/bar#frag", "/foo/bar"), + ("file:/foo/space%20%3Fbar#frag", "/foo/space ?bar"), + ], +) def test_to_fs_path(uri, path): assert uris.to_fs_path(uri) == path @windows_only -@pytest.mark.parametrize('uri,path', [ - ('file:///c:/far/boo', 'c:\\far\\boo'), - ('file:///C:/far/boo', 'c:\\far\\boo'), - ('file:///C:/far/space%20%3Fboo', 'c:\\far\\space ?boo'), -]) +@pytest.mark.parametrize( + "uri,path", + [ + ("file:///c:/far/boo", "c:\\far\\boo"), + ("file:///C:/far/boo", "c:\\far\\boo"), + ("file:///C:/far/space%20%3Fboo", "c:\\far\\space ?boo"), + ], +) def test_win_to_fs_path(uri, path): assert uris.to_fs_path(uri) == path @unix_only -@pytest.mark.parametrize('path,uri', [ - ('/foo/bar', 'file:///foo/bar'), - ('/foo/space ?bar', 'file:///foo/space%20%3Fbar'), -]) +@pytest.mark.parametrize( + "path,uri", + [ + ("/foo/bar", "file:///foo/bar"), + ("/foo/space ?bar", "file:///foo/space%20%3Fbar"), + ], +) def test_from_fs_path(path, uri): assert uris.from_fs_path(path) == uri @windows_only -@pytest.mark.parametrize('path,uri', [ - ('c:\\far\\boo', 'file:///c:/far/boo'), - ('C:\\far\\space ?boo', 'file:///c:/far/space%20%3Fboo') -]) +@pytest.mark.parametrize( + "path,uri", + [ + ("c:\\far\\boo", "file:///c:/far/boo"), + ("C:\\far\\space ?boo", "file:///c:/far/space%20%3Fboo"), + ], +) def test_win_from_fs_path(path, uri): assert uris.from_fs_path(path) == uri -@pytest.mark.parametrize('uri,kwargs,new_uri', [ - ('file:///foo/bar', {'path': '/baz/boo'}, 'file:///baz/boo'), - ('file:///D:/hello%20world.py', {'path': 'D:/hello universe.py'}, 'file:///d:/hello%20universe.py') -]) +@pytest.mark.parametrize( + "uri,kwargs,new_uri", + [ + ("file:///foo/bar", {"path": "/baz/boo"}, "file:///baz/boo"), + ( + "file:///D:/hello%20world.py", + {"path": "D:/hello universe.py"}, + "file:///d:/hello%20universe.py", + ), + ], +) def test_uri_with(uri, kwargs, new_uri): assert uris.uri_with(uri, **kwargs) == new_uri diff --git a/test/test_utils.py b/test/test_utils.py index 4b41155b..50e3ca8d 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -38,7 +38,7 @@ def test_debounce_keyed_by(): interval = 0.1 obj = mock.Mock() - @_utils.debounce(0.1, keyed_by='key') + @_utils.debounce(0.1, keyed_by="key") def call_m(key): obj(key) @@ -50,11 +50,14 @@ def call_m(key): assert not obj.mock_calls time.sleep(interval * 2) - obj.assert_has_calls([ - mock.call(1), - mock.call(2), - mock.call(3), - ], any_order=True) + obj.assert_has_calls( + [ + mock.call(1), + mock.call(2), + mock.call(3), + ], + any_order=True, + ) assert len(obj.mock_calls) == 3 call_m(1) @@ -74,23 +77,25 @@ def test_find_parents(tmpdir): path = subsubdir.ensure("path.py") test_cfg = tmpdir.ensure("test.cfg") - assert _utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [test_cfg.strpath] + assert _utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [ + test_cfg.strpath + ] def test_merge_dicts(): assert _utils.merge_dicts( - {'a': True, 'b': {'x': 123, 'y': {'hello': 'world'}}}, - {'a': False, 'b': {'y': [], 'z': 987}} - ) == {'a': False, 'b': {'x': 123, 'y': [], 'z': 987}} + {"a": True, "b": {"x": 123, "y": {"hello": "world"}}}, + {"a": False, "b": {"y": [], "z": 987}}, + ) == {"a": False, "b": {"x": 123, "y": [], "z": 987}} def test_clip_column(): assert _utils.clip_column(0, [], 0) == 0 - assert _utils.clip_column(2, ['123'], 0) == 2 - assert _utils.clip_column(3, ['123'], 0) == 3 - assert _utils.clip_column(5, ['123'], 0) == 3 - assert _utils.clip_column(0, ['\n', '123'], 0) == 0 - assert _utils.clip_column(1, ['\n', '123'], 0) == 0 - assert _utils.clip_column(2, ['123\n', '123'], 0) == 2 - assert _utils.clip_column(3, ['123\n', '123'], 0) == 3 - assert _utils.clip_column(4, ['123\n', '123'], 1) == 3 + assert _utils.clip_column(2, ["123"], 0) == 2 + assert _utils.clip_column(3, ["123"], 0) == 3 + assert _utils.clip_column(5, ["123"], 0) == 3 + assert _utils.clip_column(0, ["\n", "123"], 0) == 0 + assert _utils.clip_column(1, ["\n", "123"], 0) == 0 + assert _utils.clip_column(2, ["123\n", "123"], 0) == 2 + assert _utils.clip_column(3, ["123\n", "123"], 0) == 3 + assert _utils.clip_column(4, ["123\n", "123"], 1) == 3 diff --git a/test/test_workspace.py b/test/test_workspace.py index 88b16507..363ee976 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -14,58 +14,60 @@ def path_as_uri(path): def test_local(pylsp): - """ Since the workspace points to the test directory """ + """Since the workspace points to the test directory""" assert pylsp.workspace.is_local() def test_put_document(pylsp): - pylsp.workspace.put_document(DOC_URI, 'content') + pylsp.workspace.put_document(DOC_URI, "content") assert DOC_URI in pylsp.workspace._docs def test_put_notebook_document(pylsp): - pylsp.workspace.put_notebook_document(DOC_URI, 'jupyter-notebook', []) + pylsp.workspace.put_notebook_document(DOC_URI, "jupyter-notebook", []) assert DOC_URI in pylsp.workspace._docs def test_put_cell_document(pylsp): - pylsp.workspace.put_cell_document(DOC_URI, 'python', 'content') + pylsp.workspace.put_cell_document(DOC_URI, "python", "content") assert DOC_URI in pylsp.workspace._docs def test_get_document(pylsp): - pylsp.workspace.put_document(DOC_URI, 'TEXT') - assert pylsp.workspace.get_document(DOC_URI).source == 'TEXT' + pylsp.workspace.put_document(DOC_URI, "TEXT") + assert pylsp.workspace.get_document(DOC_URI).source == "TEXT" def test_get_missing_document(tmpdir, pylsp): - source = 'TEXT' + source = "TEXT" doc_path = tmpdir.join("test_document.py") doc_path.write(source) doc_uri = uris.from_fs_path(str(doc_path)) - assert pylsp.workspace.get_document(doc_uri).source == 'TEXT' + assert pylsp.workspace.get_document(doc_uri).source == "TEXT" def test_rm_document(pylsp): - pylsp.workspace.put_document(DOC_URI, 'TEXT') - assert pylsp.workspace.get_document(DOC_URI).source == 'TEXT' + pylsp.workspace.put_document(DOC_URI, "TEXT") + assert pylsp.workspace.get_document(DOC_URI).source == "TEXT" pylsp.workspace.rm_document(DOC_URI) assert pylsp.workspace.get_document(DOC_URI)._source is None -@pytest.mark.parametrize('metafiles', [('setup.py',), ('pyproject.toml',), ('setup.py', 'pyproject.toml')]) +@pytest.mark.parametrize( + "metafiles", [("setup.py",), ("pyproject.toml",), ("setup.py", "pyproject.toml")] +) def test_non_root_project(pylsp, metafiles): - repo_root = os.path.join(pylsp.workspace.root_path, 'repo-root') + repo_root = os.path.join(pylsp.workspace.root_path, "repo-root") os.mkdir(repo_root) - project_root = os.path.join(repo_root, 'project-root') + project_root = os.path.join(repo_root, "project-root") os.mkdir(project_root) for metafile in metafiles: - with open(os.path.join(project_root, metafile), 'w+', encoding='utf-8') as f: - f.write('# ' + metafile) + with open(os.path.join(project_root, metafile), "w+", encoding="utf-8") as f: + f.write("# " + metafile) - test_uri = uris.from_fs_path(os.path.join(project_root, 'hello/test.py')) - pylsp.workspace.put_document(test_uri, 'assert True') + test_uri = uris.from_fs_path(os.path.join(project_root, "hello/test.py")) + pylsp.workspace.put_document(test_uri, "assert True") test_doc = pylsp.workspace.get_document(test_uri) assert project_root in test_doc.sys_path() @@ -73,8 +75,8 @@ def test_non_root_project(pylsp, metafiles): def test_root_project_with_no_setup_py(pylsp): """Default to workspace root.""" workspace_root = pylsp.workspace.root_path - test_uri = uris.from_fs_path(os.path.join(workspace_root, 'hello/test.py')) - pylsp.workspace.put_document(test_uri, 'assert True') + test_uri = uris.from_fs_path(os.path.join(workspace_root, "hello/test.py")) + pylsp.workspace.put_document(test_uri, "assert True") test_doc = pylsp.workspace.get_document(test_uri) assert workspace_root in test_doc.sys_path() @@ -92,146 +94,132 @@ def test_multiple_workspaces_from_initialize(pylsp_w_workspace_folders): 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' - } + 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 + 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' - } + 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 + 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') - file1 = workspace1_dir.join('file1.py') - file2 = workspace2_dir.join('file1.py') - file1.write('import os') - file2.write('import sys') - - msg = { - 'uri': path_as_uri(str(file1)), - 'version': 1, - 'text': 'import os' - } + workspace1_dir = tmpdir.mkdir("workspace1") + workspace2_dir = tmpdir.mkdir("workspace2") + file1 = workspace1_dir.join("file1.py") + file2 = workspace2_dir.join("file1.py") + file1.write("import os") + file2.write("import sys") + + msg = {"uri": path_as_uri(str(file1)), "version": 1, "text": "import os"} pylsp.m_text_document__did_open(textDocument=msg) - assert msg['uri'] in pylsp.workspace._docs + assert msg["uri"] in pylsp.workspace._docs - added_workspaces = [{'uri': path_as_uri(str(x))} - for x in (workspace1_dir, workspace2_dir)] - event = {'added': added_workspaces, 'removed': []} + added_workspaces = [ + {"uri": path_as_uri(str(x))} for x in (workspace1_dir, workspace2_dir) + ] + event = {"added": added_workspaces, "removed": []} pylsp.m_workspace__did_change_workspace_folders(event) for workspace in added_workspaces: - assert workspace['uri'] in pylsp.workspaces + assert workspace["uri"] in pylsp.workspaces - workspace1_uri = added_workspaces[0]['uri'] - assert msg['uri'] not in pylsp.workspace._docs - assert msg['uri'] in pylsp.workspaces[workspace1_uri]._docs + workspace1_uri = added_workspaces[0]["uri"] + assert msg["uri"] not in pylsp.workspace._docs + assert msg["uri"] in pylsp.workspaces[workspace1_uri]._docs - msg = { - 'uri': path_as_uri(str(file2)), - 'version': 1, - 'text': 'import sys' - } + msg = {"uri": path_as_uri(str(file2)), "version": 1, "text": "import sys"} pylsp.m_text_document__did_open(textDocument=msg) - workspace2_uri = added_workspaces[1]['uri'] - assert msg['uri'] in pylsp.workspaces[workspace2_uri]._docs + workspace2_uri = added_workspaces[1]["uri"] + assert msg["uri"] in pylsp.workspaces[workspace2_uri]._docs - event = {'added': [], 'removed': [added_workspaces[0]]} + event = {"added": [], "removed": [added_workspaces[0]]} pylsp.m_workspace__did_change_workspace_folders(event) assert workspace1_uri not in pylsp.workspaces def test_multiple_workspaces_wrong_removed_uri(pylsp, tmpdir): - workspace = {'uri': str(tmpdir.mkdir('Test123'))} - event = {'added': [], 'removed': [workspace]} + workspace = {"uri": str(tmpdir.mkdir("Test123"))} + event = {"added": [], "removed": [workspace]} pylsp.m_workspace__did_change_workspace_folders(event) - assert workspace['uri'] not in pylsp.workspaces + assert workspace["uri"] not in pylsp.workspaces def test_root_workspace_changed(pylsp, tmpdir): - test_uri = str(tmpdir.mkdir('Test123')) + test_uri = str(tmpdir.mkdir("Test123")) pylsp.root_uri = test_uri pylsp.workspace._root_uri = test_uri - workspace1 = {'uri': test_uri} - workspace2 = {'uri': str(tmpdir.mkdir('NewTest456'))} + workspace1 = {"uri": test_uri} + workspace2 = {"uri": str(tmpdir.mkdir("NewTest456"))} - event = {'added': [workspace2], 'removed': [workspace1]} + event = {"added": [workspace2], "removed": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) - assert workspace2['uri'] == pylsp.workspace._root_uri - assert workspace2['uri'] == pylsp.root_uri + assert workspace2["uri"] == pylsp.workspace._root_uri + assert workspace2["uri"] == pylsp.root_uri def test_root_workspace_not_changed(pylsp, tmpdir): # removed uri != root_uri - test_uri_1 = str(tmpdir.mkdir('Test12')) + test_uri_1 = str(tmpdir.mkdir("Test12")) pylsp.root_uri = test_uri_1 pylsp.workspace._root_uri = test_uri_1 - workspace1 = {'uri': str(tmpdir.mkdir('Test1234'))} - workspace2 = {'uri': str(tmpdir.mkdir('NewTest456'))} - event = {'added': [workspace2], 'removed': [workspace1]} + workspace1 = {"uri": str(tmpdir.mkdir("Test1234"))} + workspace2 = {"uri": str(tmpdir.mkdir("NewTest456"))} + event = {"added": [workspace2], "removed": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) assert test_uri_1 == pylsp.workspace._root_uri assert test_uri_1 == pylsp.root_uri # empty 'added' list - test_uri_2 = str(tmpdir.mkdir('Test123')) - new_root_uri = workspace2['uri'] + test_uri_2 = str(tmpdir.mkdir("Test123")) + new_root_uri = workspace2["uri"] pylsp.root_uri = test_uri_2 pylsp.workspace._root_uri = test_uri_2 - workspace1 = {'uri': test_uri_2} - event = {'added': [], 'removed': [workspace1]} + workspace1 = {"uri": test_uri_2} + event = {"added": [], "removed": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) assert new_root_uri == pylsp.workspace._root_uri assert new_root_uri == pylsp.root_uri # empty 'removed' list - event = {'added': [workspace1], 'removed': []} + event = {"added": [workspace1], "removed": []} pylsp.m_workspace__did_change_workspace_folders(event) assert new_root_uri == pylsp.workspace._root_uri assert new_root_uri == pylsp.root_uri # 'added' list has no 'uri' - workspace2 = {'TESTuri': 'Test1234'} - event = {'added': [workspace2], 'removed': [workspace1]} + workspace2 = {"TESTuri": "Test1234"} + event = {"added": [workspace2], "removed": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) assert new_root_uri == pylsp.workspace._root_uri assert new_root_uri == pylsp.root_uri def test_root_workspace_removed(tmpdir, pylsp): - workspace1_dir = tmpdir.mkdir('workspace1') - workspace2_dir = tmpdir.mkdir('workspace2') + workspace1_dir = tmpdir.mkdir("workspace1") + workspace2_dir = tmpdir.mkdir("workspace2") root_uri = pylsp.root_uri # Add workspaces to the pylsp - added_workspaces = [{'uri': path_as_uri(str(x))} - for x in (workspace1_dir, workspace2_dir)] - event = {'added': added_workspaces, 'removed': []} + added_workspaces = [ + {"uri": path_as_uri(str(x))} for x in (workspace1_dir, workspace2_dir) + ] + event = {"added": added_workspaces, "removed": []} pylsp.m_workspace__did_change_workspace_folders(event) # Remove the root workspace - removed_workspaces = [{'uri': root_uri}] - event = {'added': [], 'removed': removed_workspaces} + removed_workspaces = [{"uri": root_uri}] + event = {"added": [], "removed": removed_workspaces} pylsp.m_workspace__did_change_workspace_folders(event) # Assert that the first of the workspaces (in alphabetical order) is now @@ -240,73 +228,71 @@ def test_root_workspace_removed(tmpdir, pylsp): assert pylsp.workspace._root_uri == path_as_uri(str(workspace1_dir)) -@pytest.mark.skipif(os.name == 'nt', reason="Fails on Windows") +@pytest.mark.skipif(os.name == "nt", reason="Fails on Windows") def test_workspace_loads_pycodestyle_config(pylsp, tmpdir): - workspace1_dir = tmpdir.mkdir('Test123') + workspace1_dir = tmpdir.mkdir("Test123") pylsp.root_uri = str(workspace1_dir) pylsp.workspace._root_uri = str(workspace1_dir) # Test that project settings are loaded - workspace2_dir = tmpdir.mkdir('NewTest456') + workspace2_dir = tmpdir.mkdir("NewTest456") cfg = workspace2_dir.join("pycodestyle.cfg") - cfg.write( - "[pycodestyle]\n" - "max-line-length = 1000" - ) + # pylint: disable=implicit-str-concat + cfg.write("[pycodestyle]\n" "max-line-length = 1000") - workspace1 = {'uri': str(workspace1_dir)} - workspace2 = {'uri': str(workspace2_dir)} + workspace1 = {"uri": str(workspace1_dir)} + workspace2 = {"uri": str(workspace2_dir)} - event = {'added': [workspace2], 'removed': [workspace1]} + event = {"added": [workspace2], "removed": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) seetings = pylsp.workspaces[str(workspace2_dir)]._config.settings() - assert seetings['plugins']['pycodestyle']['maxLineLength'] == 1000 + assert seetings["plugins"]["pycodestyle"]["maxLineLength"] == 1000 # Test that project settings prevail over server ones. - server_settings = {'pylsp': {'plugins': {'pycodestyle': {'maxLineLength': 10}}}} + server_settings = {"pylsp": {"plugins": {"pycodestyle": {"maxLineLength": 10}}}} pylsp.m_workspace__did_change_configuration(server_settings) - assert seetings['plugins']['pycodestyle']['maxLineLength'] == 1000 + assert seetings["plugins"]["pycodestyle"]["maxLineLength"] == 1000 # Test switching to another workspace with different settings - workspace3_dir = tmpdir.mkdir('NewTest789') + workspace3_dir = tmpdir.mkdir("NewTest789") cfg1 = workspace3_dir.join("pycodestyle.cfg") - cfg1.write( - "[pycodestyle]\n" - "max-line-length = 20" - ) + # pylint: disable=implicit-str-concat + cfg1.write("[pycodestyle]\n" "max-line-length = 20") - workspace3 = {'uri': str(workspace3_dir)} + workspace3 = {"uri": str(workspace3_dir)} - event = {'added': [workspace3], 'removed': [workspace2]} + event = {"added": [workspace3], "removed": [workspace2]} pylsp.m_workspace__did_change_workspace_folders(event) seetings = pylsp.workspaces[str(workspace3_dir)]._config.settings() - assert seetings['plugins']['pycodestyle']['maxLineLength'] == 20 + assert seetings["plugins"]["pycodestyle"]["maxLineLength"] == 20 def test_settings_of_added_workspace(pylsp, tmpdir): - test_uri = str(tmpdir.mkdir('Test123')) + test_uri = str(tmpdir.mkdir("Test123")) pylsp.root_uri = test_uri pylsp.workspace._root_uri = test_uri # Set some settings for the server. - server_settings = {'pylsp': {'plugins': {'jedi': {'environment': '/usr/bin/python3'}}}} + server_settings = { + "pylsp": {"plugins": {"jedi": {"environment": "/usr/bin/python3"}}} + } pylsp.m_workspace__did_change_configuration(server_settings) # Create a new workspace. - workspace1 = {'uri': str(tmpdir.mkdir('NewTest456'))} - event = {'added': [workspace1]} + workspace1 = {"uri": str(tmpdir.mkdir("NewTest456"))} + event = {"added": [workspace1]} pylsp.m_workspace__did_change_workspace_folders(event) # Assert settings are inherited from the server config. - workspace1_object = pylsp.workspaces[workspace1['uri']] - workspace1_jedi_settings = workspace1_object._config.plugin_settings('jedi') - assert workspace1_jedi_settings == server_settings['pylsp']['plugins']['jedi'] + workspace1_object = pylsp.workspaces[workspace1["uri"]] + workspace1_jedi_settings = workspace1_object._config.plugin_settings("jedi") + assert workspace1_jedi_settings == server_settings["pylsp"]["plugins"]["jedi"] def test_no_progress_without_capability(workspace, consumer): - workspace._config.capabilities['window'] = {"workDoneProgress": False} + workspace._config.capabilities["window"] = {"workDoneProgress": False} with workspace.report_progress("some_title"): pass @@ -315,20 +301,28 @@ def test_no_progress_without_capability(workspace, consumer): def test_progress_simple(workspace, consumer): - workspace._config.capabilities['window'] = {"workDoneProgress": True} + workspace._config.capabilities["window"] = {"workDoneProgress": True} with workspace.report_progress("some_title"): pass init_call, *progress_calls = consumer.call_args_list - assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + assert init_call[0][0]["method"] == "window/workDoneProgress/create" # same method for all calls - assert all(call[0][0]["method"] == "$/progress" for call in progress_calls), consumer.call_args_list + assert all( + call[0][0]["method"] == "$/progress" for call in progress_calls + ), consumer.call_args_list # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 + assert ( + len( + {call[0][0]["params"]["token"] for call in progress_calls} + | {init_call[0][0]["params"]["token"]} + ) + == 1 + ) assert [call[0][0]["params"]["value"] for call in progress_calls] == [ {"kind": "begin", "title": "some_title"}, @@ -337,12 +331,17 @@ def test_progress_simple(workspace, consumer): @pytest.mark.parametrize("exc", [Exception("something"), TimeoutError()]) -def test_progress_initialization_fails_but_is_skipped(workspace, consumer, endpoint, exc): +def test_progress_initialization_fails_but_is_skipped( + workspace, consumer, endpoint, exc +): def failing_token_initialization(self, *_args, **_kwargs): raise exc - endpoint._dispatcher.m_window__work_done_progress__create = failing_token_initialization - workspace._config.capabilities['window'] = {"workDoneProgress": True} + endpoint._dispatcher.m_window__work_done_progress__create = ( + failing_token_initialization + ) + + workspace._config.capabilities["window"] = {"workDoneProgress": True} with workspace.report_progress("some_title", skip_token_initialization=True): pass @@ -358,7 +357,7 @@ def failing_token_initialization(self, *_args, **_kwargs): def test_progress_with_percent(workspace, consumer): - workspace._config.capabilities['window'] = {"workDoneProgress": True} + workspace._config.capabilities["window"] = {"workDoneProgress": True} with workspace.report_progress( "some_title", "initial message", percentage=1 @@ -369,13 +368,19 @@ def test_progress_with_percent(workspace, consumer): init_call, *progress_calls = consumer.call_args_list - assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + assert init_call[0][0]["method"] == "window/workDoneProgress/create" # same method for all progress calls assert all(call[0][0]["method"] == "$/progress" for call in progress_calls) # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 + assert ( + len( + {call[0][0]["params"]["token"] for call in progress_calls} + | {init_call[0][0]["params"]["token"]} + ) + == 1 + ) assert [call[0][0]["params"]["value"] for call in progress_calls] == [ { @@ -392,7 +397,7 @@ def test_progress_with_percent(workspace, consumer): def test_progress_with_exception(workspace, consumer): - workspace._config.capabilities['window'] = {"workDoneProgress": True} + workspace._config.capabilities["window"] = {"workDoneProgress": True} class DummyError(Exception): pass @@ -408,13 +413,19 @@ class DummyError(Exception): pass init_call, *progress_calls = consumer.call_args_list - assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + assert init_call[0][0]["method"] == "window/workDoneProgress/create" # same method for all calls assert all(call[0][0]["method"] == "$/progress" for call in progress_calls) # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 + assert ( + len( + {call[0][0]["params"]["token"] for call in progress_calls} + | {init_call[0][0]["params"]["token"]} + ) + == 1 + ) assert [call[0][0]["params"]["value"] for call in progress_calls] == [ {"kind": "begin", "title": "some_title"},