Hello community, here is the log from the commit of package python-python-mpv for openSUSE:Factory checked in at 2020-07-20 21:02:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-python-mpv (Old) and /work/SRC/openSUSE:Factory/.python-python-mpv.new.3592 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-mpv" Mon Jul 20 21:02:36 2020 rev:14 rq:821925 version:0.5.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-python-mpv/python-python-mpv.changes 2020-07-17 20:50:46.756915856 +0200 +++ /work/SRC/openSUSE:Factory/.python-python-mpv.new.3592/python-python-mpv.changes 2020-07-20 21:04:36.981290325 +0200 @@ -1,0 +2,21 @@ +Mon Jul 20 14:50:15 UTC 2020 - aloi...@gmx.com + +- Update to version 0.5.1 + * mpv.py: terminate: Raise warning when called from event + thread. + * mpv.py: add wait_for_shutdown + * mpv.py: add check_core_alive, check core in __getattr__, + __setattr__ + +------------------------------------------------------------------- +Mon Jul 20 07:38:56 UTC 2020 - Luigi Baldoni <aloi...@gmx.com> + +- Update to version 0.5.0 + * mpv.py: add prepare_and_wait_for_property + * mpv.py: Update copyright date + * mpv.py: Add docstrings to new additions to API + * Sprinkle some thread safety over event loop, add + *wait_for_event + * mpv.py: improve shutdown handling, replace wait_for_playback + +------------------------------------------------------------------- Old: ---- python-mpv-0.4.8.tar.gz New: ---- python-mpv-0.5.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-python-mpv.spec ++++++ --- /var/tmp/diff_new_pack.vh2SNY/_old 2020-07-20 21:04:39.661293043 +0200 +++ /var/tmp/diff_new_pack.vh2SNY/_new 2020-07-20 21:04:39.665293046 +0200 @@ -17,7 +17,7 @@ Name: python-python-mpv -Version: 0.4.8 +Version: 0.5.1 Release: 0 Summary: Python interface to the mpv media player License: AGPL-3.0-or-later ++++++ python-mpv-0.4.8.tar.gz -> python-mpv-0.5.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-0.4.8/PKG-INFO new/python-mpv-0.5.1/PKG-INFO --- old/python-mpv-0.4.8/PKG-INFO 2020-07-16 21:06:35.000000000 +0200 +++ new/python-mpv-0.5.1/PKG-INFO 2020-07-20 14:20:52.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-mpv -Version: 0.4.8 +Version: 0.5.1 Summary: A python interface to the mpv media player Home-page: https://github.com/jaseg/python-mpv Author: jaseg diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-0.4.8/mpv.py new/python-mpv-0.5.1/mpv.py --- old/python-mpv-0.4.8/mpv.py 2020-07-16 19:19:44.000000000 +0200 +++ new/python-mpv-0.5.1/mpv.py 2020-07-19 22:29:09.000000000 +0200 @@ -2,7 +2,7 @@ # vim: ts=4 sw=4 et # # Python MPV library module -# Copyright (C) 2017 Sebastian Götte <c...@jaseg.net> +# Copyright (C) 2017-2020 Sebastian Götte <c...@jaseg.net> # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any @@ -23,6 +23,7 @@ import sys from warnings import warn from functools import partial, wraps +from contextlib import contextmanager import collections import re import traceback @@ -53,6 +54,9 @@ fs_enc = sys.getfilesystemencoding() +class ShutdownError(SystemError): + pass + class MpvHandle(c_void_p): pass @@ -633,41 +637,6 @@ yield event -def _event_loop(event_handle, playback_cond, event_callbacks, message_handlers, property_handlers, log_handler): - for event in _event_generator(event_handle): - try: - devent = event.as_dict(decoder=lazy_decoder) # copy data from ctypes - eid = devent['event_id'] - for callback in event_callbacks: - callback(devent) - - if eid in (MpvEventID.SHUTDOWN, MpvEventID.END_FILE): - with playback_cond: - playback_cond.notify_all() - - if eid == MpvEventID.PROPERTY_CHANGE: - pc = devent['event'] - name, value, _fmt = pc['name'], pc['value'], pc['format'] - for handler in property_handlers[name]: - handler(name, value) - - if eid == MpvEventID.LOG_MESSAGE and log_handler is not None: - ev = devent['event'] - log_handler(ev['level'], ev['prefix'], ev['text']) - - if eid == MpvEventID.CLIENT_MESSAGE: - # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} - target, *args = devent['event']['args'] - if target in message_handlers: - message_handlers[target](*args) - - if eid == MpvEventID.SHUTDOWN: - _mpv_detach_destroy(event_handle) - return - - except Exception as e: - traceback.print_exc() - _py_to_mpv = lambda name: name.replace('_', '-') _mpv_to_py = lambda name: name.replace('-', '_') @@ -840,6 +809,7 @@ self.handle = _mpv_create() self._event_thread = None + self._core_shutdown = False _mpv_set_option_string(self.handle, b'audio-display', b'no') istr = lambda o: ('yes' if o else 'no') if type(o) is bool else str(o) @@ -858,13 +828,13 @@ self.lazy = _DecoderPropertyProxy(self, lazy_decoder) self._event_callbacks = [] + self._event_handler_lock = threading.Lock() self._property_handlers = collections.defaultdict(lambda: []) + self._quit_handlers = set() self._message_handlers = {} self._key_binding_handlers = {} - self._playback_cond = threading.Condition() self._event_handle = _mpv_create_client(self.handle, b'py_event_handler') - self._loop = partial(_event_loop, self._event_handle, self._playback_cond, self._event_callbacks, - self._message_handlers, self._property_handlers, log_handler) + self._log_handler = log_handler self._stream_protocol_cbs = {} self._stream_protocol_frontends = collections.defaultdict(lambda: {}) self.register_stream_protocol('python', self._python_stream_open) @@ -881,32 +851,157 @@ else: self._event_thread = None - def wait_for_playback(self): - """Waits until playback of the current title is paused or done.""" - with self._playback_cond: - self._playback_cond.wait() + def _loop(self): + for event in _event_generator(self._event_handle): + try: + devent = event.as_dict(decoder=lazy_decoder) # copy data from ctypes + eid = devent['event_id'] + + with self._event_handler_lock: + if eid == MpvEventID.SHUTDOWN: + self._core_shutdown = True + + for callback in self._event_callbacks: + callback(devent) + + if eid == MpvEventID.PROPERTY_CHANGE: + pc = devent['event'] + name, value, _fmt = pc['name'], pc['value'], pc['format'] + for handler in self._property_handlers[name]: + handler(name, value) + + if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None: + ev = devent['event'] + self._log_handler(ev['level'], ev['prefix'], ev['text']) + + if eid == MpvEventID.CLIENT_MESSAGE: + # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} + target, *args = devent['event']['args'] + if target in self._message_handlers: + self._message_handlers[target](*args) + + if eid == MpvEventID.SHUTDOWN: + _mpv_detach_destroy(self._event_handle) + return + + except Exception as e: + print('Exception inside python-mpv event loop:', file=sys.stderr) + traceback.print_exc() + + @property + def core_shutdown(self): + """Property indicating whether the core has been shut down. Possible causes for this are e.g. the `quit` command + or a user closing the mpv window.""" + return self._core_shutdown + + def check_core_alive(self): + """ This method can be used as a sanity check to tests whether the core is still alive at the time it is + called.""" + if self._core_shutdown: + raise ShutdownError('libmpv core has been shutdown') def wait_until_paused(self): - """Waits until playback of the current title is paused or done.""" + """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while + waiting.""" self.wait_for_property('core-idle') + def wait_for_playback(self): + """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while + waiting. + """ + self.wait_for_event('end_file') + def wait_until_playing(self): - """Waits until playback of the current title has started.""" + """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while + waiting.""" self.wait_for_property('core-idle', lambda idle: not idle) def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True): """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. """ + with self.prepare_and_wait_for_property(name, cond, level_sensitive): + pass + + def wait_for_shutdown(self): + '''Wait for core to shutdown (e.g. through quit() or terminate()).''' sema = threading.Semaphore(value=0) + + @self.event_callback('shutdown') + def shutdown_handler(event): + sema.release() + + sema.acquire() + shutdown_handler.unregister_mpv_events() + + @contextmanager + def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True): + """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See + prepare_and_wait_for_event for usage. + Raises a ShutdownError when the core is shutdown while waiting. + """ + sema = threading.Semaphore(value=0) + def observer(name, val): if cond(val): sema.release() self.observe_property(name, observer) + + @self.event_callback('shutdown') + def shutdown_handler(event): + sema.release() + + yield if not level_sensitive or not cond(getattr(self, name.replace('-', '_'))): sema.acquire() + + self.check_core_alive() + + shutdown_handler.unregister_mpv_events() self.unobserve_property(name, observer) + def wait_for_event(self, *event_types, cond=lambda evt: True): + """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. + """ + with self.prepare_and_wait_for_event(*event_types, cond=cond): + pass + + @contextmanager + def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True): + """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 + when 'shutdown' is in event_types. + + 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: + + with self.m.prepare_and_wait_for_event('client_message'): + self.m.keypress(key) + + 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. + """ + sema = threading.Semaphore(value=0) + + @self.event_callback('shutdown') + def shutdown_handler(event): + sema.release() + + @self.event_callback(*event_types) + def target_handler(evt): + if cond(evt): + sema.release() + + yield + sema.acquire() + + self.check_core_alive() + + shutdown_handler.unregister_mpv_events() + target_handler.unregister_mpv_events() + def __del__(self): if self.handle: self.terminate() @@ -914,13 +1009,16 @@ def terminate(self): """Properly terminates this player instance. Preferably use this instead of relying on python's garbage collector to cause this to be called from the object's destructor. + + This method will detach the main libmpv handle and wait for mpv to shut down and the event thread to finish. """ self.handle, handle = None, self.handle if threading.current_thread() is self._event_thread: - # Handle special case to allow event handle to be detached. - # This is necessary since otherwise the event thread would deadlock itself. - grim_reaper = threading.Thread(target=lambda: _mpv_terminate_destroy(handle)) - grim_reaper.start() + raise UserWarning('terminate() should not be called from event thread (e.g. from a callback function). If ' + 'you want to terminate mpv from here, please call quit() instead, then sync the main thread ' + 'against the event thread using e.g. wait_for_shutdown(), then terminate() from the main thread. ' + 'This call has been transformed into a call to quit().') + self.quit() else: _mpv_terminate_destroy(handle) if self._event_thread: @@ -1229,6 +1327,9 @@ print("It's loud!", volume) my_handler.unregister_mpv_properties() + + exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g. + from calling MPV.terminate() or issuing a "quit" input command). """ self._property_handlers[name].append(handler) _mpv_observe_property(self._event_handle, hash(name)&0xffffffffffffffff, name.encode('utf-8'), MpvFormat.NODE) @@ -1342,14 +1443,16 @@ my_handler.unregister_mpv_events() """ def register(callback): - types = [MpvEventID.from_str(t) if isinstance(t, str) else t for t in event_types] or MpvEventID.ANY - @wraps(callback) - def wrapper(event, *args, **kwargs): - if event['event_id'] in types: - callback(event, *args, **kwargs) - self._event_callbacks.append(wrapper) - wrapper.unregister_mpv_events = partial(self.unregister_event_callback, wrapper) - return wrapper + with self._event_handler_lock: + self.check_core_alive() + types = [MpvEventID.from_str(t) if isinstance(t, str) else t for t in event_types] or MpvEventID.ANY + @wraps(callback) + def wrapper(event, *args, **kwargs): + if event['event_id'] in types: + callback(event, *args, **kwargs) + self._event_callbacks.append(wrapper) + wrapper.unregister_mpv_events = partial(self.unregister_event_callback, wrapper) + return wrapper return register @staticmethod @@ -1645,6 +1748,7 @@ # Property accessors def _get_property(self, name, decoder=strict_decoder, fmt=MpvFormat.NODE): + self.check_core_alive() out = create_string_buffer(sizeof(MpvNode)) try: cval = _mpv_get_property(self.handle, name.encode('utf-8'), fmt, out) @@ -1661,6 +1765,7 @@ return None def _set_property(self, name, value): + self.check_core_alive() ename = name.encode('utf-8') if isinstance(value, (list, set, dict)): _1, _2, _3, pointer = _make_node_str_list(value) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-0.4.8/python_mpv.egg-info/PKG-INFO new/python-mpv-0.5.1/python_mpv.egg-info/PKG-INFO --- old/python-mpv-0.4.8/python_mpv.egg-info/PKG-INFO 2020-07-16 21:06:35.000000000 +0200 +++ new/python-mpv-0.5.1/python_mpv.egg-info/PKG-INFO 2020-07-20 14:20:52.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-mpv -Version: 0.4.8 +Version: 0.5.1 Summary: A python interface to the mpv media player Home-page: https://github.com/jaseg/python-mpv Author: jaseg diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-0.4.8/setup.py new/python-mpv-0.5.1/setup.py --- old/python-mpv-0.4.8/setup.py 2020-07-16 21:06:00.000000000 +0200 +++ new/python-mpv-0.5.1/setup.py 2020-07-20 14:20:10.000000000 +0200 @@ -3,7 +3,7 @@ from setuptools import setup setup( name = 'python-mpv', - version = '0.4.8', + version = '0.5.1', py_modules = ['mpv'], description = 'A python interface to the mpv media player', url = 'https://github.com/jaseg/python-mpv',