Skip to content

Commit

Permalink
Require a value when calling resolve function of a Promise (#1583)
Browse files Browse the repository at this point in the history
Make the value passed to ResolveFunc non-optional. This allows to
skip some if-checks when handling resolved value from a "fulfill" function.

This also aligns the behavior with a recent change in Typescript [1].

And, for consistency, also make the argument to
Promise.[resolve|on_main_thread|on_async_thread] non-optional.

[1] https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#resolves-parameters-are-no-longer-optional-in-promises
  • Loading branch information
rchl authored Feb 16, 2021
1 parent ce7e116 commit ffef40e
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 27 deletions.
6 changes: 3 additions & 3 deletions plugin/core/open.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ def center_selection(v: Optional[sublime.View]) -> None:


def open_file_and_center_async(window: sublime.Window, file_path: str, r: Optional[RangeLsp], flag: int = 0,
group: int = -1) -> Promise:
group: int = -1) -> Promise[None]:
"""Open a file asynchronously and center the range, worker thread version."""
return Promise.on_main_thread() \
return Promise.on_main_thread(None) \
.then(lambda _: open_file_and_center(window, file_path, r, flag, group)) \
.then(Promise.on_async_thread)
.then(lambda _: Promise.on_async_thread(None))


def open_externally(uri: str, take_focus: bool) -> bool:
Expand Down
32 changes: 16 additions & 16 deletions plugin/core/promise.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
import threading

T = TypeVar('T')
TExecutor = TypeVar('TExecutor')
T_contra = TypeVar('T_contra', contravariant=True)
TResult = TypeVar('TResult')


class ResolveFunc(Protocol[T_contra]):
def __call__(self, value: Union[T_contra, 'Promise[T_contra]'] = None) -> None:
def __call__(self, value: Union[T_contra, 'Promise[T_contra]']) -> None:
...


FullfillFunc = Callable[[T], Union[TResult, 'Promise[TResult]', None]]
FullfillFunc = Callable[[T], Union[TResult, 'Promise[TResult]']]
ExecutorFunc = Callable[[ResolveFunc[T]], None]
PackagedTask = Tuple['Promise[T]', ResolveFunc[T]]

Expand Down Expand Up @@ -64,7 +65,7 @@ def process_value(value):
"""

@classmethod
def resolve(cls, resolve_value: T = None) -> 'Promise[T]':
def resolve(cls, resolve_value: T) -> 'Promise[T]':
"""Immediately resolves a Promise.
Convenience function for creating a Promise that gets immediately
Expand All @@ -79,29 +80,29 @@ def executor_func(resolve_fn: ResolveFunc[T]) -> None:
return cls(executor_func)

@classmethod
def on_main_thread(cls, value: T = None) -> 'Promise[T]':
def on_main_thread(cls, value: T) -> 'Promise[T]':
"""Return a promise that resolves on the main thread."""
return Promise(lambda resolve: sublime.set_timeout(lambda: resolve(value)))

@classmethod
def on_async_thread(cls, value: T = None) -> 'Promise[T]':
def on_async_thread(cls, value: T) -> 'Promise[T]':
"""Return a promise that resolves on the worker thread."""
return Promise(lambda resolve: sublime.set_timeout_async(lambda: resolve(value)))

@classmethod
def packaged_task(cls) -> PackagedTask[T]:

class Executor:
class Executor(Generic[TExecutor]):

__slots__ = ("resolver",)

def __init__(self) -> None:
self.resolver = None # type: Optional[ResolveFunc[T]]
self.resolver = None # type: Optional[ResolveFunc[TExecutor]]

def __call__(self, resolver: ResolveFunc[T]) -> None:
def __call__(self, resolver: ResolveFunc[TExecutor]) -> None:
self.resolver = resolver

executor = Executor()
executor = Executor() # type: Executor[T]
promise = cls(executor)
assert callable(executor.resolver)
return promise, executor.resolver
Expand All @@ -121,14 +122,14 @@ def all(cls, promises: List['Promise[T]']) -> 'Promise[List[T]]':
def executor(resolve: ResolveFunc[List[T]]) -> None:
was_resolved = False

def recheck_resolve_status(_: Optional[T]) -> None:
def recheck_resolve_status(_: T) -> None:
nonlocal was_resolved
# We're being called from a Promise that is holding a lock so don't try to use
# any methods that would try to acquire it.
if not was_resolved and all(p.resolved for p in promises):
was_resolved = True
values = [p.value for p in promises]
resolve(values) # type: ignore
resolve(values)

for p in promises:
assert isinstance(p, Promise)
Expand All @@ -146,7 +147,6 @@ def __init__(self, executor_func: ExecutorFunc[T]) -> None:
It gets passed a "resolve" function. The "resolve" function, when
called, resolves the Promise with the value passed to it.
"""
self.value = None # type: Optional[T]
self.resolved = False
self.mutex = threading.Lock()
self.callbacks = [] # type: List[ResolveFunc[T]]
Expand All @@ -157,7 +157,7 @@ def __repr__(self) -> str:
return 'Promise({})'.format(self.value)
return 'Promise(<pending>)'

def then(self, onfullfilled: FullfillFunc[Optional[T], TResult]) -> 'Promise[TResult]':
def then(self, onfullfilled: FullfillFunc[T, TResult]) -> 'Promise[TResult]':
"""Create a new promise and chain it with this promise.
When this promise gets resolved, the callback will be called with the
Expand All @@ -168,7 +168,7 @@ def then(self, onfullfilled: FullfillFunc[Optional[T], TResult]) -> 'Promise[TRe
Arguments:
onfullfilled: The callback to call when this promise gets resolved.
"""
def callback_wrapper(resolve_fn: ResolveFunc[TResult], resolve_value: Optional[T]) -> None:
def callback_wrapper(resolve_fn: ResolveFunc[TResult], resolve_value: T) -> None:
"""A wrapper called when this promise resolves.
Arguments:
Expand All @@ -179,7 +179,7 @@ def callback_wrapper(resolve_fn: ResolveFunc[TResult], resolve_value: Optional[T
# If returned value is a promise then this promise needs to be
# resolved with the value of returned promise.
if isinstance(result, Promise):
result.then(resolve_fn)
result.then(lambda value: resolve_fn(value))
else:
resolve_fn(result)

Expand Down Expand Up @@ -221,6 +221,6 @@ def _is_resolved(self) -> bool:
with self.mutex:
return self.resolved

def _get_value(self) -> Optional[T]:
def _get_value(self) -> T:
with self.mutex:
return self.value
16 changes: 8 additions & 8 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ def execute_command(self, command: ExecuteCommandParams, progress: bool) -> Prom
if self._plugin:
task = Promise.packaged_task() # type: PackagedTask[None]
promise, resolve = task
if self._plugin.on_pre_server_command(command, resolve):
if self._plugin.on_pre_server_command(command, lambda: resolve(None)):
return promise
# TODO: Our Promise class should be able to handle errors/exceptions
return Promise(
Expand Down Expand Up @@ -1020,15 +1020,15 @@ def _maybe_resolve_code_action(self, code_action: CodeAction) -> Promise[Union[C
return self.send_request_task(request)
return Promise.resolve(code_action)

def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) -> Promise:
def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) -> Promise[None]:
if not code_action:
return Promise.resolve()
return Promise.resolve(None)
if isinstance(code_action, Error):
# TODO: our promise must be able to handle exceptions (or, wait until we can use coroutines)
self.window.status_message("Failed to apply code action: {}".format(code_action))
return Promise.resolve()
return Promise.resolve(None)
edit = code_action.get("edit")
promise = self._apply_workspace_edit_async(edit) if edit else Promise.resolve()
promise = self._apply_workspace_edit_async(edit) if edit else Promise.resolve(None)
command = code_action.get("command")
if isinstance(command, dict):
execute_command = {
Expand All @@ -1038,15 +1038,15 @@ def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None])
return promise.then(lambda _: self.execute_command(execute_command, False))
return promise

def _apply_workspace_edit_async(self, edit: Any) -> Promise:
def _apply_workspace_edit_async(self, edit: Any) -> Promise[None]:
"""
Apply workspace edits, and return a promise that resolves on the async thread again after the edits have been
applied.
"""
changes = parse_workspace_edit(edit)
return Promise.on_main_thread() \
return Promise.on_main_thread(None) \
.then(lambda _: apply_workspace_edit(self.window, changes)) \
.then(Promise.on_async_thread)
.then(lambda _: Promise.on_async_thread(None))

# --- server request handlers --------------------------------------------------------------------------------------

Expand Down

0 comments on commit ffef40e

Please sign in to comment.