Skip to content

Instantly share code, notes, and snippets.

@lulle2007200
Created July 29, 2026 17:49
Show Gist options
  • Select an option

  • Save lulle2007200/54afa7d82f074d45702f5d500e13e88e to your computer and use it in GitHub Desktop.

Select an option

Save lulle2007200/54afa7d82f074d45702f5d500e13e88e to your computer and use it in GitHub Desktop.
import sublime
import sublime_plugin
try:
import sublime_aio
except:
sublime_aio = None
import sys
from contextvars import Context
import inspect
import functools
import os
import threading
import logging
import platform
import heapq
import asyncio
import contextlib
import io
import faulthandler
from contextlib import contextmanager, redirect_stderr, redirect_stdout
from concurrent.futures import ThreadPoolExecutor, Future as ConcurrentFuture
from typing import IO, Callable, ClassVar, Concatenate, Dict, Generator, Generic, Iterable, List, Optional, ParamSpec, Protocol, Sequence, Set, TextIO, Tuple, Type, TypeVar, Coroutine, Any, TypeVarTuple, Unpack, cast, overload
from asyncio import AbstractEventLoop, EventLoop, Handle, ProactorEventLoop, Future as AsyncIoFuture, SelectorEventLoop, TimerHandle, queues
from asyncio.base_events import _MIN_SCHEDULED_TIMER_HANDLES # type: ignore
from asyncio.base_events import _MIN_CANCELLED_TIMER_HANDLES_FRACTION # type: ignore
from asyncio.base_events import MAXIMUM_SELECT_TIMEOUT # type: ignore
T = TypeVar('T')
Ts = TypeVarTuple("Ts")
U = TypeVar('U')
P = ParamSpec('P')
R = TypeVar('R')
Self = TypeVar('Self')
asyncio_logger = logging.getLogger("asyncio")
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if platform.system() != "Windows":
logger.warning("SublimeAsync has only been tested on Windows. Expect dragons.")
__all__ = [
"plugin_loaded",
"get_ui_event_loop",
"get_background_event_loop",
"run_in_executor",
"run_as_task",
"run_as_task_ui",
"run_as_task_background",
"fire_and_forget",
"with_event_loop",
"AsyncWindowCommand",
"AsyncApplicationCommand",
"run_as_text_command",
"run_as_window_command",
"run_as_application_command",
"SublimeAsyncRunAsTextCommandCommand",
"SublimeAsyncRunAsWindowCommandCommand",
"SublimeAsyncRunAsApplicationCommandCommand"
]
_debug_stream: Optional[TextIO] = None
def _enable_debug_logging():
global _debug_stream
try:
_debug_stream = open(r"\\.\pipe\debugpipe", "w", buffering=1, encoding="utf-8")
_debug_handler = logging.StreamHandler(_debug_stream)
_debug_handler.setLevel(logging.DEBUG)
_debug_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
sys.stderr = _debug_stream
sys.stdout = _debug_stream
logging.getLogger().addHandler(_debug_handler)
except:
pass
# _enable_debug_logging()
def _enable_fault_handler():
if _debug_stream is None:
return
def _reset_faulthandler_watchdog():
faulthandler.cancel_dump_traceback_later()
faulthandler.dump_traceback_later(5, repeat=False, file=cast(io.StringIO, _debug_stream))
sublime.set_timeout(_reset_faulthandler_watchdog, 4000)
_reset_faulthandler_watchdog()
"""Event loop implementation that runs on Sublime's UI/main thread."""
class SublimeUiEventLoop(ProactorEventLoop):
def __init__(self) -> None:
super().__init__()
self._event_list = None
self._executor: Optional[ThreadPoolExecutor] = None
self._next: Optional[Callable[[], None]] = None
self._scheduling_stopped = True
self._check_thread_disabled = False
self._executor_thread_id: int
self._select_fut: ConcurrentFuture = ConcurrentFuture()
self._select_fut.set_result(None)
@property
def _is_waiting_for_select(self):
if self._select_fut.done():
return False
return True
@contextmanager
def _disable_thread_check(self):
prev = self._check_thread_disabled
self._check_thread_disabled = True
try:
yield
finally:
self._check_thread_disabled = prev
def _wait_for_wakeup(self) -> None:
print(f"waiting for wakeupg {self._select_fut}")
self._select_fut.exception()
def _wake_up(self) -> None:
# NOTE: The wake up is only actually required when waiting for a select(...) in the executor.
if not self._is_waiting_for_select:
return
# NOTE: Do not wake up, if called by the executor thread.
# When a self write is received, _poll schedules the
# self read future callback via call_soon, which registers
# a new future for the next self write.
# If we issue another self write during call_soon, before the callback
# has executed, there is no future to handle the self write and
# _poll errors, because it received an unexpected IOCP completion for
# the self write.
if threading.get_ident() == self._executor_thread_id:
return
print("waking up")
try:
self._write_to_self() # type: ignore
except:
# NOTE: If _write_to_self is not available,
# wake up by scheduling a lambda function.
self.call_soon_threadsafe(lambda: ())
print("waiting for wakeup")
self._wait_for_wakeup()
def _start_scheduling(self) -> None:
self._scheduling_stopped = False
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="UiLoopSelectThread")
fut = self._executor.submit(threading.get_ident)
self._executor_thread_id = fut.result()
self._schedule_next()
def _stop_scheduling(self) -> None:
# NOTE: Shuts down the executor and prevents further automatic
# scheduling of steps via sublime.set_timeout(...).
# This *MUST* only be called on the UI thread.
self._scheduling_stopped = True
self._wake_up()
if (self._executor is not None):
self._executor.shutdown(True, cancel_futures=True)
self._executor = None
def call_soon(self, callback: Callable[[Unpack[Ts]], object], *args: Unpack[Ts], context: Context | None = None) -> Handle:
self._wake_up()
res = super().call_soon(callback, *args, context=context)
return res
def call_at(self, when: float, callback: Callable[[Unpack[Ts]], object], *args: Unpack[Ts], context: Context | None = None) -> TimerHandle:
self._wake_up()
res = super().call_at(when, callback, *args, context=context)
return res
def _run_next(self) -> None:
if (self._next is not None):
logger.debug(f"Running next step: {self._next}.")
self._next()
def _check_thread(self) -> None:
# NOTE: Skip the thread check if disabled (e.g. while running select on a different thread).
if (not self._check_thread_disabled):
super()._check_thread() # type: ignore
def _run_until_run_forever_complete(self) -> None:
# NOTE: Runs until run_forever completes.
# This *MUST* only be called on the UI thread and the loop *MUST* have
# been signaled to stop or a pending coroutine or callback that stops the loop
# *MUST* have already been scheduled to the loop.
self._stop_scheduling()
while (self._next is not None):
self._run_next()
def _schedule_next(self) -> None:
if (self._next is not None and not self._scheduling_stopped):
sublime.set_timeout(self._run_next, 0)
def _run_once_begin(self) -> None:
# NOTE: This is effectively the first part of BaseEventLoop.run_once.
try:
sched_count = len(self._scheduled)
if (sched_count > _MIN_SCHEDULED_TIMER_HANDLES and
self._timer_cancelled_count / sched_count >
_MIN_CANCELLED_TIMER_HANDLES_FRACTION):
new_scheduled = []
for handle in self._scheduled:
if handle._cancelled:
handle._scheduled = False
else:
new_scheduled.append(handle)
heapq.heapify(new_scheduled)
self._scheduled = new_scheduled
self._timer_cancelled_count = 0
else:
while self._scheduled and self._scheduled[0]._cancelled:
self._timer_cancelled_count -= 1
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
timeout = None
if self._ready or self._stopping: # type: ignore
timeout = 0
elif self._scheduled:
timeout = self._scheduled[0]._when - self.time()
if timeout > MAXIMUM_SELECT_TIMEOUT:
timeout = MAXIMUM_SELECT_TIMEOUT
elif timeout < 0:
timeout = 0
self._select(timeout)
except:
logger.exception("An exception occured in the UI event loop.")
self._run_forever_cleanup() # type: ignore
def _select(self, timeout):
# NOTE: This wraps _selector.select(...).
# select(...) blocks until a coroutine becomes ready to run, or until the timeout expires.
# This is run on a separate thread to not block the UI thread.
# Once it returns, a single iteration of the event loop is scheduled to the UI thread.
def do_select(timeout):
try:
event_list = None
# NOTE: When debugging is enabled, call_soon verifies that the caller
# is on the same thread as the event loop and throws otherwise.
# Disable the check here since we are running select on a different thread.
with self._disable_thread_check():
event_list = self._selector.select(timeout) # type: ignore
self._next = lambda: self._run_once_end(event_list)
except Exception as e:
logger.exception("An exception occured in the UI event loop.")
self._next = self._run_forever_cleanup
self._schedule_next()
if self._select_fut and not self._select_fut.done():
logger.error("Skip scheduling new select, because the previous one has not completed. Future: %s. Traceback:\n", self._select_fut)
faulthandler.dump_traceback(_debug_stream, True)
return
try:
if (self._executor is None or self._scheduling_stopped):
raise RuntimeError("UI event loop executor has been shut down.")
self._select_fut = self._executor.submit(lambda: do_select(timeout))
self._select_fut.add_done_callback(lambda t: print(f"{t} is done"))
logger.debug("Scheduled new select. Future: %s. Traceback:\n", self._select_fut)
faulthandler.dump_traceback(_debug_stream, True)
except Exception as ex:
# NOTE: executor has been shutdown, schedule on the UI thread instead.
# This should only happen during shutdown.
# When running on the UI thread, we cannot block.
# Set the timeout to 0, regardless of what was requested.
self._next = lambda: do_select(0)
self._schedule_next()
def _run_once_end(self, event_list) -> None:
# NOTE: This is the final part of BaseEventLoop.run_once.
try:
self._process_events(event_list) # type: ignore
# Needed to break cycles when an exception occurs.
event_list = None
# Handle 'later' callbacks that are ready.
end_time = self.time() + self._clock_resolution # type: ignore
while self._scheduled:
handle = self._scheduled[0]
if handle._when >= end_time:
break
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
self._ready.append(handle) # type: ignore
ntodo = len(self._ready) # type: ignore
for i in range(ntodo):
handle = self._ready.popleft() # type: ignore
if handle._cancelled:
continue
if self._debug: # type: ignore
try:
self._current_handle = handle
t0 = self.time()
handle._run()
dt = self.time() - t0
if dt >= self.slow_callback_duration:
asyncio_logger.warning('Executing %s took %.3f seconds',
_format_handle(handle), dt) # type: ignore
finally:
self._current_handle = None
else:
handle._run()
handle = None
if not self._stopping: # type: ignore
self._next = self._run_once_begin
else:
self._run_forever_cleanup() # type: ignore
except:
logger.exception("An exception occured in the UI event loop.")
self._run_forever_cleanup() # type: ignore
self._schedule_next()
def _run_forever_cleanup(self):
# NOTE: No more steps to execute.
logger.info("run forever cleanup")
self._next = None
super()._run_forever_cleanup() # type: ignore
def run_forever(self):
self.start_run_forever()
if (self._scheduling_stopped):
# NOTE: If scheduling via sublime.set_timeout is disabled (e.g. during shutdown),
# just run run_forever synchronously.
# This is required for run_until_completion to work properly.
self._run_until_run_forever_complete()
else:
self._schedule_next()
def start_run_forever(self):
self._run_forever_setup() # type: ignore
self._next = self._run_once_begin
class TaskSetBase:
def __init__(self, tag: str = "[unknown]") -> None:
self._tag = tag
def add_impl(self, task: Future) -> bool:
raise NotImplementedError()
def remove_impl(self, task: Future) -> bool:
raise NotImplementedError()
@property
def pending_tasks(self) -> Sequence[Future]:
raise NotImplementedError()
def tasks(self) -> List[Future]:
raise NotImplementedError()
def ensure_empty(self) -> None:
tasks = self.pending_tasks
if len(tasks) > 0:
logger.warning("Task set %s is not empty.", self._tag)
if logger.isEnabledFor(logging.DEBUG):
for t in tasks:
logger.debug("Task %s in task set %s is still pending.", t, self._tag)
raise RuntimeError(f"Task set {self._tag} is not empty.")
def add(self, task: Future) -> None:
res = self.add_impl(task)
if res:
logger.debug(f"Added future %s to task set %s.", task, self._tag)
elif logger.isEnabledFor(logging.WARNING):
logger.warning(f"Failed to add future %s to task set %s.", task, self._tag)
def remove(self, task: Future) -> None:
res = self.remove_impl(task)
if res:
logger.debug(f"Removed future %s from task set %s.", task, self._tag)
else:
logger.warning(f"Failed to remove future %s from task set %s.", task, self._tag)
class ThreadSafeTaskSet(TaskSetBase):
def __init__(self, tag: str):
super().__init__(tag)
self._lock = threading.Lock()
self._tasks: Set[Future] = set()
@property
def pending_tasks(self) -> Sequence[Future]:
with self._lock:
return tuple(self._tasks.copy())
def add_impl(self, task: Future) -> bool:
with self._lock:
if task in self._tasks:
return False
self._tasks.add(task)
return True
def remove_impl(self, task: Future) -> bool:
with self._lock:
try:
self._tasks.remove(task)
return True
except KeyError:
return False
class TaskSet(TaskSetBase):
def __init__(self, tag: str):
super().__init__(tag)
self._tasks: Set[Future] = set()
@property
def pending_tasks(self) -> Sequence[Future]:
return tuple(self._tasks.copy())
def add_impl(self, task: Future) -> bool:
if task in self._tasks:
return False
self._tasks.add(task)
return True
def remove_impl(self, task: Future) -> bool:
try:
self._tasks.remove(task)
return True
except KeyError:
return False
class EventLoopManagerBase:
_threadsafe_tasks: ClassVar[TaskSetBase] = ThreadSafeTaskSet("fallback")
def start(self) -> None:
pass
def stop(self) -> None:
pass
def plugin_loaded(self) -> None:
pass
@classmethod
def get_manager(cls, loop: Optional[AbstractEventLoop]) -> Optional[EventLoopManagerBase]:
return getattr(loop, "_owning_manager", None)
@classmethod
def get_tasks(cls, loop: Optional[AbstractEventLoop]) -> TaskSetBase:
mgr = cls.get_manager(loop)
return mgr.tasks if mgr else cls._threadsafe_tasks
@property
def loop(self) -> AbstractEventLoop:
raise NotImplementedError()
@property
def tasks(self) -> TaskSetBase:
return self._threadsafe_tasks
# NOTE: FIXME: This is a hack to reuse sublime_aio's event loop as the background loop if present
class SublimeAioEventLoopManager(EventLoopManagerBase):
def __init__(self):
self.loop._owning_manager = self # type: ignore
self._tasks = TaskSet("sublime aio background")
def stop(self):
# NOTE: This is the shutdown procedure of sublime_aio's event loop.
# In sublime_aio, this is part of their custom on_exit handler.
assert(sublime_aio)
sublime_aio.ExitEvent.wait()
if sublime_aio._loop is not None and sublime_aio._thread is not None:
loop = sublime_aio._loop
sublime_aio._loop = None
def shutdown():
for task in asyncio.all_tasks(loop):
task.cancel()
loop.stop()
loop.call_soon_threadsafe(shutdown)
sublime_aio._thread.join()
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
self._tasks.ensure_empty()
@property
def loop(self) -> AbstractEventLoop:
assert(sublime_aio)
if (sublime_aio._loop is None):
raise RuntimeError("sublime_aio event loop is not initialized.")
return sublime_aio._loop
@property
def tasks(self) -> TaskSetBase:
return self._tasks
class AsyncioEventLoopManager(EventLoopManagerBase):
def __init__(self):
self._executor = ThreadPoolExecutor(max_workers=16)
self._loop = asyncio.EventLoop()
self._loop.set_debug(True)
self._loop.set_default_executor(self._executor)
self._thread = threading.Thread(target=self._loop.run_forever)
self._loop._owning_manager = self # type: ignore
self._tasks = TaskSet("background")
def start(self):
self._thread.start()
def stop(self):
def shutdown():
for task in asyncio.all_tasks(self._loop):
task.cancel()
self._loop.stop()
self._loop.call_soon_threadsafe(shutdown)
self._thread.join()
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
self._loop.close()
self._executor.shutdown()
self._tasks.ensure_empty()
@property
def loop(self) -> AbstractEventLoop:
return self._loop
@property
def tasks(self) -> TaskSetBase:
return self._tasks
class SublimeUiEventLoopManager(EventLoopManagerBase):
def __init__(self):
self._loop = SublimeUiEventLoop()
self._loop.set_debug(True)
self._loop._owning_manager = self # type: ignore
self._tasks = TaskSet("ui")
def start(self):
# NOTE: This will also set the event loop as the running event loop
# for the calling thread. This *MUST* be called on the UI thread.
self._loop.start_run_forever()
def plugin_loaded(self):
logger.debug("Plugin loaded. Start scheduling UI loop steps.")
self._loop._start_scheduling()
def stop(self):
# NOTE: At this point, sublime API is no longer available.
# Remainder of the loop is run synchronously instead of through sublime.set_timeout.
def shutdown():
for task in asyncio.all_tasks(self._loop):
task.cancel()
# self._loop.stop()
# logging.info("shutdown proc complete")
print("stop scheduling")
self._loop._stop_scheduling()
print("schedule shutdown")
# self._loop.call_soon(shutdown)
# logging.info("run until complete")
# self._loop._run_until_run_forever_complete()
print(f"{self._loop}")
# self._loop.run_until_complete(self._loop.shutdown_asyncgens())
# self._loop.close()
# self._tasks.ensure_empty()
@property
def loop(self) -> AbstractEventLoop:
return self._loop
@property
def tasks(self) -> TaskSetBase:
return self._tasks
class EventLoopManager:
def __init__(self):
self._ui_loop_mgr = SublimeUiEventLoopManager()
self._background_loop_mgr: EventLoopManagerBase
# if sublime_aio:
# logger.info("sublime_aio installed. Use its event loop as background loop.")
# self._background_loop_mgr = SublimeAioEventLoopManager()
# else:
# self._background_loop_mgr = AsyncioEventLoopManager()
@property
def ui_event_loop(self):
return self._ui_loop_mgr.loop
@property
def background_event_loop(self):
pass
# return self._background_loop_mgr.loop
def start(self):
self._ui_loop_mgr.start()
# self._background_loop_mgr.start()
def plugin_loaded(self):
self._ui_loop_mgr.plugin_loaded()
# self._background_loop_mgr.plugin_loaded()
def stop(self):
try:
self._ui_loop_mgr.stop()
logger.info(f"UI event loop shut down.")
except Exception as ex:
logger.exception(f"An exception occured while shutting down the UI event loop: {ex}.")
# NOTE: Background loop must be stopped last.
# We can't call run_until_complete otherwise, because the UI loop is still running.
# try:
# self._background_loop_mgr.stop()
# logger.info(f"Background event loop shut down.")
# except Exception as ex:
# logger.exception(f"An exception occured while shutting down the background event loop: {ex}.")
_event_loop_manager: EventLoopManager
if globals().get("_event_loop_manager"):
logger.info("Event loops already started. Shutting down first.")
_event_loop_manager.stop() # type: ignore
logger.info("Event loops shut down.")
# _event_loop_manager = EventLoopManager()
# _event_loop_manager.start()
_counter = 0
def plugin_loaded():
global _counter
print(f"count {_counter}")
_counter += 1
_enable_fault_handler()
# _event_loop_manager.plugin_loaded()
class TeeStream(io.TextIOBase, TextIO):
def __init__(self, *streams: IO[str], close_streams: bool = True) -> None:
super().__init__()
self._streams: List[IO[str]] = list(streams)
self._close_streams = close_streams
def write(self, data: str) -> int:
for s in self._streams:
s.write(data)
return len(data)
def flush(self) -> None:
for s in self._streams:
s.flush()
def writable(self) -> bool:
return True
def readable(self) -> bool:
return False
def seekable(self) -> bool:
return False
def read(self, *args, **kwargs):
raise io.UnsupportedOperation("TeeStream is write-only.")
def close(self) -> None:
if self._close_streams:
for s in self._streams:
s.close()
super().close()
def __add__(self, stream: IO[str]) -> TeeStream:
if not stream.writable:
raise io.UnsupportedOperation("Stream is not writable.")
return TeeStream(*self._streams, stream)
def __iadd__(self, stream: IO[str]) -> TeeStream:
if not stream.writable:
raise io.UnsupportedOperation("Stream is not writable.")
self._streams.append(stream)
return self
def on_exit(log_path: str):
# NOTE: Custom exit handler to properly shut down the event loops.
# This overrides the one registered by sublime_aio, if present.
# The sublime_aio event loop will be shut down via _background_loop_manager.stop(), see above.
stdout = io.StringIO()
redirect_stream = TeeStream(stdout)
if _debug_stream:
redirect_stream += _debug_stream
root = logging.getLogger()
handler = logging.StreamHandler(stdout)
handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
root.addHandler(handler)
with contextlib.redirect_stdout(redirect_stream), contextlib.redirect_stderr(redirect_stream):
logger.info("Executing on_exit.")
for callback in sublime_plugin.el_callbacks('on_exit'):
callback()
_event_loop_manager.stop()
handler.flush()
redirect_stream.flush()
if len(stdout.getvalue()):
with open(log_path, "w", encoding="utf-8") as f:
f.write(stdout.getvalue())
else:
os.unlink(log_path)
# Register the custom exit handler.
sublime_plugin.on_exit = on_exit
def get_ui_event_loop() -> AbstractEventLoop:
"""
Returns the UI event loop.
Returns:
AbstractEventLoop:
The UI event loop.
"""
return _event_loop_manager.ui_event_loop
def get_background_event_loop() -> AbstractEventLoop:
"""
Returns the background event loop.
Returns:
AbstractEventLoop:
The background event loop.
"""
return _event_loop_manager.background_event_loop
class Future[T]:
"""
Wrapper for `AsyncIoFuture` and `ConcurrentFuture` objects to provide a unified interface for asynchronous operations.
Raises:
RuntimeError:
If an operation is attempted that is not available on the wrapped Future.
"""
def __init__(self, future: AsyncIoFuture[T] | ConcurrentFuture[T]):
setattr(future, "_owner", self)
self._future: AsyncIoFuture[T] | ConcurrentFuture[T] = future
def __repr__(self) -> str:
return self._future.__repr__()
def __await__(self) -> Generator[Any, None, T]:
if isinstance(self._future, AsyncIoFuture):
return self._future.__await__()
raise RuntimeError("Future Object is not an asyncio Future.")
def __iter__(self) -> Generator[Any, None, T]:
if isinstance(self._future, AsyncIoFuture):
return self._future.__iter__()
raise RuntimeError("Future Object is not an asyncio Future.")
def cancel(self) -> None:
self._future.cancel()
def get_loop(self) -> AbstractEventLoop:
if isinstance(self._future, AsyncIoFuture):
return self._future.get_loop()
raise RuntimeError("Future Object is not an asyncio Future.")
def try_get_loop(self) -> Optional[AbstractEventLoop]:
if isinstance(self._future, AsyncIoFuture):
return self._future.get_loop()
return None
def cancelled(self) -> bool:
return self._future.cancelled()
def running(self) -> bool:
if isinstance(self._future, ConcurrentFuture):
return self._future.running()
raise RuntimeError("Future Object is an asyncio Future.")
def done(self) -> bool:
return self._future.done()
def result(self, timeout=None) -> T:
if isinstance(self._future, AsyncIoFuture):
return self._future.result()
return self._future.result(timeout=timeout)
def exception(self, timeout=None) -> Optional[BaseException]:
if isinstance(self._future, AsyncIoFuture):
return self._future.exception()
return self._future.exception(timeout=timeout)
def add_done_callback(self, fn: Callable[[Future], object], *, context=None) -> None:
if isinstance(self._future, AsyncIoFuture):
self._future.add_done_callback(lambda f: fn(getattr(f, "_owner")), context=context)
else:
self._future.add_done_callback(lambda f: fn(getattr(f, "_owner")))
def remove_done_callback(self, fn) -> None:
# NOTE: Because added callbacks are wrapped in a lambda, we can't really remove them.
raise RuntimeError("remove_done_callback is not supported.")
def set_result(self, result: T) -> None:
self._future.set_result(result)
def set_exception(self, exception: Exception) -> None:
self._future.set_exception(exception)
@overload
def run_as_task(loop_or_factory: Callable[[], AbstractEventLoop] = asyncio.get_running_loop) -> \
Callable[[Callable[P, Coroutine[Any, Any, T] | T]], Callable[P, Future[T]]]:
...
@overload
def run_as_task(loop_or_factory: AbstractEventLoop) -> \
Callable[[Callable[P, Coroutine[Any, Any, T] | T]], Callable[P, Future[T]]]:
...
def run_as_task(loop_or_factory: Callable[[], AbstractEventLoop] | AbstractEventLoop = asyncio.get_running_loop) -> \
Callable[[Callable[P, Coroutine[Any, Any, T] | T]], Callable[P, Future[T]]]:
"""
Decorator to run a function or coroutine as an asynchronous task in a an event loop.
Parameters:
loop_or_factory (Callable[[], AbstractEventLoop] | AbstractEventLoop):
An event loop or a factory function that returns an event loop.
Defaults to the currently running loop.
Returns:
Callable[[Callable[..., Coroutine[Any, Any, T] | T]], Callable[..., Future[T]]]:
A decorator that wraps the input function to run it as an asynchronous task, returning a `Future`.
Usage Example:
```python
@run_as_task()
async def my_async_function() -> int:
await asyncio.sleep(1)
print("Hello from my_async_function!")
return 1
async def main():
task = my_async_function()
print("Hello from main!")
return await task
asyncio.runt(main())
"""
if isinstance(loop_or_factory, AbstractEventLoop):
loop_factory = lambda: loop_or_factory
else:
loop_factory = loop_or_factory
def decorator(func: Callable[P, Coroutine[Any, Any, T] | T]) -> Callable[P, Future[T]]:
coro_func: Callable[..., Coroutine[Any, Any, T]]
if (asyncio.iscoroutinefunction(func)):
coro_func = func
else:
async def coro_wrapper(*args, **kwargs) -> T:
return cast(Callable[P, T], func)(*args, **kwargs)
coro_func = coro_wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Future[T]:
loop = loop_factory()
running_loop = asyncio._get_running_loop()
coro = coro_func(*args, **kwargs)
if (running_loop is not None):
if (loop is running_loop):
logger.debug
res = Future(loop.create_task(coro))
else:
res = Future(asyncio.wrap_future(asyncio.run_coroutine_threadsafe(coro, loop)))
else:
res = Future(asyncio.run_coroutine_threadsafe(coro, loop))
logger.debug("Running %s as task on event loop %s, scheduled from %s. Future: %s", func, loop, running_loop, res)
return res
return wrapper
return decorator
def run_as_task_ui(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Future[T]]:
return run_as_task(get_ui_event_loop)(func)
def run_as_task_background(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Future[T]]:
return run_as_task(get_background_event_loop)(func)
def fire_and_forget(func: Callable[P, Future[T]]) -> Callable[P, None]:
"""
A decorator to run a coroutine in a fire and forget manner.
The function's result (or exception) is ignored.
Parameters:
func (Callable[..., Future[T]]):
The async function to be wrapped. It must return a Future object.
Returns:
Callable[..., None]:
A wrapper function that executes the original function in a fire-and-forget manner.
Example:
```python
@fire_and_forget
async def background_task():
await asyncio.sleep(5)
print("Task completed")
background_task() # Runs in the background without blocking
"""
@functools.wraps(func)
def wrapper(*args, **kwargs) -> None:
fut = func(*args, **kwargs)
loop = fut.try_get_loop()
tasks = EventLoopManagerBase.get_tasks(loop)
logger.debug("Running task %s as fire and forget.", fut)
tasks.add(fut)
def done_callback(f: Future):
exc = fut.exception()
if exc is not None:
logger.error("Fire and forget task %s has failed: %s.", fut, exc, exc_info=(type(exc), exc, exc.__traceback__))
else:
logger.debug("Fire and forget task %s has completed successfully.", fut)
tasks.remove(fut)
fut.add_done_callback(done_callback)
return wrapper
def run_in_executor(loop_or_factory: Callable[[], AbstractEventLoop] | AbstractEventLoop = asyncio.get_running_loop) -> \
Callable[[Callable[P, T]], Callable[P, Future[T]]]:
"""
A decorator to run a synchronous function in an executor and return a Future.
Parameters:
loop_or_factory (Callable[[], AbstractEventLoop] | AbstractEventLoop):
Either an event loop or a factory function that returns an event loop.
Defaults to the currently running event loop.
Returns:
Callable[[Callable[..., T]], Callable[..., Future[T]]]:
A decorator that wraps the input function to run it in an executor.
Raises:
RuntimeError: If the decorated function is a coroutine.
Example:
```python
@run_in_executor()
def blocking_io_task():
time.sleep(2)
return "Done"
async def main():
future = blocking_io_task()
result = await future
print(result) # Output: "Done"
asyncio.run(main())
"""
if isinstance(loop_or_factory, AbstractEventLoop):
loop_factory = lambda: loop_or_factory
else:
loop_factory = loop_or_factory
def decorator(func: Callable[P, T]) -> Callable[P, Future[T]]:
if (asyncio.iscoroutinefunction(func)):
raise RuntimeError("run_in_executor does not support coroutines. Use run_as_task instead.")
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Future[T]:
loop = loop_factory()
running_loop = asyncio._get_running_loop()
if (running_loop is not None):
res = Future(asyncio.wrap_future(loop.run_in_executor(None, lambda: func(*args, **kwargs))))
else:
res = Future(loop.run_in_executor(None, lambda: func(*args, **kwargs)))
logger.debug("Running %s in executor of loop %s. Future: %s.", func, loop, res)
return res
return wrapper
return decorator
class CommandHandlerDict(Generic[U]):
def __init__(self, tag: str):
self._dict: Dict[int, U] = dict()
self._lock = threading.Lock()
self._tag = tag
def pop(self, key: int, default: Optional[U] = None) -> Optional[U]:
with self._lock:
return self._dict.pop(key, default)
def get(self, key: int, default: Optional[U] = None) -> Optional[U]:
with self._lock:
return self._dict.get(key, default)
@property
def pending_handlers(self) -> Dict[int, U]:
with self._lock:
return self._dict.copy()
def ensure_empty(self) -> None:
pending_handlers = self.pending_handlers.items()
if len(pending_handlers) > 0:
logger.warning("Command handler dict %s is not empty.", self._tag)
if logger.isEnabledFor(logging.DEBUG):
for k, v in pending_handlers:
logger.debug("Pending handler %s (handler id %s) in command handler dict %s was never executed", v, k, self._tag)
def __setitem__(self, key: int, value: U) -> None:
with self._lock:
if key in self._dict:
logger.warning("Failed to add command handler %s (handler id %s) to command handler dict %s.", value, key, self._tag)
else:
self._dict[key] = value
logger.debug("Added command handler %s (handler id %s) to command handler dict %s.", value, key, self._tag)
def __getitem__(self, key: int) -> U:
with self._lock:
return self._dict[key]
class HasViewProperty(Protocol):
@property
def view(self) -> sublime.View:
...
HasViewPropertyT = TypeVar('HasViewPropertyT', bound="HasViewProperty")
@overload
def run_as_text_command(func: Callable[Concatenate[HasViewPropertyT, sublime.Edit, sublime.View, P], R]) -> Callable[Concatenate[HasViewPropertyT, P], Future[R]]:
...
@overload
def run_as_text_command(func: Callable[Concatenate[Self, sublime.Edit, sublime.View, P], R]) -> Callable[Concatenate[Self, sublime.View, P], Future[R]]:
...
@overload
def run_as_text_command(func: Callable[Concatenate[sublime.Edit, sublime.View, P], R]) -> Callable[Concatenate[sublime.View, P], Future[R]]:
...
def run_as_text_command(func: Callable[..., R]) -> Callable[..., Future[R]]:
"""
Decorator that converts a function or method into a text command.
Executes the decorated function within in a text command context, providing a valid
`sublime.Edit` instance, and returns a `Future` that resolves with the function's return value once
the text command has completed.
Parameters:
`func` (`Callable[[sublime.Edit, sublime.View, ...], R]`):
The function or method to wrap. Expects the first two parameters to be `sublime.Edit`
and `sublime.View`.
Returns:
`Callable[[sublime.View, ...], Future[R]]`:
A wrapper function that accepts the `sublime.View` instance on which to run the text command and remaining arguments,
executes the the orignal function or method in a text command context and returns a Future that resolves
once the text command has completed.
If the wrapped function is a method of a class that has a `view` field of type `sublime.View`, the view
parameter is not required.
Example:
```python
@run_as_text_command
def insert_text(edit: sublime.Edit, view: sublime.View, text: str) -> int:
return view.insert(edit, 0, text)
future = insert_text(view, "Hello World")
class Foo:
@property
def view(self) -> sublime.View:
return self._view
@run_as_text_command
def insert_text(self, edit: sublime.Edit, view: sublime.View, text: str) -> int:
return view.insert(edit, 0, text)
future = Foo().insert_text("Hello World")
"""
sig = inspect.signature(func)
params = list(sig.parameters.values())
param0 = params[0]
is_method = param0.name == "self"
def _run(f: Callable[[sublime.Edit, sublime.View], R], view: sublime.View, *args, **kwargs) -> Tuple[Future[R], int]:
running_loop = asyncio.get_running_loop()
fut: AsyncIoFuture[R] | ConcurrentFuture[R]
res: Future[R]
if (running_loop is not get_ui_event_loop()):
# NOTE: Called without running event loop or from a thread different from the UI thread.
fut = ConcurrentFuture()
if (running_loop is None):
res = Future(fut)
else:
res = Future(asyncio.wrap_future(fut))
else:
# NOTE: Command will be run on the UI thread.
# If called from UI thread, we can safely use an asyncio Future.
# This also prevents synchronously waiting for the future to
# avoid deadlocks.
fut = AsyncIoFuture()
res = Future(fut)
def handler(edit: sublime.Edit, view: sublime.View) -> None:
try:
res = f(edit, view, *args, **kwargs)
fut.set_result(res)
except Exception as ex:
fut.set_exception(ex)
handler_id = SublimeAsyncRunAsTextCommandCommand.register_handler(handler)
view.run_command("sublime_async_run_as_text_command", {"_handler_id": handler_id})
return (res, handler_id)
if (is_method):
def method_wrapper(self, *args, **kwargs) -> Future[R]:
if (hasattr(self, "view") and isinstance(self.view, sublime.View)):
res = _run(functools.partial(func, self), self.view, *args, **kwargs)
else:
res = _run(functools.partial(func, self), *args, **kwargs)
logger.debug("Running %s as text command (handler id %s). Future: %s.", func, res[1], res[0])
return res[0]
return method_wrapper
else:
def wrapper(view: sublime.View, *args, **kwargs) -> Future[R]:
res = _run(func, view, *args, **kwargs)
logger.debug("Running %s as text command (handler id %s). Future: %s.", func, res[1], res[0])
return res[0]
return wrapper
class HasWindowProperty(Protocol):
@property
def window(self) -> sublime.Window:
...
HasWindowPropertyT = TypeVar("HasWindowPropertyT", bound=HasWindowProperty)
@overload
def run_as_window_command(func: Callable[Concatenate[HasWindowPropertyT, sublime.Window, P], R]) -> Callable[Concatenate[HasWindowPropertyT, P], Future[R]]:
...
@overload
def run_as_window_command(func: Callable[Concatenate[Self, sublime.Window, P], R]) -> Callable[Concatenate[Self, sublime.Window, P], Future[R]]:
...
@overload
def run_as_window_command(func: Callable[Concatenate[sublime.Window, P], R]) -> Callable[Concatenate[sublime.Window, P], Future[R]]:
...
def run_as_window_command(func: Callable[..., R]) -> Callable[..., Future[R]]:
"""
Decorator that converts a function or method into a window command.
Executes the decorated function within in a window command context,
and returns a `Future` that resolves with the function's return value once
the text command has completed.
Parameters:
`func` (`Callable[[sublime.Window, ...], R]`):
The function or method to wrap. Expects the first parameters to be `sublime.Window`.
Returns:
`Callable[[sublime.Window, ...], Future[R]]`:
A wrapper function that accepts the `sublime.Window` instance on which to run the window command and remaining arguments,
executes the the orignal function or method in a window command context and returns a Future that resolves
once the text command has completed.
If the wrapped function is a method of a class that has a `window` field of type `sublime.Window`, the window
parameter is not required.
Example:
```python
@run_as_window_command
def to_front(window: sublime.Window, text: str) -> None:
window.bring_to_front()
future = to_front(window)
class Foo:
@property
def window(self) -> sublime.Window:
return self._window
@run_as_window_command
def to_front(self, window: sublime.Window) -> None:
window.bring_to_front()
future = Foo().to_front()
"""
sig = inspect.signature(func)
params = list(sig.parameters.values())
param0 = params[0]
is_method = param0.name == "self"
def _run(f: Callable[[sublime.Window], R], window: sublime.Window, *args, **kwargs) -> Tuple[Future[R], int]:
running_loop = asyncio._get_running_loop()
fut: AsyncIoFuture[R] | ConcurrentFuture[R]
res: Future[R]
if (running_loop is not get_ui_event_loop()):
# NOTE: Called without running event loop or from a thread different from the UI thread.
fut = ConcurrentFuture()
if (running_loop is None):
res = Future(fut)
else:
res = Future(asyncio.wrap_future(fut))
else:
# NOTE: Command will be run on the UI thread.
# If called from UI thread, we can safely use an asyncio Future.
# This also prevents synchronously waiting for the future to
# avoid deadlocks.
fut = AsyncIoFuture()
res = Future(fut)
def handler(window: sublime.Window) -> None:
try:
res = f(window, *args, **kwargs)
fut.set_result(res)
except Exception as ex:
fut.set_exception(ex)
handler_id = SublimeAsyncRunAsWindowCommandCommand.register_handler(handler)
window.run_command("sublime_async_run_as_window_command", {"_handler_id": handler_id})
return (res, handler_id)
if (is_method):
def method_wrapper(self, *args, **kwargs) -> Future[R]:
if (hasattr(self, "window") and isinstance(self.window, sublime.Window)):
res = _run(functools.partial(func, self), self.window, *args, **kwargs)
else:
res = _run(functools.partial(func, self), *args, **kwargs)
logger.debug("Running %s as window command (handler id %s). Future: %s.", func, res[1], res[0])
return res[0]
return method_wrapper
else:
def wrapper(window: sublime.Window, *args, **kwargs) -> Future[R]:
res = _run(func, window, *args, **kwargs)
logger.debug("Running %s as window command (handler id %s). Future: %s.", func, res[1], res[0])
return res[0]
return wrapper
def run_as_application_command(func: Callable[P, R]) -> Callable[P, Future[R]]:
"""
Decorator that converts a function or method into an application command.
Executes the decorated function within in an application command context,
and returns a `Future` that resolves with the function's return value once
the application command has completed.
Parameters:
`func` (`Callable[..., R]`):
The function or method to wrap.
Returns:
`Callable[..., Future[R]]`:
A wrapper function that executes the the orignal function or method in an application
command context and returns a Future that resolves once the application command has completed.
Example:
```python
@run_as_application_command
def hello_world() -> None:
print("Hello world")
future = hello_world()
"""
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Future[R]:
running_loop = asyncio._get_running_loop()
fut: AsyncIoFuture[R] | ConcurrentFuture[R]
res: Future[R]
if (running_loop is not get_ui_event_loop()):
# NOTE: Called without running event loop or from a thread different from the UI thread.
fut = ConcurrentFuture()
if (running_loop is None):
res = Future(fut)
else:
res = Future(asyncio.wrap_future(fut))
else:
# NOTE: Command will be run on the UI thread.
# If called from UI thread, we can safely use an asyncio Future.
# This also prevents synchronously waiting for the future to
# avoid deadlocks.
fut = AsyncIoFuture()
res = Future(fut)
def handler():
try:
res = func(*args, **kwargs)
fut.set_result(res)
except Exception as ex:
fut.set_exception(ex)
handler_id = SublimeAsyncRunAsApplicationCommandCommand.register_handler(handler)
sublime.run_command("sublime_async_run_as_application_command", {"_handler_id": handler_id})
logger.debug("Running %s as application command (handler id %s). Future: %s.", func, handler_id, res)
return res
return wrapper
class SublimeAsyncRunAsTextCommandCommand(sublime_plugin.TextCommand):
_pending: ClassVar[CommandHandlerDict[Callable[[sublime.Edit, sublime.View], Any]]] = CommandHandlerDict("text command")
@classmethod
def register_handler(cls, handler: Callable[[sublime.Edit, sublime.View], Any]) -> int:
id = int.from_bytes(os.urandom(4))
cls._pending[id] = handler
return id
def run(self, edit: sublime.Edit, *, _handler_id: sublime.Value, **kwargs: sublime.Value):
handler = self._pending.pop(cast(int, _handler_id))
if(handler is not None):
logger.debug("Executing text command handler %s for handler id %s.", _handler_id)
handler(edit, self.view)
else:
logger.warning("No text command handler registered for handler id %s.", _handler_id)
class SublimeAsyncRunAsWindowCommandCommand(sublime_plugin.WindowCommand):
_pending: ClassVar[CommandHandlerDict[Callable[[sublime.Window], Any]]] = CommandHandlerDict("window command")
@classmethod
def register_handler(cls, handler: Callable[[sublime.Window], Any]) -> int:
id = int.from_bytes(os.urandom(4))
cls._pending[id] = handler
return id
def run(self, *, _handler_id: sublime.Value, **kwargs: sublime.Value):
handler = self._pending.pop(cast(int, _handler_id))
if (handler is not None):
logger.debug("Executing window command handler %s for handler id %s.", _handler_id)
handler(self.window)
else:
logger.warning("No window command handler registered for handler id %s.", _handler_id)
class SublimeAsyncRunAsApplicationCommandCommand(sublime_plugin.ApplicationCommand):
_pending: ClassVar[CommandHandlerDict[Callable[[], Any]]] = CommandHandlerDict("application command")
@classmethod
def register_handler(cls, handler: Callable[[], Any]) -> int:
id = int.from_bytes(os.urandom(4))
cls._pending[id] = handler
return id
def run(self, *, _handler_id: sublime.Value, **kwargs: sublime.Value):
handler = self._pending.pop(cast(int, _handler_id))
if (handler is not None):
logger.debug("Executing application command handler %s for handler id %s.", _handler_id)
handler()
else:
logger.warning("No application command handler registered for handler id %s.", _handler_id)
def with_event_loop(loop_or_factory: Callable[[], AbstractEventLoop] | AbstractEventLoop):
if isinstance(loop_or_factory, AbstractEventLoop):
loop_factory = lambda: loop_or_factory
else:
loop_factory = loop_or_factory
def decorator(cls: Type[AsyncCommandT]) -> Type:
setattr(cls, "_loop_factory", staticmethod(loop_factory))
return cls
return decorator
@with_event_loop(asyncio.get_running_loop)
class AsyncCommand:
_loop_factory: ClassVar[Callable[[], AbstractEventLoop]] = staticmethod(asyncio.get_running_loop)
async def run(self, **kwargs) -> None:
raise NotImplementedError()
def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
original_run: Optional[Callable[..., Coroutine[Any, Any, None]]] = cls.__dict__.get("run")
if original_run is None:
return
@functools.wraps(original_run)
def wrapper(self: AsyncCommand, **kwargs) -> None:
# loop: AbstractEventLoop = type(self)._loop_factory()
# fut = run_as_task(self._loop_factory)(original_run)(self, **kwargs)
fut = Future(asyncio.run_coroutine_threadsafe(original_run(self, **kwargs), self._loop_factory()))
def handle_exception(fut: Future) -> None:
print("handle exception 11")
if fut.cancelled():
return
exc = fut.exception()
if (exc is not None):
logger.error(f"Async command failed: {exc}", exc_info=(type(exc), exc, exc.__traceback__))
fut.add_done_callback(handle_exception)
print(fut.done)
fire_and_forget(lambda: fut)
setattr(cls, "run", wrapper)
setattr(cls, "_original_run", original_run)
# NOTE: For some reason, with these command classes \/ \/ \/ plugin_loaded is called twice. If commented out, its fine?
class AsyncWindowCommand(AsyncCommand, sublime_plugin.WindowCommand): # type: ignore
pass
class AsyncApplicationCommand(AsyncCommand, sublime_plugin.ApplicationCommand): # type: ignore
pass
AsyncCommandT = TypeVar('AsyncCommandT', bound=AsyncCommand)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment