Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-python-mpv for openSUSE:Factory checked in at 2023-02-28 12:48:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-python-mpv (Old) and /work/SRC/openSUSE:Factory/.python-python-mpv.new.31432 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-mpv" Tue Feb 28 12:48:41 2023 rev:19 rq:1068003 version:1.0.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-python-mpv/python-python-mpv.changes 2023-02-27 12:55:22.047422153 +0100 +++ /work/SRC/openSUSE:Factory/.python-python-mpv.new.31432/python-python-mpv.changes 2023-02-28 12:49:02.588636741 +0100 @@ -1,0 +2,6 @@ +Mon Feb 27 11:45:28 UTC 2023 - Luigi Baldoni <aloi...@gmx.com> + +- Update to version 1.0.3 + * Move to new old pypi project name + +------------------------------------------------------------------- Old: ---- python-mpv-1.0.2.tar.gz New: ---- python-mpv-1.0.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-python-mpv.spec ++++++ --- /var/tmp/diff_new_pack.Tl3XLE/_old 2023-02-28 12:49:03.172640536 +0100 +++ /var/tmp/diff_new_pack.Tl3XLE/_new 2023-02-28 12:49:03.180640588 +0100 @@ -17,7 +17,7 @@ Name: python-python-mpv -Version: 1.0.2 +Version: 1.0.3 Release: 0 Summary: Python interface to the mpv media player License: GPL-2.0-or-later OR LGPL-2.1-or-later ++++++ python-mpv-1.0.2.tar.gz -> python-mpv-1.0.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/PKG-INFO new/python-mpv-1.0.3/PKG-INFO --- old/python-mpv-1.0.2/PKG-INFO 2023-02-26 15:12:58.527600800 +0100 +++ new/python-mpv-1.0.3/PKG-INFO 2023-02-27 10:31:30.520204800 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-mpv -Version: 1.0.2 +Version: 1.0.3 Summary: A python interface to the mpv media player Author-email: jaseg <m...@jaseg.de> License: GPLv2+ or LGPLv2.1+ @@ -40,7 +40,7 @@ .. code:: bash - pip install python-mpv + pip install mpv ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/README.rst new/python-mpv-1.0.3/README.rst --- old/python-mpv-1.0.2/README.rst 2023-02-26 13:13:42.000000000 +0100 +++ new/python-mpv-1.0.3/README.rst 2023-02-27 10:26:59.000000000 +0100 @@ -11,7 +11,7 @@ .. code:: bash - pip install python-mpv + pip install mpv ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/mpv.py new/python-mpv-1.0.3/mpv.py --- old/python-mpv-1.0.2/mpv.py 2023-02-26 13:13:42.000000000 +0100 +++ new/python-mpv-1.0.3/mpv.py 2023-02-27 10:26:59.000000000 +0100 @@ -20,6 +20,7 @@ from ctypes import * import ctypes.util import threading +import queue import os import sys from warnings import warn @@ -880,6 +881,7 @@ self.register_stream_protocol('python', self._python_stream_open) self._python_streams = {} self._python_stream_catchall = None + self._exception_futures = set() self.overlay_ids = set() self.overlays = {} if loglevel is not None or log_handler is not None: @@ -891,6 +893,20 @@ else: self._event_thread = None + @contextmanager + def _enqueue_exceptions(self): + try: + yield + except Exception as e: + for fut in self._exception_futures: + try: + fut.set_exception(e) + break + except InvalidStateError: + pass + else: + warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning) + def _loop(self): for event in _event_generator(self._event_handle): try: @@ -901,45 +917,51 @@ self._core_shutdown = True for callback in self._event_callbacks: - callback(event) + with self._enqueue_exceptions(): + callback(event) if eid == MpvEventID.PROPERTY_CHANGE: pc = event.data name, value, _fmt = pc.name, pc.value, pc.format for handler in self._property_handlers[name]: - handler(name, value) + with self._enqueue_exceptions(): + handler(name, value) if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None: ev = event.data - self._log_handler(ev.level, ev.prefix, ev.text) + with self._enqueue_exceptions(): + 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 = event.data.args target = target.decode("utf-8") if target in self._message_handlers: - self._message_handlers[target](*args) + with self._enqueue_exceptions(): + self._message_handlers[target](*args) if eid == MpvEventID.COMMAND_REPLY: key = event.reply_userdata callback = self._command_reply_callbacks.pop(key, None) if callback: - callback(ErrorCode.exception_for_ec(event.error), event.data) + with self._enqueue_exceptions(): + callback(ErrorCode.exception_for_ec(event.error), event.data) if eid == MpvEventID.QUEUE_OVERFLOW: # cache list, since error handlers will unregister themselves for cb in list(self._command_reply_callbacks.values()): - cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None) + with self._enqueue_exceptions(): + cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None) if eid == MpvEventID.SHUTDOWN: _mpv_destroy(self._event_handle) for cb in list(self._command_reply_callbacks.values()): - cb(ShutdownError('libmpv core has been shutdown'), None) + with self._enqueue_exceptions(): + cb(ShutdownError('libmpv core has been shutdown'), None) return except Exception as e: - print('Exception inside python-mpv event loop:', file=sys.stderr) - traceback.print_exc() + warn(f'Unhandled {e} inside python-mpv event loop!\n{traceback.format_exc()}', RuntimeWarning) @property def core_shutdown(self): @@ -953,35 +975,35 @@ if self._core_shutdown: raise ShutdownError('libmpv core has been shutdown') - def wait_until_paused(self, timeout=None): + def wait_until_paused(self, timeout=None, catch_errors=True): """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', timeout=timeout) + self.wait_for_property('core-idle', timeout=timeout, catch_errors=catch_errors) - def wait_for_playback(self, timeout=None): + def wait_for_playback(self, timeout=None, catch_errors=True): """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', timeout=timeout) + self.wait_for_event('end_file', timeout=timeout, catch_errors=catch_errors) - def wait_until_playing(self, timeout=None): + def wait_until_playing(self, timeout=None, catch_errors=True): """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, timeout=timeout) + self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout, catch_errors=catch_errors) - def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None): + def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=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. Raises a ShutdownError when the core is shutdown while waiting. """ - with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout) as result: + with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout, catch_errors=catch_errors) as result: pass return result.result() - def wait_for_shutdown(self, timeout=None): + def wait_for_shutdown(self, timeout=None, catch_errors=True): '''Wait for core to shutdown (e.g. through quit() or terminate()).''' try: - self.wait_for_event(None, timeout=timeout) + self.wait_for_event(None, timeout=timeout, catch_errors=catch_errors) except ShutdownError: return @@ -999,7 +1021,7 @@ return shutdown_handler.unregister_mpv_events @contextmanager - def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None): + def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=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. Re-raises any errors inside ``cond``. @@ -1023,6 +1045,9 @@ try: result.set_running_or_notify_cancel() + if catch_errors: + self._exception_futures.add(result) + yield result rv = cond(getattr(self, name.replace('-', '_'))) @@ -1035,18 +1060,19 @@ finally: err_unregister() self.unobserve_property(name, observer) + self._exception_futures.discard(result) - def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None): + def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=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. Re-raises any error inside ``cond``. """ - with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout) as result: + with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout, catch_errors=catch_errors) as result: pass return result.result() @contextmanager - def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None): + def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=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. Re-raises any error inside ``cond``. @@ -1064,7 +1090,6 @@ @self.event_callback(*event_types) def target_handler(evt): - try: rv = cond(evt) if rv: @@ -1081,13 +1106,18 @@ try: result.set_running_or_notify_cancel() + if catch_errors: + self._exception_futures.add(result) + yield result + self.check_core_alive() result.result(timeout) finally: err_unregister() target_handler.unregister_mpv_events() + self._exception_futures.discard(result) def __del__(self): if self.handle: @@ -1772,32 +1802,68 @@ frontend = open_fn(uri.decode('utf-8')) except ValueError: return ErrorCode.LOADING_FAILED + except Exception as e: + for fut in self._exception_futures: + try: + fut.set_exception(e) + break + except InvalidStateError: + pass + else: + warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}') + - def read_backend(_userdata, buf, bufsize): - data = frontend.read(bufsize) - for i in range(len(data)): - buf[i] = data[i] - return len(data) + + return ErrorCode.LOADING_FAILED cb_info.contents.cookie = None + + def read_backend(_userdata, buf, bufsize): + with self._enqueue_exceptions(): + data = frontend.read(bufsize) + for i in range(len(data)): + buf[i] = data[i] + return len(data) + return -1 read = cb_info.contents.read = StreamReadFn(read_backend) - close = cb_info.contents.close = StreamCloseFn(lambda _userdata: frontend.close()) + + def close_backend(_userdata): + with self._enqueue_exceptions(): + del self._stream_protocol_frontends[proto][uri] + if hasattr(frontend, 'close'): + frontend.close() + close = cb_info.contents.close = StreamCloseFn(close_backend) seek, size, cancel = None, None, None + if hasattr(frontend, 'seek'): - seek = cb_info.contents.seek = StreamSeekFn(lambda _userdata, offx: frontend.seek(offx)) + def seek_backend(_userdata, offx): + with self._enqueue_exceptions(): + return frontend.seek(offx) + return ErrorCode.GENERIC + seek = cb_info.contents.seek = StreamSeekFn(seek_backend) + if hasattr(frontend, 'size') and frontend.size is not None: - size = cb_info.contents.size = StreamSizeFn(lambda _userdata: frontend.size) + def size_backend(_userdata): + with self._enqueue_exceptions(): + return frontend.size + return 0 + size = cb_info.contents.size = StreamSizeFn(size_backend) + if hasattr(frontend, 'cancel'): - cancel = cb_info.contents.cancel = StreamCancelFn(lambda _userdata: frontend.cancel()) + def cancel_backend(_userdata): + with self._enqueue_exceptions(): + frontend.cancel() + cancel = cb_info.contents.cancel = StreamCancelFn(cancel_backend) - # keep frontend and callbacks in memory forever (TODO) + # keep frontend and callbacks in memory until closed frontend._registered_callbacks = [read, close, seek, size, cancel] self._stream_protocol_frontends[proto][uri] = frontend return 0 if proto in self._stream_protocol_cbs: raise KeyError('Stream protocol already registered') + # keep backend in memory forever self._stream_protocol_cbs[proto] = [open_backend] _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/pyproject.toml new/python-mpv-1.0.3/pyproject.toml --- old/python-mpv-1.0.2/pyproject.toml 2023-02-26 14:06:41.000000000 +0100 +++ new/python-mpv-1.0.3/pyproject.toml 2023-02-27 10:31:22.000000000 +0100 @@ -7,7 +7,7 @@ [project] name = "python-mpv" -version = "v1.0.2" +version = "v1.0.3" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "m...@jaseg.de"}] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/python_mpv.egg-info/PKG-INFO new/python-mpv-1.0.3/python_mpv.egg-info/PKG-INFO --- old/python-mpv-1.0.2/python_mpv.egg-info/PKG-INFO 2023-02-26 15:12:58.000000000 +0100 +++ new/python-mpv-1.0.3/python_mpv.egg-info/PKG-INFO 2023-02-27 10:31:30.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-mpv -Version: 1.0.2 +Version: 1.0.3 Summary: A python interface to the mpv media player Author-email: jaseg <m...@jaseg.de> License: GPLv2+ or LGPLv2.1+ @@ -40,7 +40,7 @@ .. code:: bash - pip install python-mpv + pip install mpv ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-mpv-1.0.2/tests/test_mpv.py new/python-mpv-1.0.3/tests/test_mpv.py --- old/python-mpv-1.0.2/tests/test_mpv.py 2023-02-26 13:13:42.000000000 +0100 +++ new/python-mpv-1.0.3/tests/test_mpv.py 2023-02-27 10:26:59.000000000 +0100 @@ -535,6 +535,115 @@ m.terminate() disp.stop() + def test_stream_open_exception(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo, video=False) + + @m.register_stream_protocol('raiseerror') + def open_fn(uri): + raise SystemError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(False) + except SystemError: + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('raiseerror://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + def test_python_stream_exception(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo) + + @m.python_stream('foo') + def foo_gen(): + with open(TESTVID, 'rb') as f: + yield f.read(100) + raise SystemError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(False) + except SystemError: + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('python://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + def test_stream_open_forward(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo, video=False) + + @m.register_stream_protocol('raiseerror') + def open_fn(uri): + raise ValueError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('raiseerror://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + class TestLifecycle(unittest.TestCase): def test_create_destroy(self):