Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions batchflow/plotter/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
57 changes: 57 additions & 0 deletions batchflow/tests/detachable_test.py
Original file line number Diff line number Diff line change
@@ -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)
Loading