From 5128d7bcc60378918f59e95bdb264a6bc01bf4bf Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Thu, 17 Sep 2026 04:59:09 +0530 Subject: [PATCH 1/2] Guard multiprocessing logging by its required start method --- fancylog/fancylog.py | 10 +++++++--- tests/tests/test_general.py | 39 +++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/fancylog/fancylog.py b/fancylog/fancylog.py index 737c9fa..3f350e3 100644 --- a/fancylog/fancylog.py +++ b/fancylog/fancylog.py @@ -3,8 +3,8 @@ import contextlib import json import logging +import multiprocessing import os -import platform import shutil import subprocess import sys @@ -65,6 +65,8 @@ def start_logging( Header for the log file, if the args are written. multiprocessing_aware Log from multiple processes. Default: True + Requires the 'fork' start method; otherwise warns and uses + ordinary logging. write_header Write a header for the log file. Default: True write_git @@ -600,6 +602,8 @@ def setup_logging( Logging level for file output. Default is 'DEBUG'. multiprocessing_aware If True, enables multiprocessing-safe logging. Default is True. + Requires the 'fork' start method; otherwise warns and uses + ordinary logging. log_to_console If True, logs will also be printed to the console. Default is True. logger_name @@ -618,9 +622,9 @@ def setup_logging( "must be performed with the root logger." ) - if multiprocessing_aware and platform.system() == "Windows": + if multiprocessing_aware and multiprocessing.get_start_method() != "fork": warnings.warn( - "Multiprocessing logging is not supported on Windows. " + "Multiprocessing logging requires the 'fork' start method. " "It has been disabled.", UserWarning, stacklevel=2, diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index b2e6c42..3683bcb 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -488,14 +488,15 @@ def test_mock_no_environment(tmp_path): assert f"{'pytest':20} {'1.1.1'}" -def test_multiprocessing_warning_on_windows(tmp_path): - """A warning is raised and multiprocessing logging - is disabled on Windows. - """ +@pytest.mark.parametrize("start_method", ["spawn", "forkserver"]) +def test_multiprocessing_warning_without_fork(tmp_path, start_method): + """Unsupported start methods fall back to ordinary logging.""" + mp_logging = MagicMock() with ( - patch("platform.system", return_value="Windows"), + patch("multiprocessing.get_start_method", return_value=start_method), + patch.dict(sys.modules, {"multiprocessing_logging": mp_logging}), pytest.warns( - UserWarning, match="Multiprocessing logging is not supported" + UserWarning, match="Multiprocessing logging requires.*fork" ), ): fancylog.start_logging( @@ -503,3 +504,29 @@ def test_multiprocessing_warning_on_windows(tmp_path): fancylog, multiprocessing_aware=True, ) + mp_logging.install_mp_handler.assert_not_called() + log_file = next(tmp_path.glob("*.log")) + assert "Not logging multiple processes" in log_file.read_text() + + +def test_multiprocessing_with_fork(tmp_path): + """The supported start method still installs the handler.""" + mp_logging = MagicMock() + with ( + patch("multiprocessing.get_start_method", return_value="fork"), + patch.dict(sys.modules, {"multiprocessing_logging": mp_logging}), + ): + fancylog.start_logging(tmp_path, fancylog, multiprocessing_aware=True) + mp_logging.install_mp_handler.assert_called_once_with() + + +def test_multiprocessing_opt_out(tmp_path): + """Opting out does not inspect the context or install a handler.""" + mp_logging = MagicMock() + with ( + patch("multiprocessing.get_start_method") as get_start_method, + patch.dict(sys.modules, {"multiprocessing_logging": mp_logging}), + ): + fancylog.start_logging(tmp_path, fancylog, multiprocessing_aware=False) + get_start_method.assert_not_called() + mp_logging.install_mp_handler.assert_not_called() From 486a5068a7930463911ef2916755e105d7675416 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Sat, 19 Sep 2026 08:58:17 +0530 Subject: [PATCH 2/2] Read the start method without fixing the global context multiprocessing.get_start_method() without allow_none sets the default context as a side effect, so a later set_start_method() call made by the user after start_logging() would raise RuntimeError. Read it with allow_none=True and fall back to the platform default. Co-Authored-By: Claude Opus 5 --- fancylog/fancylog.py | 15 ++++++++++++++- tests/tests/test_general.py | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/fancylog/fancylog.py b/fancylog/fancylog.py index 3f350e3..5e7f686 100644 --- a/fancylog/fancylog.py +++ b/fancylog/fancylog.py @@ -580,6 +580,19 @@ def initialise_logger( return logger +def _start_method(): + """Return the multiprocessing start method without fixing it. + + ``multiprocessing.get_start_method()`` without ``allow_none`` sets the + default context as a side effect, so a later ``set_start_method()`` call + by the user would raise. The first supported method is the default. + """ + return ( + multiprocessing.get_start_method(allow_none=True) + or multiprocessing.get_all_start_methods()[0] + ) + + def setup_logging( filename, print_level="INFO", @@ -622,7 +635,7 @@ def setup_logging( "must be performed with the root logger." ) - if multiprocessing_aware and multiprocessing.get_start_method() != "fork": + if multiprocessing_aware and _start_method() != "fork": warnings.warn( "Multiprocessing logging requires the 'fork' start method. " "It has been disabled.", diff --git a/tests/tests/test_general.py b/tests/tests/test_general.py index 3683bcb..624a974 100644 --- a/tests/tests/test_general.py +++ b/tests/tests/test_general.py @@ -5,6 +5,7 @@ import shutil import subprocess import sys +import warnings from unittest.mock import MagicMock, patch import pytest @@ -530,3 +531,29 @@ def test_multiprocessing_opt_out(tmp_path): fancylog.start_logging(tmp_path, fancylog, multiprocessing_aware=False) get_start_method.assert_not_called() mp_logging.install_mp_handler.assert_not_called() + + +@pytest.mark.parametrize( + "default_methods, installed", + [(["fork", "spawn"], True), (["spawn", "fork"], False)], +) +def test_multiprocessing_unset_start_method_uses_default( + tmp_path, default_methods, installed +): + """An unset start method is read without fixing the global context.""" + mp_logging = MagicMock() + with ( + patch( + "multiprocessing.get_start_method", return_value=None + ) as get_start_method, + patch( + "multiprocessing.get_all_start_methods", + return_value=default_methods, + ), + patch.dict(sys.modules, {"multiprocessing_logging": mp_logging}), + warnings.catch_warnings(), + ): + warnings.simplefilter("ignore", UserWarning) + fancylog.start_logging(tmp_path, fancylog, multiprocessing_aware=True) + get_start_method.assert_called_once_with(allow_none=True) + assert mp_logging.install_mp_handler.called == installed