Skip to content
Open
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
69 changes: 69 additions & 0 deletions freezegun/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dateutil
import datetime
import functools
import os
import sys
import time
import uuid
Expand Down Expand Up @@ -579,6 +580,53 @@ def move_to(self, target_datetime: _Freezable) -> None:
self.tick(delta=delta)




def _reapply_freeze_in_child() -> None:
"""Re-apply freezegun patches after fork (Python 3.14+ may use forkserver/spawn)."""
if not freeze_factories:
return
datetime.datetime = FakeDatetime # type: ignore[misc]
datetime.date = FakeDate # type: ignore[misc]
time.time = fake_time
time.monotonic = fake_monotonic
time.perf_counter = fake_perf_counter
time.localtime = fake_localtime # type: ignore
time.gmtime = fake_gmtime # type: ignore
time.strftime = fake_strftime # type: ignore
if _TIME_NS_PRESENT:
time.time_ns = fake_time_ns
if _MONOTONIC_NS_PRESENT:
time.monotonic_ns = fake_monotonic_ns
if _PERF_COUNTER_NS_PRESENT:
time.perf_counter_ns = fake_perf_counter_ns


def _ensure_fork_start_method() -> Optional[str]:
"""On Unix, prefer fork so children inherit frozen time (3.13 behavior).

Python 3.14 switched the default Linux start method toward forkserver/spawn,
which starts a fresh interpreter and loses freezegun patches (#593).
"""
if sys.platform == "win32":
return None
try:
import multiprocessing as mp
except Exception:
return None
try:
previous = mp.get_start_method(allow_none=True)
except Exception:
previous = None
if previous == "fork":
return None
try:
mp.set_start_method("fork", force=True)
return previous if previous is not None else ""
except Exception:
return None


class _freeze_time:
"""
A class to freeze time for testing purposes.
Expand Down Expand Up @@ -631,6 +679,8 @@ def __init__(
self.as_arg = as_arg
self.as_kwarg = as_kwarg
self.real_asyncio = real_asyncio
self._mp_previous_start_method: Optional[str] = None
self._mp_atfork_registered = False

# mypy objects to this because Type is Callable, but Pytype needs it because
# (unlike mypy's) its inference does not assume class decorators always leave
Expand Down Expand Up @@ -755,6 +805,16 @@ def start(self) -> Union[StepTickTimeFactory, TickingDateTimeFactory, FrozenDate
ignore_lists.append(self.ignore)
tick_flags.append(self.tick)

if not is_already_started:
# Inherit freeze across Process on Linux (issue #593 / Py3.14 forkserver default)
self._mp_previous_start_method = _ensure_fork_start_method()
if hasattr(os, "register_at_fork") and not getattr(_reapply_freeze_in_child, "_registered", False):
try:
os.register_at_fork(after_in_child=_reapply_freeze_in_child)
_reapply_freeze_in_child._registered = True # type: ignore[attr-defined]
except Exception:
pass

if is_already_started:
return freeze_factory

Expand Down Expand Up @@ -856,6 +916,15 @@ def stop(self) -> None:
tz_offsets.pop()

if not freeze_factories:
if self._mp_previous_start_method is not None:
try:
import multiprocessing as mp
# empty string means method was unset before; best-effort restore
if self._mp_previous_start_method:
mp.set_start_method(self._mp_previous_start_method, force=True)
except Exception:
pass
self._mp_previous_start_method = None
datetime.datetime = real_datetime # type: ignore[misc]
datetime.date = real_date # type: ignore[misc]
copyreg.dispatch_table.pop(real_datetime)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_multiprocess_freeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import datetime
import multiprocessing as mp
import sys

from freezegun import freeze_time


class _QueueProcess(mp.Process):
def __init__(self, data_queue: mp.Queue):
self._data_queue = data_queue
mp.Process.__init__(self)

def run(self) -> None:
self._data_queue.put(datetime.datetime.now())


@freeze_time("2012-01-14")
def test_freeze_time_propagates_to_process_child():
"""Regression #593: Py3.14 default start method broke child freeze inheritance."""
if sys.platform == "win32":
return
queue = mp.Queue()
process = _QueueProcess(queue)
process.start()
result = queue.get(timeout=10)
process.join(timeout=10)
assert result == datetime.datetime(2012, 1, 14)