Skip to content

Commit a1c5d8d

Browse files
Change process to thread
1 parent f2b361b commit a1c5d8d

2 files changed

Lines changed: 65 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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
import pytest
16+
17+
18+
def test_detachable_plot_with_detach():
19+
"""Plot.plot with detach=True should complete without PicklingError."""
20+
from batchflow.plotter.plot import Plot
21+
22+
data = np.random.rand(10, 10)
23+
with tempfile.TemporaryDirectory() as tmpdir:
24+
savepath = os.path.join(tmpdir, "test_detach.png")
25+
p = Plot(data=data, mode="image", show=False, detach=True, savepath=savepath)
26+
# detach=True runs in a daemon thread — give it a moment to finish
27+
import time
28+
time.sleep(1)
29+
# The plot object should have been created without error
30+
assert p is not None
31+
32+
33+
def test_plot_save_with_detach():
34+
"""Plot.save with detach=True should complete without PicklingError."""
35+
from batchflow.plotter.plot import Plot
36+
37+
data = np.random.rand(10, 10)
38+
with tempfile.TemporaryDirectory() as tmpdir:
39+
savepath = os.path.join(tmpdir, "test_save_detach.png")
40+
p = Plot(data=data, mode="image", show=False, savepath=savepath)
41+
assert os.path.exists(savepath)
42+
43+
savepath2 = os.path.join(tmpdir, "test_save_detach2.png")
44+
p.save(savepath=savepath2, detach=True)
45+
import time
46+
time.sleep(1)
47+
assert os.path.exists(savepath2)
48+
49+
50+
def test_detachable_plot_without_detach():
51+
"""Plot.plot with detach=False (default) should work as before."""
52+
from batchflow.plotter.plot import Plot
53+
54+
data = np.random.rand(10, 10)
55+
with tempfile.TemporaryDirectory() as tmpdir:
56+
savepath = os.path.join(tmpdir, "test_no_detach.png")
57+
p = Plot(data=data, mode="image", show=False, savepath=savepath)
58+
assert os.path.exists(savepath)

0 commit comments

Comments
 (0)