Repository: python-lsp/python-lsp-server

Issue:
WorkDoneProgress tokens not initialized properly by the server
1.7.0 introduced progress reporting support but this doesn't work in Sublime Text for me and I'm pretty sure that's because it's not implemented correctly.

The `$/progress` notifications are being sent with random `token` that hasn't been created beforehand using the `window/workDoneProgress/create` request.

As far as my understanding of the spec goes, the `token` used in `$/progress` notification has to be first explicitly created by the server. Refer to https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initiatingWorkDoneProgress and https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverInitiatedProgress.

Discussion:
ccordoba12: @rchl, could you take a look at this one? Thanks! 
rchl: I can't mark it because it's not touched in this PR but this code in `workspace.py`:

```py
        if message:
            value["message"] = message
        if percentage:
            value["percentage"] = percentage
```

should check `if-check` against `None` since an empty message and a percentage value of 0 should be allowed.
syphar: short update here: 
when testing I discovered that after the first reported progresses the token-initialization starts to timeout, and other LSP requests too. 

I'm not sure (yet) if this is because of my editor or something else in my change. 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 167ce58..637b911 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
 # History of changes
 
+## Version 1.7.1 (2023/01/17)
+
+### Issues Closed
+
+* [Issue 332](https://github.com/python-lsp/python-lsp-server/issues/332) - Failed to load hook pylsp_lint: too many values to unpack (expected 3) ([PR 329](https://github.com/python-lsp/python-lsp-server/pull/329) by [@ccordoba12](https://github.com/ccordoba12))
+
+In this release 1 issue was closed.
+
+### Pull Requests Merged
+
+* [PR 338](https://github.com/python-lsp/python-lsp-server/pull/338) - Use shlex.split() to split pylint flags, by [@hfrentzel](https://github.com/hfrentzel)
+* [PR 337](https://github.com/python-lsp/python-lsp-server/pull/337) - Improve Jedi file completions for directories, by [@ccordoba12](https://github.com/ccordoba12)
+* [PR 334](https://github.com/python-lsp/python-lsp-server/pull/334) - Include missing Pylint "information" category, by [@juliangilbey](https://github.com/juliangilbey)
+* [PR 333](https://github.com/python-lsp/python-lsp-server/pull/333) - Add top constraint to Pylint and fix constraint for `whatthepatch`, by [@ccordoba12](https://github.com/ccordoba12)
+* [PR 329](https://github.com/python-lsp/python-lsp-server/pull/329) - Fix pydocstyle linting with its 6.2.0 version, by [@ccordoba12](https://github.com/ccordoba12) ([332](https://github.com/python-lsp/python-lsp-server/issues/332))
+* [PR 327](https://github.com/python-lsp/python-lsp-server/pull/327) - Use `sys.executable` instead of `python` in Pylint plugin, by [@bnavigator](https://github.com/bnavigator)
+
+In this release 6 pull requests were closed.
+
+----
+
 ## Version 1.7.0 (2022/12/29)
 
 ### New features
diff --git a/pylsp/_utils.py b/pylsp/_utils.py
index 2c6111d..610cdbd 100644
--- a/pylsp/_utils.py
+++ b/pylsp/_utils.py
@@ -293,8 +293,7 @@ else:
             os.kill(pid, 0)
         except OSError as e:
             return e.errno == errno.EPERM
-        else:
-            return True
+        return True
 
 
 def get_eol_chars(text):
diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py
index 44f45dc..50be2c3 100644
--- a/pylsp/plugins/autopep8_format.py
+++ b/pylsp/plugins/autopep8_format.py
@@ -23,17 +23,16 @@ def pylsp_format_document(config, workspace, document, options):  # pylint: disa
 def pylsp_format_range(
     config, workspace, document, range, options
 ):  # pylint: disable=redefined-builtin,unused-argument
-    with workspace.report_progress("format_range: autopep8"):
-        log.info("Formatting document %s in range %s with autopep8", document, 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
+    # First we 'round' the range up/down to full lines only
+    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)
-        return _format(config, document, line_range=line_range)
+    # Add 1 for 1-indexing vs LSP's 0-indexing
+    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):
diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py
index b2110af..fe06ab3 100644
--- a/pylsp/plugins/definition.py
+++ b/pylsp/plugins/definition.py
@@ -8,26 +8,25 @@ log = logging.getLogger(__name__)
 
 
 @hookimpl
-def pylsp_definitions(config, workspace, document, position):
-    with workspace.report_progress("go to definitions"):
-        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)
+def pylsp_definitions(config, document, position):
+    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_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)},
-                }
+    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)},
             }
-            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):
diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py
index 4c4bc94..90b4c19 100644
--- a/pylsp/plugins/jedi_completion.py
+++ b/pylsp/plugins/jedi_completion.py
@@ -2,7 +2,7 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
-import os.path as osp
+import os
 
 import parso
 
@@ -219,10 +219,20 @@ def _format_completion(d, markup_kind: str, include_params=True, resolve=False,
     if resolve:
         completion = _resolve_completion(completion, d, markup_kind)
 
+    # Adjustments for file completions
     if d.type == 'path':
-        path = osp.normpath(d.name)
+        path = os.path.normpath(d.name)
         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 + '\\\\'
+            else:
+                path = path + '\\/'
+
         completion['insertText'] = path
 
     if include_params and not is_exception_class(d.name):
diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py
index 64949bb..d44b28e 100644
--- a/pylsp/plugins/jedi_rename.py
+++ b/pylsp/plugins/jedi_rename.py
@@ -9,44 +9,42 @@ log = logging.getLogger(__name__)
 
 
 @hookimpl
-def pylsp_rename(config, workspace, document, position, new_name):  # pylint: disable=unused-argument,too-many-locals
-    with workspace.report_progress("rename", percentage=0) as report_progress:
-        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
-        report_progress("refactoring")
-        try:
-            refactoring = document.jedi_script().rename(**kwargs)
-        except NotImplementedError as exc:
-            raise Exception('No support for renaming in Python 2/3.5 with Jedi. '
-                            'Consider using the rope_rename plugin instead') from exc
-        log.debug('Finished rename: %s', refactoring.get_diff())
-        changes = []
-
-        changed_files = refactoring.get_changed_files()
-        for n, (file_path, changed_file) in enumerate(changed_files.items()):
-            report_progress(changed_file, percentage=n/len(changed_files)*100)
-            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,
-                            },
+def pylsp_rename(config, workspace, document, position, new_name):  # pylint: disable=unused-argument
+    log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name)
+    kwargs = _utils.position_to_jedi_linecolumn(document, position)
+    kwargs['new_name'] = new_name
+    try:
+        refactoring = document.jedi_script().rename(**kwargs)
+    except NotImplementedError as exc:
+        # 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())
+    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,
                         },
-                        'newText': changed_file.get_new_code(),
-                    }
-                ],
-            })
-        return {'documentChanges': changes}
+                    },
+                    'newText': changed_file.get_new_code(),
+                }
+            ],
+        })
+    return {'documentChanges': changes}
 
 
 def _num_lines(file_contents):
diff --git a/pylsp/plugins/pydocstyle_lint.py b/pylsp/plugins/pydocstyle_lint.py
index 7f4e072..2e34ccc 100644
--- a/pylsp/plugins/pydocstyle_lint.py
+++ b/pylsp/plugins/pydocstyle_lint.py
@@ -28,6 +28,7 @@ def pylsp_settings():
 
 @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)
         log.debug("Got pydocstyle settings: %s", settings)
@@ -66,9 +67,19 @@ def pylsp_lint(config, workspace, document):
 
         # Will only yield a single filename, the document path
         diags = []
-        for filename, checked_codes, ignore_decorators in conf.get_files_to_check():
+        for (
+            filename,
+            checked_codes,
+            ignore_decorators,
+            property_decorators,
+            ignore_self_only_init,
+        ) in conf.get_files_to_check():
             errors = pydocstyle.checker.ConventionChecker().check_source(
-                document.source, filename, ignore_decorators=ignore_decorators
+                document.source,
+                filename,
+                ignore_decorators=ignore_decorators,
+                property_decorators=property_decorators,
+                ignore_self_only_init=ignore_self_only_init,
             )
 
             try:
diff --git a/pylsp/plugins/pylint_lint.py b/pylsp/plugins/pylint_lint.py
index 222cdb8..76e990c 100644
--- a/pylsp/plugins/pylint_lint.py
+++ b/pylsp/plugins/pylint_lint.py
@@ -9,6 +9,7 @@ import sys
 import re
 from subprocess import Popen, PIPE
 import os
+import shlex
 
 from pylsp import hookimpl, lsp
 
@@ -91,7 +92,7 @@ class PylintLinter:
             '-f',
             'json',
             document.path
-        ] + (str(flags).split(' ') if flags else [])
+        ] + (shlex.split(str(flags)) if flags else [])
         log.debug("Calling pylint with '%s'", ' '.join(cmd))
 
         with Popen(cmd, stdout=PIPE, stderr=PIPE,
@@ -126,6 +127,7 @@ class PylintLinter:
         # The type can be any of:
         #
         #  * convention
+        #  * information
         #  * error
         #  * fatal
         #  * refactor
@@ -151,6 +153,8 @@ class PylintLinter:
 
             if diag['type'] == 'convention':
                 severity = lsp.DiagnosticSeverity.Information
+            elif diag['type'] == 'information':
+                severity = lsp.DiagnosticSeverity.Information
             elif diag['type'] == 'error':
                 severity = lsp.DiagnosticSeverity.Error
             elif diag['type'] == 'fatal':
diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py
index 9873d7e..4ef2072 100644
--- a/pylsp/plugins/references.py
+++ b/pylsp/plugins/references.py
@@ -8,20 +8,19 @@ log = logging.getLogger(__name__)
 
 
 @hookimpl
-def pylsp_references(document, workspace, position, exclude_declaration=False):
-    with workspace.report_progress("references"):
-        code_position = _utils.position_to_jedi_linecolumn(document, position)
-        usages = document.jedi_script().get_references(**code_position)
+def pylsp_references(document, position, exclude_declaration=False):
+    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    usages = document.jedi_script().get_references(**code_position)
 
-        if exclude_declaration:
-            # Filter out if the usage is the actual declaration of the thing
-            usages = [d for d in usages if not d.is_definition()]
+    if exclude_declaration:
+        # Filter out if the usage is the actual declaration of the thing
+        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)}
-            }
-        } for d in usages if not d.in_builtin_module()]
+    # 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)}
+        }
+    } for d in usages if not d.in_builtin_module()]
diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py
index eccbbbe..d9ebab5 100644
--- a/pylsp/plugins/rope_rename.py
+++ b/pylsp/plugins/rope_rename.py
@@ -19,42 +19,41 @@ def pylsp_settings():
 
 @hookimpl
 def pylsp_rename(config, workspace, document, position, new_name):
-    with workspace.report_progress("rename"):
-        rope_config = config.settings(document_path=document.path).get('rope', {})
-        rope_project = workspace._rope_project_builder(rope_config)
-
-        rename = Rename(
-            rope_project,
-            libutils.path_to_resource(rope_project, document.path),
-            document.offset_at_position(position)
-        )
-
-        log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name)
-        changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True)
-        log.debug("Finished rename: %s", changeset.changes)
-        changes = []
-        for change in changeset.changes:
-            uri = uris.from_fs_path(change.resource.path)
-            doc = workspace.get_maybe_document(uri)
-            changes.append({
-                'textDocument': {
-                    'uri': uri,
-                    'version': doc.version if doc else None
-                },
-                'edits': [
-                    {
-                        'range': {
-                            'start': {'line': 0, 'character': 0},
-                            'end': {
-                                'line': _num_lines(change.resource),
-                                'character': 0,
-                            },
+    rope_config = config.settings(document_path=document.path).get('rope', {})
+    rope_project = workspace._rope_project_builder(rope_config)
+
+    rename = Rename(
+        rope_project,
+        libutils.path_to_resource(rope_project, document.path),
+        document.offset_at_position(position)
+    )
+
+    log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name)
+    changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True)
+    log.debug("Finished rename: %s", changeset.changes)
+    changes = []
+    for change in changeset.changes:
+        uri = uris.from_fs_path(change.resource.path)
+        doc = workspace.get_maybe_document(uri)
+        changes.append({
+            'textDocument': {
+                'uri': uri,
+                'version': doc.version if doc else None
+            },
+            'edits': [
+                {
+                    'range': {
+                        'start': {'line': 0, 'character': 0},
+                        'end': {
+                            'line': _num_lines(change.resource),
+                            'character': 0,
                         },
-                        'newText': change.new_contents,
-                    }
-                ]
-            })
-        return {'documentChanges': changes}
+                    },
+                    'newText': change.new_contents,
+                }
+            ]
+        })
+    return {'documentChanges': changes}
 
 
 def _num_lines(resource):
diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py
index 754d9cb..308fd5c 100644
--- a/pylsp/plugins/yapf_format.py
+++ b/pylsp/plugins/yapf_format.py
@@ -23,23 +23,22 @@ def pylsp_format_document(workspace, document, options):
 
 
 @hookimpl
-def pylsp_format_range(workspace, document, range, options):  # pylint: disable=redefined-builtin
+def pylsp_format_range(document, range, options):  # pylint: disable=redefined-builtin
     log.info("Formatting document %s in range %s with yapf", document, range)
-    with workspace.report_progress("format_range: yapf"):
-        # First we 'round' the range up/down to full lines only
-        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],
-        #   that we want to format. The lines are 1-based indexed. It can be used by
-        #   third-party code (e.g., IDEs) when reformatting a snippet of code rather
-        #   than a whole file.
-
-        # Add 1 for 1-indexing vs LSP's 0-indexing
-        lines = [(range['start']['line'] + 1, range['end']['line'] + 1)]
-        return _format(document, lines=lines, options=options)
+    # First we 'round' the range up/down to full lines only
+    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],
+    #   that we want to format. The lines are 1-based indexed. It can be used by
+    #   third-party code (e.g., IDEs) when reformatting a snippet of code rather
+    #   than a whole file.
+
+    # Add 1 for 1-indexing vs LSP's 0-indexing
+    lines = [(range['start']['line'] + 1, range['end']['line'] + 1)]
+    return _format(document, lines=lines, options=options)
 
 
 def get_style_config(document_path, options=None):
diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py
index e663a8a..43f886c 100644
--- a/pylsp/python_lsp.py
+++ b/pylsp/python_lsp.py
@@ -152,7 +152,7 @@ class PythonLSPServer(MethodDispatcher):
 
     # pylint: disable=too-many-public-methods,redefined-builtin
 
-    def __init__(self, rx, tx, check_parent_process=False, consumer=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
@@ -172,11 +172,13 @@ class PythonLSPServer(MethodDispatcher):
         else:
             self._jsonrpc_stream_writer = None
 
+        endpoint_cls = endpoint_cls or Endpoint
+
         # if consumer is None, it is assumed that the default streams-based approach is being used
         if consumer is None:
-            self._endpoint = Endpoint(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(self, consumer, max_workers=MAX_WORKERS)
+            self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)
 
         self._dispatchers = []
         self._shutdown = False
@@ -358,7 +360,7 @@ class PythonLSPServer(MethodDispatcher):
         return self._hook('pylsp_execute_command', command=command, arguments=arguments)
 
     def format_document(self, doc_uri, options):
-        return 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)
diff --git a/pylsp/workspace.py b/pylsp/workspace.py
index 5647a90..ea7f55e 100644
--- a/pylsp/workspace.py
+++ b/pylsp/workspace.py
@@ -37,6 +37,7 @@ class Workspace:
 
     M_PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics'
     M_PROGRESS = '$/progress'
+    M_INITIALIZE_PROGRESS = 'window/workDoneProgress/create'
     M_APPLY_EDIT = 'workspace/applyEdit'
     M_SHOW_MESSAGE = 'window/showMessage'
 
@@ -135,15 +136,43 @@ class Workspace:
         message: Optional[str] = None,
         percentage: Optional[int] = None,
     ) -> Generator[Callable[[str, Optional[int]], None], None, None]:
-        token = self._progress_begin(title, message, percentage)
+        if self._config:
+            client_supports_progress_reporting = (
+                self._config.capabilities.get("window", {}).get("workDoneProgress", False)
+            )
+        else:
+            client_supports_progress_reporting = False
+
+        if client_supports_progress_reporting:
+            try:
+                token = self._progress_begin(title, message, percentage)
+            except Exception:  # pylint: disable=broad-exception-caught
+                log.warning(
+                    "There was an error while trying to initialize progress reporting."
+                    "Likely progress reporting was used in a synchronous LSP handler, "
+                    "which is not supported by progress reporting yet.",
+                    exc_info=True
+                )
+
+            else:
+                def progress_message(message: str, percentage: Optional[int] = None) -> None:
+                    self._progress_report(token, message, percentage)
+
+                try:
+                    yield progress_message
+                finally:
+                    self._progress_end(token)
 
-        def progress_message(message: str, percentage: Optional[int] = None) -> None:
-            self._progress_report(token, message, percentage)
+                return
 
-        try:
-            yield progress_message
-        finally:
-            self._progress_end(token)
+        # FALLBACK:
+        # If the client doesn't support progress reporting, or if we failed to
+        # initialize it, we have a dummy method for the caller to use.
+        def dummy_progress_message(message: str, percentage: Optional[int] = None) -> None:
+            # pylint: disable=unused-argument
+            pass
+
+        yield dummy_progress_message
 
     def _progress_begin(
         self,
@@ -152,13 +181,16 @@ class Workspace:
         percentage: Optional[int] = None,
     ) -> str:
         token = str(uuid.uuid4())
+
+        self._endpoint.request(self.M_INITIALIZE_PROGRESS, {'token': token}).result(timeout=1.0)
+
         value = {
             "kind": "begin",
             "title": title,
         }
-        if message:
+        if message is not None:
             value["message"] = message
-        if percentage:
+        if percentage is not None:
             value["percentage"] = percentage
 
         self._endpoint.notify(
diff --git a/pyproject.toml b/pyproject.toml
index 1422313..1dc01fa 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,29 +27,30 @@ Homepage = "https://github.com/python-lsp/python-lsp-server"
 
 [project.optional-dependencies]
 all = [
-    "autopep8>=1.6.0,<1.7.0",
+    "autopep8>=1.6.0,<2.1.0",
     "flake8>=5.0.0,<7",
     "mccabe>=0.7.0,<0.8.0",
     "pycodestyle>=2.9.0,<2.11.0",
-    "pydocstyle>=2.0.0",
+    "pydocstyle>=6.3.0,<6.4.0",
     "pyflakes>=2.5.0,<3.1.0",
-    "pylint>=2.5.0",
+    "pylint>=2.5.0,<3",
     "rope>1.2.0",
-    "yapf",
-    "whatthepatch"
+    "yapf<=0.32.0",
+    "toml",
+    "whatthepatch>=1.0.2,<2.0.0"
 ]
-autopep8 = ["autopep8>=1.6.0,<1.7.0"]
+autopep8 = ["autopep8>=1.6.0,<2.1.0"]
 flake8 = ["flake8>=5.0.0,<7"]
 mccabe = ["mccabe>=0.7.0,<0.8.0"]
 pycodestyle = ["pycodestyle>=2.9.0,<2.11.0"]
-pydocstyle = ["pydocstyle>=2.0.0"]
+pydocstyle = ["pydocstyle>=6.3.0,<6.4.0"]
 pyflakes = ["pyflakes>=2.5.0,<3.1.0"]
-pylint = ["pylint>=2.5.0"]
+pylint = ["pylint>=2.5.0,<3"]
 rope = ["rope>1.2.0"]
-yapf = ["yapf", "whatthepatch>=1.0.2,<2.0.0"]
+yapf = ["yapf<=0.32.0", "whatthepatch>=1.0.2,<2.0.0", "toml"]
 websockets = ["websockets>=10.3"]
 test = [
-    "pylint>=2.5.0",
+    "pylint>=2.5.0,<3",
     "pytest",
     "pytest-cov",
     "coverage",


Test patch:
diff --git a/test/fixtures.py b/test/fixtures.py
index 5763d46..7c7bcf6 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -4,8 +4,11 @@
 import os
 from io import StringIO
 from unittest.mock import MagicMock
+
 import pytest
+from pylsp_jsonrpc.dispatchers import MethodDispatcher
 from pylsp_jsonrpc.endpoint import Endpoint
+from pylsp_jsonrpc.exceptions import JsonRpcException
 
 from pylsp import uris
 from pylsp.config.config import Config
@@ -21,10 +24,48 @@ def main():
 """
 
 
+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`.
+
+        related spec:
+        https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_workDoneProgress_create
+        """
+        return None
+
+
+class FakePythonLSPServer(FakeEditorMethodsMixin, PythonLSPServer):
+    pass
+
+
+class FakeEndpoint(Endpoint):
+    """
+    Fake Endpoint representing the editor / LSP client.
+
+    The `dispatcher` dict will be used to synchronously calculate the responses
+    for calls to `.request` and resolve the futures with the value or errors.
+
+    Fake methods in the `dispatcher` should raise `JsonRpcException` for any
+    error.
+    """
+    def request(self, method, params=None):
+        request_future = super().request(method, params)
+        try:
+            request_future.set_result(self._dispatcher[method](params))
+        except JsonRpcException as e:
+            request_future.set_exception(e)
+
+        return request_future
+
+
 @pytest.fixture
 def pylsp(tmpdir):
     """ Return an initialized python LS """
-    ls = PythonLSPServer(StringIO, StringIO)
+    ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)
 
     ls.m_initialize(
         processId=1,
@@ -38,7 +79,7 @@ def pylsp(tmpdir):
 @pytest.fixture
 def pylsp_w_workspace_folders(tmpdir):
     """ Return an initialized python LS """
-    ls = PythonLSPServer(StringIO, StringIO)
+    ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)
 
     folder1 = tmpdir.mkdir('folder1')
     folder2 = tmpdir.mkdir('folder2')
@@ -70,7 +111,10 @@ def consumer():
 
 @pytest.fixture()
 def endpoint(consumer):  # pylint: disable=redefined-outer-name
-    return Endpoint({}, consumer, id_generator=lambda: "id")
+    class Dispatcher(FakeEditorMethodsMixin, MethodDispatcher):
+        pass
+
+    return FakeEndpoint(Dispatcher(), consumer, id_generator=lambda: "id")
 
 
 @pytest.fixture
diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py
index 16e278e..fc22c34 100644
--- a/test/plugins/test_completion.py
+++ b/test/plugins/test_completion.py
@@ -528,3 +528,26 @@ mymodule.f"""
     com_position = {'line': 1, 'character': 10}
     completions = pylsp_jedi_completions(doc._config, doc, com_position)
     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')
+
+    # Content of doc to test completion
+    doc_content = '"'
+    doc = Document(DOC_URI, workspace, doc_content)
+
+    # Request for completions
+    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"'
diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py
index a8972cd..4a8855c 100644
--- a/test/plugins/test_definitions.py
+++ b/test/plugins/test_definitions.py
@@ -35,7 +35,7 @@ def test_definitions(config, workspace):
     }
 
     doc = Document(DOC_URI, workspace, DOC)
-    assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, workspace, doc, cursor_pos)
+    assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, doc, cursor_pos)
 
 
 def test_builtin_definition(config, workspace):
@@ -49,14 +49,14 @@ def test_builtin_definition(config, workspace):
     follow_defns_setting = {'follow_builtin_definitions': True}
     settings = {'plugins': {'jedi_definition': follow_defns_setting}}
     config.update(settings)
-    defns = pylsp_definitions(config, workspace, doc, cursor_pos)
+    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
     config.update(settings)
-    defns = pylsp_definitions(config, workspace, doc, cursor_pos)
+    defns = pylsp_definitions(config, doc, cursor_pos)
     assert not defns
 
     config.update(orig_settings)
@@ -73,7 +73,7 @@ def test_assignment(config, workspace):
     }
 
     doc = Document(DOC_URI, workspace, DOC)
-    assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, workspace, 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):
@@ -107,5 +107,5 @@ def foo():
     module_uri = uris.from_fs_path(module_path)
 
     assert [{"uri": module_uri, "range": def_range}] == pylsp_definitions(
-        config, workspace_other_root_path, doc, cursor_pos
+        config, doc, cursor_pos
     )
diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py
index 79cd7a0..c1df037 100644
--- a/test/plugins/test_references.py
+++ b/test/plugins/test_references.py
@@ -40,13 +40,13 @@ def test_references(tmp_workspace):  # pylint: disable=redefined-outer-name
     DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME))
     doc1 = Document(DOC1_URI, tmp_workspace)
 
-    refs = pylsp_references(doc1, tmp_workspace, position)
+    refs = pylsp_references(doc1, position)
 
     # Definition, the import and the instantiation
     assert len(refs) == 3
 
     # Briefly check excluding the definitions (also excludes imports, only counts uses)
-    no_def_refs = pylsp_references(doc1, tmp_workspace, position, exclude_declaration=True)
+    no_def_refs = pylsp_references(doc1, position, exclude_declaration=True)
     assert len(no_def_refs) == 1
 
     # Make sure our definition is correctly located
@@ -70,7 +70,7 @@ def test_references_builtin(tmp_workspace):  # pylint: disable=redefined-outer-n
     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, tmp_workspace, position)
+    refs = pylsp_references(doc2, position)
     assert len(refs) >= 1
 
     expected = {'start': {'line': 4, 'character': 7},
diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py
index 0e989c0..62808e6 100644
--- a/test/plugins/test_yapf_format.py
+++ b/test/plugins/test_yapf_format.py
@@ -41,7 +41,7 @@ def test_range_format(workspace):
         'start': {'line': 0, 'character': 0},
         'end': {'line': 4, 'character': 10}
     }
-    res = pylsp_format_range(workspace, doc, def_range, None)
+    res = pylsp_format_range(doc, def_range, None)
 
     # Make sure B is still badly formatted
     assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n"
diff --git a/test/test_workspace.py b/test/test_workspace.py
index 6699b4b..94a9cba 100644
--- a/test/test_workspace.py
+++ b/test/test_workspace.py
@@ -295,23 +295,56 @@ def test_settings_of_added_workspace(pylsp, tmpdir):
     assert workspace1_jedi_settings == server_settings['pylsp']['plugins']['jedi']
 
 
+def test_no_progress_without_capability(workspace, consumer):
+    workspace._config.capabilities['window'] = {"workDoneProgress": False}
+
+    with workspace.report_progress("some_title"):
+        pass
+
+    assert len(consumer.call_args_list) == 0
+
+
 def test_progress_simple(workspace, consumer):
+    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'
+
     # same method for all calls
-    assert all(call[0][0]["method"] == "$/progress" for call in 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 consumer.call_args_list}) == 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 consumer.call_args_list] == [
+    assert [call[0][0]["params"]["value"] for call in progress_calls] == [
         {"kind": "begin", "title": "some_title"},
         {"kind": "end"},
     ]
 
 
+@pytest.mark.parametrize("exc", [Exception("something"), TimeoutError()])
+def test_progress_initialization_fails(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}
+
+    with workspace.report_progress("some_title"):
+        pass
+
+    # we only see the failing token initialization call, no other calls
+    init_call, = consumer.call_args_list
+    assert init_call[0][0]['method'] == 'window/workDoneProgress/create'
+
+
 def test_progress_with_percent(workspace, consumer):
+    workspace._config.capabilities['window'] = {"workDoneProgress": True}
+
     with workspace.report_progress(
         "some_title", "initial message", percentage=1
     ) as progress_message:
@@ -319,13 +352,17 @@ def test_progress_with_percent(workspace, consumer):
         progress_message("fifty", 50)
         progress_message("ninety", 90)
 
-    # same method for all calls
-    assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
+    init_call, *progress_calls = consumer.call_args_list
+
+    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 consumer.call_args_list}) == 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 consumer.call_args_list] == [
+    assert [call[0][0]["params"]["value"] for call in progress_calls] == [
         {
             "kind": "begin",
             "message": "initial message",
@@ -340,6 +377,8 @@ def test_progress_with_percent(workspace, consumer):
 
 
 def test_progress_with_exception(workspace, consumer):
+    workspace._config.capabilities['window'] = {"workDoneProgress": True}
+
     class DummyError(Exception):
         pass
 
@@ -353,13 +392,16 @@ def test_progress_with_exception(workspace, consumer):
         # test.
         pass
 
+    init_call, *progress_calls = consumer.call_args_list
+    assert init_call[0][0]['method'] == 'window/workDoneProgress/create'
+
     # same method for all calls
-    assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
+    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 consumer.call_args_list}) == 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 consumer.call_args_list] == [
+    assert [call[0][0]["params"]["value"] for call in progress_calls] == [
         {"kind": "begin", "title": "some_title"},
         {"kind": "end"},
     ]