Repository: adamchainz/time-machine

Issue:
Flag that we're time traveling, with access to true datetime
### Description

In some of our integration tests we make API calls to external services. Some of these require a timestamp for use in authentication. I know that excluding some modules from time-machine isn't possible due to the implementation, but having some signal that we're time traveling would suffice, along with access to the unpatched datetime module.

Comments:

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 78e067b..7af039e 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,10 @@
 History
 =======
 
+* Add ``time_machine.escape_hatch``, which provides functions to bypass time-machine.
+
+  Thanks to Matt Pegler for the feature request in `Issue #206 <https://github.com/adamchainz/time-machine/issues/206>`__.
+
 2.4.1 (2021-11-27)
 ------------------
 
diff --git a/README.rst b/README.rst
index 4532090..f23ce06 100644
--- a/README.rst
+++ b/README.rst
@@ -310,6 +310,52 @@ For example:
 
         assert dt.date.today().isoformat() == "2015-10-21"
 
+``escape_hatch``
+----------------
+
+The ``escape_hatch`` object provides functions to bypass time-machine.
+These allow you to call the real datetime functions, without any mocking.
+It also provides a way to check if time-machine is currently time travelling.
+
+These capabilities are useful in rare circumstances.
+For example, if you need to authenticate with an external service during time travel, you may need the real value of ``datetime.now()``.
+
+The functions are:
+
+* ``escape_hatch.is_travelling() -> bool`` - returns ``True`` if ``time_machine.travel()`` is active, ``False`` otherwise.
+
+* ``escape_hatch.datetime.datetime.now()`` - wraps the real ``datetime.datetime.now()``.
+
+* ``escape_hatch.datetime.datetime.utcnow()`` - wraps the real ``datetime.datetime.utcnow()``.
+
+* ``escape_hatch.time.clock_gettime()`` - wraps the real ``time.clock_gettime()``.
+
+* ``escape_hatch.time.clock_gettime_ns()`` - wraps the real ``time.clock_gettime_ns()``.
+
+* ``escape_hatch.time.gmtime()`` - wraps the real ``time.gmtime()``.
+
+* ``escape_hatch.time.localtime()`` - wraps the real ``time.localtime()``.
+
+* ``escape_hatch.time.strftime()`` - wraps the real ``time.strftime()``.
+
+* ``escape_hatch.time.time()`` - wraps the real ``time.time()``.
+
+* ``escape_hatch.time.time_ns()`` - wraps the real ``time.time_ns()``.
+
+For example:
+
+.. code-block:: python
+
+    import time_machine
+
+
+    with time_machine.travel(...):
+        if time_machine.escape_hatch.is_travelling():
+            print("We need to go back to the future!")
+
+        real_now = time_machine.escape_hatch.datetime.datetime.now()
+        external_authenticate(now=real_now)
+
 Caveats
 =======
 
diff --git a/src/time_machine/__init__.py b/src/time_machine/__init__.py
index 0e67b1b..4c57870 100644
--- a/src/time_machine/__init__.py
+++ b/src/time_machine/__init__.py
@@ -77,6 +77,9 @@ DestinationType = Union[
     TypingGenerator[DestinationBaseType, None, None],
 ]
 
+# copied from typeshed:
+_TimeTuple = Tuple[int, int, int, int, int, int, int, int, int]
+
 
 def extract_timestamp_tzname(
     destination: DestinationType,
@@ -389,10 +392,6 @@ def localtime(secs: Optional[float] = None) -> struct_time:
     return _time_machine.original_localtime(coordinates_stack[-1].time())
 
 
-# copied from typeshed:
-_TimeTuple = Tuple[int, int, int, int, int, int, int, int, int]
-
-
 def strftime(format: str, t: Union[_TimeTuple, struct_time, None] = None) -> str:
     if t is not None:
         return _time_machine.original_strftime(format, t)
@@ -453,3 +452,63 @@ if pytest is not None:  # pragma: no branch
             yield fixture
         finally:
             fixture.stop()
+
+
+# escape hatch
+
+
+class _EscapeHatchDatetimeDatetime:
+    def now(self, tz: Optional[dt.tzinfo] = None) -> dt.datetime:
+        return _time_machine.original_now(tz)
+
+    def utcnow(self) -> dt.datetime:
+        return _time_machine.original_utcnow()
+
+
+class _EscapeHatchDatetime:
+    def __init__(self) -> None:
+        self.datetime = _EscapeHatchDatetimeDatetime()
+
+
+class _EscapeHatchTime:
+    def clock_gettime(self, clk_id: int) -> float:
+        return _time_machine.original_clock_gettime(clk_id)
+
+    if sys.version_info >= (3, 7):
+
+        def clock_gettime_ns(self, clk_id: int) -> int:
+            return _time_machine.original_clock_gettime_ns(clk_id)
+
+    def gmtime(self, secs: Optional[float] = None) -> struct_time:
+        return _time_machine.original_gmtime(secs)
+
+    def localtime(self, secs: Optional[float] = None) -> struct_time:
+        return _time_machine.original_localtime(secs)
+
+    def strftime(
+        self, format: str, t: Union[_TimeTuple, struct_time, None] = None
+    ) -> str:
+        if t is not None:
+            return _time_machine.original_strftime(format, t)
+        else:
+            return _time_machine.original_strftime(format)
+
+    def time(self) -> float:
+        return _time_machine.original_time()
+
+    if sys.version_info >= (3, 7):
+
+        def time_ns(self) -> int:
+            return _time_machine.original_time_ns()
+
+
+class _EscapeHatch:
+    def __init__(self) -> None:
+        self.datetime = _EscapeHatchDatetime()
+        self.time = _EscapeHatchTime()
+
+    def is_travelling(self) -> bool:
+        return bool(coordinates_stack)
+
+
+escape_hatch = _EscapeHatch()


Test patch:
diff --git a/tests/test_time_machine.py b/tests/test_time_machine.py
index a23a99e..8cb4f63 100644
--- a/tests/test_time_machine.py
+++ b/tests/test_time_machine.py
@@ -222,18 +222,18 @@ def test_time_localtime_arg():
         assert local_time.tm_mday == 1
 
 
-def test_time_strftime_no_args():
+def test_time_strftime_format():
     with time_machine.travel(EPOCH):
         assert time.strftime("%Y-%m-%d") == "1970-01-01"
     assert int(time.strftime("%Y")) >= 2020
 
 
-def test_time_strftime_no_args_no_tick():
+def test_time_strftime_format_no_tick():
     with time_machine.travel(EPOCH, tick=False):
         assert time.strftime("%S") == "00"
 
 
-def test_time_strftime_arg():
+def test_time_strftime_format_t():
     with time_machine.travel(EPOCH):
         assert (
             time.strftime("%Y-%m-%d", time.localtime(EPOCH_PLUS_ONE_YEAR))
@@ -718,3 +718,100 @@ def test_fixture_used_twice(time_machine):
 
     time_machine.move_to(EPOCH_PLUS_ONE_YEAR)
     assert time.time() == EPOCH_PLUS_ONE_YEAR
+
+
+# escape hatch tests
+
+
+class TestEscapeHatch:
+    def test_is_travelling_false(self):
+        assert time_machine.escape_hatch.is_travelling() is False
+
+    def test_is_travelling_true(self):
+        with time_machine.travel(EPOCH):
+            assert time_machine.escape_hatch.is_travelling() is True
+
+    def test_datetime_now(self):
+        real_now = dt.datetime.now()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.datetime.datetime.now()
+            assert eh_now >= real_now
+
+    def test_datetime_now_tz(self):
+        real_now = dt.datetime.now(tz=dt.timezone.utc)
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.datetime.datetime.now(tz=dt.timezone.utc)
+            assert eh_now >= real_now
+
+    def test_datetime_utcnow(self):
+        real_now = dt.datetime.utcnow()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.datetime.datetime.utcnow()
+            assert eh_now >= real_now
+
+    @py_have_clock_gettime
+    def test_time_clock_gettime(self):
+        now = time.clock_gettime(time.CLOCK_REALTIME)
+
+        with time_machine.travel(EPOCH + 180.0):
+            eh_now = time_machine.escape_hatch.time.clock_gettime(time.CLOCK_REALTIME)
+            assert eh_now >= now
+
+    @py_3_7_plus
+    @py_have_clock_gettime
+    def test_time_clock_gettime_ns(self):
+        now = time.clock_gettime_ns(time.CLOCK_REALTIME)
+
+        with time_machine.travel(EPOCH + 190.0):
+            eh_now = time_machine.escape_hatch.time.clock_gettime_ns(
+                time.CLOCK_REALTIME
+            )
+            assert eh_now >= now
+
+    def test_time_gmtime(self):
+        now = time.gmtime()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.time.gmtime()
+            assert eh_now >= now
+
+    def test_time_localtime(self):
+        now = time.localtime()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.time.localtime()
+            assert eh_now >= now
+
+    def test_time_strftime_no_arg(self):
+        today = dt.date.today()
+
+        with time_machine.travel(EPOCH):
+            eh_formatted = time_machine.escape_hatch.time.strftime("%Y-%m-%d")
+            eh_today = dt.datetime.strptime(eh_formatted, "%Y-%m-%d").date()
+            assert eh_today >= today
+
+    def test_time_strftime_arg(self):
+        with time_machine.travel(EPOCH):
+            formatted = time_machine.escape_hatch.time.strftime(
+                "%Y-%m-%d",
+                time.localtime(EPOCH_PLUS_ONE_YEAR),
+            )
+            assert formatted == "1971-01-01"
+
+    def test_time_time(self):
+        now = time.time()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.time.time()
+            assert eh_now >= now
+
+    @py_3_7_plus
+    def test_time_time_ns(self):
+        now = time.time_ns()
+
+        with time_machine.travel(EPOCH):
+            eh_now = time_machine.escape_hatch.time.time_ns()
+            assert eh_now >= now