From 812f5c4db0f13e8963af94770d0d037bfd74d3cf Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Wed, 4 Dec 2024 11:16:53 +0100 Subject: [PATCH 1/6] Fix version check timeout, add tests --- deeplabcut/gui/window.py | 29 +++++++++++++++++++++++++---- tests/gui/test_gui_window.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 tests/gui/test_gui_window.py diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index de40d326fa..b2f5c5121b 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -16,8 +16,8 @@ from pathlib import Path from typing import List from urllib.error import URLError -from concurrent.futures import ThreadPoolExecutor, TimeoutError import qdarkstyle +import multiprocessing import deeplabcut from deeplabcut import auxiliaryfunctions, VERSION, compat @@ -42,9 +42,30 @@ def call_with_timeout(func, timeout, *args, **kwargs): - with ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(func, *args, **kwargs) - return future.result(timeout=timeout) + def wrapper(queue, *args, **kwargs): + try: + result = func(*args, **kwargs) + queue.put(result) # Pass the result back via the queue + except Exception as e: + queue.put(e) # Pass any exception back via the queue + + queue = multiprocessing.Queue() + process = multiprocessing.Process(target=wrapper, args=(queue, *args), kwargs=kwargs) + process.start() + process.join(timeout) + + if process.is_alive(): + process.terminate() # Forcefully terminate the process + process.join() + raise TimeoutError(f"Function {func.__name__} did not complete within {timeout} seconds.") + + if not queue.empty(): + result = queue.get() + if isinstance(result, Exception): + raise result # Re-raise the exception if it occurred in the function + return result + else: + raise TimeoutError(f"Function {func.__name__} completed but did not return a result.") def _check_for_updates(silent=True): diff --git a/tests/gui/test_gui_window.py b/tests/gui/test_gui_window.py new file mode 100644 index 0000000000..b1f0565a83 --- /dev/null +++ b/tests/gui/test_gui_window.py @@ -0,0 +1,33 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import pytest +import time +from deeplabcut.gui.window import call_with_timeout + +def test_call_with_timeout(): + def succeeding_method(parameter): + return parameter + + parameter = (10, "Hello test") + assert call_with_timeout(succeeding_method, 1, parameter) == parameter + + def failing_method(): + raise ValueError("Raise value error on purpose") + + with pytest.raises(ValueError): + call_with_timeout(failing_method, timeout=1) + + def hanging_method(): + while True: + time.sleep(1) + + with pytest.raises(TimeoutError): + call_with_timeout(hanging_method, timeout=1) From ab55fd0cd1bb82a6f923bf51373da0033cc1175c Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Tue, 10 Dec 2024 17:49:57 +0100 Subject: [PATCH 2/6] fix 1 --- deeplabcut/gui/window.py | 29 +---------- deeplabcut/utils/multiprocessing.py | 50 +++++++++++++++++++ .../test_multiprocessing.py} | 2 +- 3 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 deeplabcut/utils/multiprocessing.py rename tests/{gui/test_gui_window.py => utils/test_multiprocessing.py} (93%) diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index b2f5c5121b..bec575c9b5 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -17,7 +17,6 @@ from typing import List from urllib.error import URLError import qdarkstyle -import multiprocessing import deeplabcut from deeplabcut import auxiliaryfunctions, VERSION, compat @@ -25,6 +24,7 @@ from deeplabcut.gui import BASE_DIR, components, utils from deeplabcut.gui.tabs import * from deeplabcut.gui.widgets import StreamReceiver, StreamWriter +from deeplabcut.utils.multiprocessing import call_with_timeout from napari_deeplabcut import misc from PySide6.QtWidgets import ( QMessageBox, @@ -41,33 +41,6 @@ from PySide6.QtCore import Qt, QTimer -def call_with_timeout(func, timeout, *args, **kwargs): - def wrapper(queue, *args, **kwargs): - try: - result = func(*args, **kwargs) - queue.put(result) # Pass the result back via the queue - except Exception as e: - queue.put(e) # Pass any exception back via the queue - - queue = multiprocessing.Queue() - process = multiprocessing.Process(target=wrapper, args=(queue, *args), kwargs=kwargs) - process.start() - process.join(timeout) - - if process.is_alive(): - process.terminate() # Forcefully terminate the process - process.join() - raise TimeoutError(f"Function {func.__name__} did not complete within {timeout} seconds.") - - if not queue.empty(): - result = queue.get() - if isinstance(result, Exception): - raise result # Re-raise the exception if it occurred in the function - return result - else: - raise TimeoutError(f"Function {func.__name__} completed but did not return a result.") - - def _check_for_updates(silent=True): try: is_latest, latest_version = call_with_timeout( diff --git a/deeplabcut/utils/multiprocessing.py b/deeplabcut/utils/multiprocessing.py new file mode 100644 index 0000000000..1d507fa7e5 --- /dev/null +++ b/deeplabcut/utils/multiprocessing.py @@ -0,0 +1,50 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +""" +DeepLabCut2.2 Toolbox (deeplabcut.org) +© A. & M. Mathis Labs +https://github.com/DeepLabCut/DeepLabCut +Please see AUTHORS for contributors. + +https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +Licensed under GNU Lesser General Public License v3.0 +""" +import multiprocessing + + + +def call_with_timeout(func, timeout, *args, **kwargs): + def wrapper(queue, *args, **kwargs): + try: + result = func(*args, **kwargs) + queue.put(result) # Pass the result back via the queue + except Exception as e: + queue.put(e) # Pass any exception back via the queue + + queue = multiprocessing.Queue() + process = multiprocessing.Process(target=wrapper, args=(queue, *args), kwargs=kwargs) + process.start() + process.join(timeout) + + if process.is_alive(): + process.terminate() # Forcefully terminate the process + process.join() + raise TimeoutError(f"Function {func.__name__} did not complete within {timeout} seconds.") + + if not queue.empty(): + result = queue.get() + if isinstance(result, Exception): + raise result # Re-raise the exception if it occurred in the function + return result + else: + raise TimeoutError(f"Function {func.__name__} completed but did not return a result.") + + diff --git a/tests/gui/test_gui_window.py b/tests/utils/test_multiprocessing.py similarity index 93% rename from tests/gui/test_gui_window.py rename to tests/utils/test_multiprocessing.py index b1f0565a83..53700a0704 100644 --- a/tests/gui/test_gui_window.py +++ b/tests/utils/test_multiprocessing.py @@ -10,7 +10,7 @@ # import pytest import time -from deeplabcut.gui.window import call_with_timeout +from deeplabcut.utils.multiprocessing import call_with_timeout def test_call_with_timeout(): def succeeding_method(parameter): From 0b2623df1b83fddc5d39c14b72a63b1d41f99fdd Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Tue, 10 Dec 2024 17:58:56 +0100 Subject: [PATCH 3/6] fix2 --- deeplabcut/utils/multiprocessing.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/deeplabcut/utils/multiprocessing.py b/deeplabcut/utils/multiprocessing.py index 1d507fa7e5..dae183f728 100644 --- a/deeplabcut/utils/multiprocessing.py +++ b/deeplabcut/utils/multiprocessing.py @@ -20,17 +20,16 @@ import multiprocessing +def _wrapper(func, queue, *args, **kwargs): + try: + result = func(*args, **kwargs) + queue.put(result) # Pass the result back via the queue + except Exception as e: + queue.put(e) # Pass any exception back via the queue def call_with_timeout(func, timeout, *args, **kwargs): - def wrapper(queue, *args, **kwargs): - try: - result = func(*args, **kwargs) - queue.put(result) # Pass the result back via the queue - except Exception as e: - queue.put(e) # Pass any exception back via the queue - queue = multiprocessing.Queue() - process = multiprocessing.Process(target=wrapper, args=(queue, *args), kwargs=kwargs) + process = multiprocessing.Process(target=_wrapper, args=(func, queue, *args), kwargs=kwargs) process.start() process.join(timeout) From 5dcc302922e73f9f83b71184f5924c67aa05bcde Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Tue, 10 Dec 2024 18:01:11 +0100 Subject: [PATCH 4/6] black --- deeplabcut/utils/multiprocessing.py | 15 ++++++++++----- tests/utils/test_multiprocessing.py | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/deeplabcut/utils/multiprocessing.py b/deeplabcut/utils/multiprocessing.py index dae183f728..3515b73125 100644 --- a/deeplabcut/utils/multiprocessing.py +++ b/deeplabcut/utils/multiprocessing.py @@ -27,16 +27,21 @@ def _wrapper(func, queue, *args, **kwargs): except Exception as e: queue.put(e) # Pass any exception back via the queue + def call_with_timeout(func, timeout, *args, **kwargs): queue = multiprocessing.Queue() - process = multiprocessing.Process(target=_wrapper, args=(func, queue, *args), kwargs=kwargs) + process = multiprocessing.Process( + target=_wrapper, args=(func, queue, *args), kwargs=kwargs + ) process.start() process.join(timeout) if process.is_alive(): process.terminate() # Forcefully terminate the process process.join() - raise TimeoutError(f"Function {func.__name__} did not complete within {timeout} seconds.") + raise TimeoutError( + f"Function {func.__name__} did not complete within {timeout} seconds." + ) if not queue.empty(): result = queue.get() @@ -44,6 +49,6 @@ def call_with_timeout(func, timeout, *args, **kwargs): raise result # Re-raise the exception if it occurred in the function return result else: - raise TimeoutError(f"Function {func.__name__} completed but did not return a result.") - - + raise TimeoutError( + f"Function {func.__name__} completed but did not return a result." + ) diff --git a/tests/utils/test_multiprocessing.py b/tests/utils/test_multiprocessing.py index 53700a0704..2485e78f06 100644 --- a/tests/utils/test_multiprocessing.py +++ b/tests/utils/test_multiprocessing.py @@ -12,6 +12,7 @@ import time from deeplabcut.utils.multiprocessing import call_with_timeout + def test_call_with_timeout(): def succeeding_method(parameter): return parameter From 072a24432e39c288d963ae6e6e26054c2a9adbec Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Thu, 12 Dec 2024 23:53:37 +0100 Subject: [PATCH 5/6] fix 3 --- tests/utils/test_multiprocessing.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/utils/test_multiprocessing.py b/tests/utils/test_multiprocessing.py index 2485e78f06..34333be81d 100644 --- a/tests/utils/test_multiprocessing.py +++ b/tests/utils/test_multiprocessing.py @@ -13,22 +13,25 @@ from deeplabcut.utils.multiprocessing import call_with_timeout -def test_call_with_timeout(): - def succeeding_method(parameter): - return parameter +def _succeeding_method(parameter): + return parameter - parameter = (10, "Hello test") - assert call_with_timeout(succeeding_method, 1, parameter) == parameter - def failing_method(): - raise ValueError("Raise value error on purpose") +def _failing_method(): + raise ValueError("Raise value error on purpose") + + +def _hanging_method(): + while True: + time.sleep(5) - with pytest.raises(ValueError): - call_with_timeout(failing_method, timeout=1) - def hanging_method(): - while True: - time.sleep(1) +def test_call_with_timeout(): + parameter = (10, "Hello test") + assert call_with_timeout(_succeeding_method, 30, parameter) == parameter + + with pytest.raises(ValueError): + call_with_timeout(_failing_method, timeout=30) with pytest.raises(TimeoutError): - call_with_timeout(hanging_method, timeout=1) + call_with_timeout(_hanging_method, timeout=1) From 84ff59990d14537868cc34c3084972be0a713b84 Mon Sep 17 00:00:00 2001 From: maximpavliv Date: Fri, 10 Jan 2025 08:16:20 +0100 Subject: [PATCH 6/6] increase timeout in version checks --- deeplabcut/gui/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index bec575c9b5..1e967fa4a2 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -44,10 +44,10 @@ def _check_for_updates(silent=True): try: is_latest, latest_version = call_with_timeout( - utils.is_latest_deeplabcut_version, 1 + utils.is_latest_deeplabcut_version, 5 ) is_latest_plugin, latest_plugin_version = call_with_timeout( - misc.is_latest_version, 1 + misc.is_latest_version, 5 ) except (URLError, TimeoutError): # Handle internet connectivity issues is_latest = is_latest_plugin = True