Skip to content

Commit 2d13991

Browse files
Change process to thread (#825)
1 parent ccbf95b commit 2d13991

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

batchflow/plotter/plot.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from itertools import cycle
66
from numbers import Number
77
from warnings import warn
8-
from multiprocessing import Process
8+
from threading import Thread
99

1010
import numpy as np
1111

@@ -36,9 +36,9 @@ def _wrapper(*args, **kwargs):
3636
detach = kwargs.get('detach', False)
3737

3838
if detach is True:
39-
process = Process(target=func, args=args, kwargs=kwargs,
40-
daemon=True, name=f'daemon_for_{func.__qualname__}')
41-
process.start()
39+
thread = Thread(target=func, args=args, kwargs=kwargs,
40+
daemon=True, name=f'daemon_for_{func.__qualname__}')
41+
thread.start()
4242
return None
4343

4444
result = func(*args, **kwargs)
@@ -1691,9 +1691,9 @@ def save(self, **kwargs):
16911691

16921692
if savepath:
16931693
if detach:
1694-
process = Process(target=self.figure.savefig, kwargs={'fname': savepath, **save_config},
1695-
daemon=True, name=f'daemon_for_{self.save.__qualname__}')
1696-
process.start()
1694+
thread = Thread(target=self.figure.savefig, kwargs={'fname': savepath, **save_config},
1695+
daemon=True, name=f'daemon_for_{self.save.__qualname__}')
1696+
thread.start()
16971697
else:
16981698
self.figure.savefig(fname=savepath, **save_config)
16991699

batchflow/tests/detachable_test.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Test that the detachable decorator and Plot.save work with detach=True.
2+
3+
The original implementation used multiprocessing.Process which fails on Python 3.14
4+
due to a pickling error: `@wraps(func)` preserves `__qualname__`, so pickle resolves
5+
`Plot.plot` by name and finds the wrapper instead of the original function.
6+
7+
Replacing Process with Thread avoids pickling entirely — figure saving doesn't need
8+
process isolation.
9+
"""
10+
11+
import os
12+
import tempfile
13+
14+
import numpy as np
15+
16+
17+
def test_detachable_plot_with_detach():
18+
"""Plot.plot with detach=True should complete without PicklingError."""
19+
from batchflow.plotter.plot import Plot
20+
21+
data = np.random.rand(10, 10)
22+
with tempfile.TemporaryDirectory() as tmpdir:
23+
savepath = os.path.join(tmpdir, "test_detach.png")
24+
p = Plot(data=data, mode="image", show=False, detach=True, savepath=savepath)
25+
# detach=True runs in a daemon thread — give it a moment to finish
26+
import time
27+
time.sleep(1)
28+
# The plot object should have been created without error
29+
assert p is not None
30+
31+
32+
def test_plot_save_with_detach():
33+
"""Plot.save with detach=True should complete without PicklingError."""
34+
from batchflow.plotter.plot import Plot
35+
36+
data = np.random.rand(10, 10)
37+
with tempfile.TemporaryDirectory() as tmpdir:
38+
savepath = os.path.join(tmpdir, "test_save_detach.png")
39+
p = Plot(data=data, mode="image", show=False, savepath=savepath)
40+
assert os.path.exists(savepath)
41+
42+
savepath2 = os.path.join(tmpdir, "test_save_detach2.png")
43+
p.save(savepath=savepath2, detach=True)
44+
import time
45+
time.sleep(1)
46+
assert os.path.exists(savepath2)
47+
48+
49+
def test_detachable_plot_without_detach():
50+
"""Plot.plot with detach=False (default) should work as before."""
51+
from batchflow.plotter.plot import Plot
52+
53+
data = np.random.rand(10, 10)
54+
with tempfile.TemporaryDirectory() as tmpdir:
55+
savepath = os.path.join(tmpdir, "test_no_detach.png")
56+
Plot(data=data, mode="image", show=False, savepath=savepath)
57+
assert os.path.exists(savepath)

0 commit comments

Comments
 (0)