diff --git a/batchflow/plotter/plot.py b/batchflow/plotter/plot.py index bba584351..06980f0ba 100644 --- a/batchflow/plotter/plot.py +++ b/batchflow/plotter/plot.py @@ -5,7 +5,7 @@ from itertools import cycle from numbers import Number from warnings import warn -from multiprocessing import Process +from threading import Thread import numpy as np @@ -36,9 +36,9 @@ def _wrapper(*args, **kwargs): detach = kwargs.get('detach', False) if detach is True: - process = Process(target=func, args=args, kwargs=kwargs, - daemon=True, name=f'daemon_for_{func.__qualname__}') - process.start() + thread = Thread(target=func, args=args, kwargs=kwargs, + daemon=True, name=f'daemon_for_{func.__qualname__}') + thread.start() return None result = func(*args, **kwargs) @@ -1691,9 +1691,9 @@ def save(self, **kwargs): if savepath: if detach: - process = Process(target=self.figure.savefig, kwargs={'fname': savepath, **save_config}, - daemon=True, name=f'daemon_for_{self.save.__qualname__}') - process.start() + thread = Thread(target=self.figure.savefig, kwargs={'fname': savepath, **save_config}, + daemon=True, name=f'daemon_for_{self.save.__qualname__}') + thread.start() else: self.figure.savefig(fname=savepath, **save_config) diff --git a/batchflow/tests/detachable_test.py b/batchflow/tests/detachable_test.py new file mode 100644 index 000000000..d5e9279f4 --- /dev/null +++ b/batchflow/tests/detachable_test.py @@ -0,0 +1,57 @@ +"""Test that the detachable decorator and Plot.save work with detach=True. + +The original implementation used multiprocessing.Process which fails on Python 3.14 +due to a pickling error: `@wraps(func)` preserves `__qualname__`, so pickle resolves +`Plot.plot` by name and finds the wrapper instead of the original function. + +Replacing Process with Thread avoids pickling entirely — figure saving doesn't need +process isolation. +""" + +import os +import tempfile + +import numpy as np + + +def test_detachable_plot_with_detach(): + """Plot.plot with detach=True should complete without PicklingError.""" + from batchflow.plotter.plot import Plot + + data = np.random.rand(10, 10) + with tempfile.TemporaryDirectory() as tmpdir: + savepath = os.path.join(tmpdir, "test_detach.png") + p = Plot(data=data, mode="image", show=False, detach=True, savepath=savepath) + # detach=True runs in a daemon thread — give it a moment to finish + import time + time.sleep(1) + # The plot object should have been created without error + assert p is not None + + +def test_plot_save_with_detach(): + """Plot.save with detach=True should complete without PicklingError.""" + from batchflow.plotter.plot import Plot + + data = np.random.rand(10, 10) + with tempfile.TemporaryDirectory() as tmpdir: + savepath = os.path.join(tmpdir, "test_save_detach.png") + p = Plot(data=data, mode="image", show=False, savepath=savepath) + assert os.path.exists(savepath) + + savepath2 = os.path.join(tmpdir, "test_save_detach2.png") + p.save(savepath=savepath2, detach=True) + import time + time.sleep(1) + assert os.path.exists(savepath2) + + +def test_detachable_plot_without_detach(): + """Plot.plot with detach=False (default) should work as before.""" + from batchflow.plotter.plot import Plot + + data = np.random.rand(10, 10) + with tempfile.TemporaryDirectory() as tmpdir: + savepath = os.path.join(tmpdir, "test_no_detach.png") + Plot(data=data, mode="image", show=False, savepath=savepath) + assert os.path.exists(savepath)