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
24 changes: 12 additions & 12 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 All @@ -27,7 +27,7 @@

# Decorators
def detachable(func):
""" Run `func` in a daemon process without result return.
""" Run `func` in a daemon thread without result return.

Note, the decorator intercept the `detach` argument from the `func`.
"""
Expand All @@ -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()
Comment on lines +39 to +41
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detachable decorator docstring still says it runs the function in a “daemon process”, but the implementation now uses threading.Thread. Please update the docstring (and any related docs in this file describing detach) to reflect the new threading behavior so users don’t get misled about isolation/semantics.

Copilot uses AI. Check for mistakes.
return None

result = func(*args, **kwargs)
Expand Down Expand Up @@ -952,10 +952,10 @@ class Plot:
If False, every time `plot` is called update config with provided keyword arguments, replacing older parameters.
If True, fix plotter config as provided on initialization. Usefull, if one want to reuse this config on updates.
detach : {True, False, 'save'}, default: False
Whether to use run `plot` in a daemon process.
If False, then don't use any daemon processes.
If True, then run :meth:`~.plot` in a daemon process.
If 'save', then run :meth:`~.save` (called from the :meth:`~.plot`) in a daemon process.
Whether to use run `plot` in a daemon thread.
If False, then don't use any daemon threads.
If True, then run :meth:`~.plot` in a daemon thread.
If 'save', then run :meth:`~.save` (called from the :meth:`~.plot`) in a daemon thread.
kwargs :
- For one of `image`, `histogram`, `curve`, `loss` methods of `Layer` (depending on chosen mode).
Parameters and data nestedness levels must match if they are lists meant for differents subplots/layers.
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
62 changes: 62 additions & 0 deletions batchflow/tests/detachable_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""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 time

import numpy as np


def _wait_for_file(path, timeout=5):
"""Poll until `path` exists or `timeout` seconds elapse."""
deadline = time.monotonic() + timeout
while not os.path.exists(path) and time.monotonic() < deadline:
time.sleep(0.05)


def test_detachable_plot_with_detach():
"""Plot.plot with detach='save' 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='save', savepath=savepath)
_wait_for_file(savepath)
assert p is not None
assert os.path.exists(savepath)


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)
_wait_for_file(savepath2)
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)
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "batchflow"
version = "0.10.0"
version = "0.10.1"
description = "ML pipelines, model configuration and batch management"
authors = [{ name = "Roman Kh", email = "rhudor@gmail.com" }]
license = {text = "Apache License 2.0"}
Expand Down Expand Up @@ -31,7 +31,7 @@ dependencies = [
[project.optional-dependencies]

image = [
"pillow>=9.4,<11.0",
"pillow>=9.4,<13.0",
"matplotlib>=3.7"
]

Expand Down Expand Up @@ -71,7 +71,7 @@ jupyter = [
]

telegram = [
"pillow>=9.4,<11.0",
"pillow>=9.4,<13.0",
]

safetensors = [
Expand Down
Loading
Loading