Add timeouts and error forwarding to wait_for_{property,event} conditions

This commit is contained in:
jaseg 2022-03-26 14:18:34 +01:00
parent a7e61c9362
commit 0cda09c628
2 changed files with 86 additions and 40 deletions

85
mpv.py
View file

@ -24,6 +24,7 @@ import sys
from warnings import warn from warnings import warn
from functools import partial, wraps from functools import partial, wraps
from contextlib import contextmanager from contextlib import contextmanager
from concurrent.futures import Future
import collections import collections
import re import re
import traceback import traceback
@ -903,79 +904,92 @@ class MPV(object):
if self._core_shutdown: if self._core_shutdown:
raise ShutdownError('libmpv core has been shutdown') raise ShutdownError('libmpv core has been shutdown')
def wait_until_paused(self): def wait_until_paused(self, timeout=None):
"""Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while
waiting.""" waiting."""
self.wait_for_property('core-idle') self.wait_for_property('core-idle', timeout=timeout)
def wait_for_playback(self): def wait_for_playback(self, timeout=None):
"""Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while
waiting. waiting.
""" """
self.wait_for_event('end_file') self.wait_for_event('end_file', timeout=timeout)
def wait_until_playing(self): def wait_until_playing(self, timeout=None):
"""Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while
waiting.""" waiting."""
self.wait_for_property('core-idle', lambda idle: not idle) self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout)
def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True): def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
"""Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for
properties such as ``idle_active`` indicating the player is done with regular playback and just idling around. properties such as ``idle_active`` indicating the player is done with regular playback and just idling around.
Raises a ShutdownError when the core is shutdown while waiting. Raises a ShutdownError when the core is shutdown while waiting.
""" """
with self.prepare_and_wait_for_property(name, cond, level_sensitive): with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout):
pass pass
def wait_for_shutdown(self): def wait_for_shutdown(self, timeout=None):
'''Wait for core to shutdown (e.g. through quit() or terminate()).''' '''Wait for core to shutdown (e.g. through quit() or terminate()).'''
sema = threading.Semaphore(value=0) result = Future()
@self.event_callback('shutdown') @self.event_callback('shutdown')
def shutdown_handler(event): def shutdown_handler(event):
sema.release() result.set_result(None)
sema.acquire() try:
if self._core_shutdown:
return
result.set_running_or_notify_cancel()
return result.result(timeout)
finally:
shutdown_handler.unregister_mpv_events() shutdown_handler.unregister_mpv_events()
@contextmanager @contextmanager
def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True): def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None):
"""Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See
prepare_and_wait_for_event for usage. prepare_and_wait_for_event for usage.
Raises a ShutdownError when the core is shutdown while waiting. Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``.
""" """
sema = threading.Semaphore(value=0) result = Future()
def observer(name, val): def observer(name, val):
if cond(val): try:
sema.release() rv = cond(val)
if rv:
result.set_result(rv)
except Exception as e:
result.set_exception(e)
self.observe_property(name, observer) self.observe_property(name, observer)
@self.event_callback('shutdown') @self.event_callback('shutdown')
def shutdown_handler(event): def shutdown_handler(event):
sema.release() result.set_exception(ShutdownError('libmpv core has been shutdown'))
try:
yield yield
if not level_sensitive or not cond(getattr(self, name.replace('-', '_'))): if not level_sensitive or not cond(getattr(self, name.replace('-', '_'))):
sema.acquire()
self.check_core_alive() self.check_core_alive()
result.set_running_or_notify_cancel()
return result.result(timeout)
finally:
shutdown_handler.unregister_mpv_events() shutdown_handler.unregister_mpv_events()
self.unobserve_property(name, observer) self.unobserve_property(name, observer)
def wait_for_event(self, *event_types, cond=lambda evt: True): def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
"""Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError
if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error
inside ``cond``.
""" """
with self.prepare_and_wait_for_event(*event_types, cond=cond): with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout):
pass pass
@contextmanager @contextmanager
def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True): def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None):
"""Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given, """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given,
waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens
when 'shutdown' is in event_types. when 'shutdown' is in event_types. Re-raises any error inside ``cond``.
Compared to wait_for_event this handles the case where a thread waits for an event it itself causes in a Compared to wait_for_event this handles the case where a thread waits for an event it itself causes in a
thread-safe way. An example from the testsuite is: thread-safe way. An example from the testsuite is:
@ -986,22 +1000,29 @@ class MPV(object):
Using just wait_for_event it would be impossible to ensure the event is caught since it may already have been Using just wait_for_event it would be impossible to ensure the event is caught since it may already have been
handled in the interval between keypress(...) running and a subsequent wait_for_event(...) call. handled in the interval between keypress(...) running and a subsequent wait_for_event(...) call.
""" """
sema = threading.Semaphore(value=0) result = Future()
@self.event_callback('shutdown') @self.event_callback('shutdown')
def shutdown_handler(event): def shutdown_handler(event):
sema.release() result.set_exception(ShutdownError('libmpv core has been shutdown'))
@self.event_callback(*event_types) @self.event_callback(*event_types)
def target_handler(evt): def target_handler(evt):
if cond(evt):
sema.release()
try:
rv = cond(evt)
if rv:
result.set_result(rv)
except Exception as e:
result.set_exception(e)
try:
yield yield
sema.acquire()
self.check_core_alive() self.check_core_alive()
result.set_running_or_notify_cancel()
return result.result(timeout)
finally:
shutdown_handler.unregister_mpv_events() shutdown_handler.unregister_mpv_events()
target_handler.unregister_mpv_events() target_handler.unregister_mpv_events()

View file

@ -380,6 +380,31 @@ class KeyBindingTest(MpvTestCase):
self.assertNotIn(b('b'), self.m._key_binding_handlers) self.assertNotIn(b('b'), self.m._key_binding_handlers)
self.assertIn(b('c'), self.m._key_binding_handlers) self.assertIn(b('c'), self.m._key_binding_handlers)
def test_wait_for_event_error_forwarding(self):
self.m.play(TESTVID)
def check(evt):
raise ValueError('fnord')
with self.assertRaises(ValueError):
self.m.wait_for_event('end_file', cond=check)
def test_wait_for_property_error_forwarding(self):
def run():
nonlocal self
self.m.wait_until_playing()
self.m.mute = True
t = threading.Thread(target=run, daemon=True)
t.start()
def cond(mute):
if mute:
raise ValueError('fnord')
with self.assertRaises(ValueError):
self.m.play(TESTVID)
self.m.wait_for_property('mute', cond=cond)
def test_register_simple_decorator_fun_chaining(self): def test_register_simple_decorator_fun_chaining(self):
self.m.loop = 'inf' self.m.loop = 'inf'
self.m.play(TESTVID) self.m.play(TESTVID)