Repository: spulec/freezegun

Issue:
[Discussion] about freezing asyncio
PR https://github.com/spulec/freezegun/pull/470 basically unfroze asyncio during freeze_time.

Although i understand the need for it (not breaking tests that perform real waits on real events), it breaks the main purpose of freeze_time: Asyncio is no-longer frozen even without tick and i can no longer trigger events that would happen in an asyncio future without waiting the full duration( for instance, a common pattern is to do asyncio.sleep(remaining_time)). This makes some tests that were instant have to wait for whole timeout instead with freezegun>=1.3.0.

I understand that both properties may be desirable, but, the first one should only work with tick=True (it is currently inconditional), and may still manipulate monotonic for asyncio (it is desirable to trigger futures events with freezegun). 

A switch to get one behaviour or another may be needed; i guess there are even case where monotonic and time may need to be handled in a completely othogonal manner (as manipulating monotonic *will* always end up breaking a fundamental promise of the api: being monotonic).

So should i make a PR to make this behavior opt-in ? Add a new switch for asyncio ?

(Note, i have a longer take on this on a similar (but note quite the same) issue in time-machine: https://github.com/adamchainz/time-machine/pull/406)

Comments:

Patch:
diff --git a/freezegun/api.py b/freezegun/api.py
index f732ff8..2917fa1 100644
--- a/freezegun/api.py
+++ b/freezegun/api.py
@@ -544,7 +544,7 @@ class StepTickTimeFactory:
 
 class _freeze_time:
 
-    def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds):
+    def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds, real_asyncio):
         self.time_to_freeze = _parse_time_to_freeze(time_to_freeze_str)
         self.tz_offset = _parse_tz_offset(tz_offset)
         self.ignore = tuple(ignore)
@@ -554,6 +554,7 @@ class _freeze_time:
         self.modules_at_start = set()
         self.as_arg = as_arg
         self.as_kwarg = as_kwarg
+        self.real_asyncio = real_asyncio
 
     def __call__(self, func):
         if inspect.isclass(func):
@@ -727,20 +728,21 @@ class _freeze_time:
                         setattr(module, attribute_name, fake)
                         add_change((module, attribute_name, attribute_value))
 
-        # To avoid breaking `asyncio.sleep()`, let asyncio event loops see real
-        # monotonic time even though we've just frozen `time.monotonic()` which
-        # is normally used there. If we didn't do this, `await asyncio.sleep()`
-        # would be hanging forever breaking many tests that use `freeze_time`.
-        #
-        # Note that we cannot statically tell the class of asyncio event loops
-        # because it is not officially documented and can actually be changed
-        # at run time using `asyncio.set_event_loop_policy`. That's why we check
-        # the type by creating a loop here and destroying it immediately.
-        event_loop = asyncio.new_event_loop()
-        event_loop.close()
-        EventLoopClass = type(event_loop)
-        add_change((EventLoopClass, "time", EventLoopClass.time))
-        EventLoopClass.time = lambda self: real_monotonic()
+        if self.real_asyncio:
+            # To avoid breaking `asyncio.sleep()`, let asyncio event loops see real
+            # monotonic time even though we've just frozen `time.monotonic()` which
+            # is normally used there. If we didn't do this, `await asyncio.sleep()`
+            # would be hanging forever breaking many tests that use `freeze_time`.
+            #
+            # Note that we cannot statically tell the class of asyncio event loops
+            # because it is not officially documented and can actually be changed
+            # at run time using `asyncio.set_event_loop_policy`. That's why we check
+            # the type by creating a loop here and destroying it immediately.
+            event_loop = asyncio.new_event_loop()
+            event_loop.close()
+            EventLoopClass = type(event_loop)
+            add_change((EventLoopClass, "time", EventLoopClass.time))
+            EventLoopClass.time = lambda self: real_monotonic()
 
         return freeze_factory
 
@@ -830,7 +832,7 @@ class _freeze_time:
 
 
 def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_arg=False, as_kwarg='',
-                auto_tick_seconds=0):
+                auto_tick_seconds=0, real_asyncio=False):
     acceptable_times = (type(None), str, datetime.date, datetime.timedelta,
              types.FunctionType, types.GeneratorType)
 
@@ -845,14 +847,14 @@ def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_ar
         raise SystemError('Calling freeze_time with tick=True is only compatible with CPython')
 
     if isinstance(time_to_freeze, types.FunctionType):
-        return freeze_time(time_to_freeze(), tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds)
+        return freeze_time(time_to_freeze(), tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds, real_asyncio=real_asyncio)
 
     if isinstance(time_to_freeze, types.GeneratorType):
-        return freeze_time(next(time_to_freeze), tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds)
+        return freeze_time(next(time_to_freeze), tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds, real_asyncio=real_asyncio)
 
     if MayaDT is not None and isinstance(time_to_freeze, MayaDT):
         return freeze_time(time_to_freeze.datetime(), tz_offset, ignore,
-                           tick, as_arg, as_kwarg, auto_tick_seconds)
+                           tick, as_arg, as_kwarg, auto_tick_seconds, real_asyncio=real_asyncio)
 
     if ignore is None:
         ignore = []
@@ -868,6 +870,7 @@ def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_ar
         as_arg=as_arg,
         as_kwarg=as_kwarg,
         auto_tick_seconds=auto_tick_seconds,
+        real_asyncio=real_asyncio,
     )
 
 
diff --git a/freezegun/api.pyi b/freezegun/api.pyi
index f0efbda..2ff9bdc 100644
--- a/freezegun/api.pyi
+++ b/freezegun/api.pyi
@@ -34,6 +34,7 @@ class _freeze_time:
         as_arg: bool,
         as_kwarg: str,
         auto_tick_seconds: float,
+        real_asyncio: bool,
     ) -> None: ...
     @overload
     def __call__(self, func: type[_T]) -> type[_T]: ...
@@ -57,4 +58,5 @@ def freeze_time(
     as_arg: bool | None = ...,
     as_kwarg: str | None = ...,
     auto_tick_seconds: float | None = ...,
+    real_asyncio: bool | None = ...
 ) -> _freeze_time: ...


Test patch:
diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py
index fe0d10c..6afc6e3 100644
--- a/tests/test_asyncio.py
+++ b/tests/test_asyncio.py
@@ -43,7 +43,7 @@ def test_asyncio_sleeping_not_affected_by_freeze_time():
     async def coroutine():
         # Sleeping with time frozen should sleep the expected duration.
         before_sleep = time.time()
-        with freeze_time('1970-01-02'):
+        with freeze_time('1970-01-02', real_asyncio=True):
             await asyncio.sleep(0.05)
         assert 0.02 <= time.time() - before_sleep < 0.3
 
@@ -76,5 +76,5 @@ def test_asyncio_to_call_later_with_frozen_time():
         await asyncio.sleep(0.15)
         assert timestamps == [86400]
 
-    with freeze_time('1970-01-02'):
+    with freeze_time('1970-01-02', real_asyncio=True):
         asyncio.run(coroutine())
diff --git a/tests/test_configure.py b/tests/test_configure.py
index 9dc0806..32fd2d4 100644
--- a/tests/test_configure.py
+++ b/tests/test_configure.py
@@ -31,6 +31,7 @@ def test_default_ignore_list_is_overridden():
             as_arg=False,
             as_kwarg='',
             auto_tick_seconds=0,
+            real_asyncio=False,
         )
 
 def test_extend_default_ignore_list():
@@ -64,4 +65,5 @@ def test_extend_default_ignore_list():
             as_arg=False,
             as_kwarg='',
             auto_tick_seconds=0,
+            real_asyncio=False,
         )