Sometimes there's a need (testing, debugging, re-running yesterday's report, …) to "pretend" that program is being run at a different point in time than what clock currently shows. Easiest/lamest approach is to just set the system clock to that time and be done with it (not really applicable to a production machine or a server). Another approach is to add another command line argument to an app and pass it around to all likes of
datetime.now() or similar but that can quickly become very messy. Simplest would be to fake the current system time just for that app.
TL;DR: code is at https://bitbucket.org/igor_b/timefake/
Disclaimer: only after I finished writing the code I checked online if the "faketime" name is available and immediately found https://github.com/wolfcw/libfaketime which is exactly what I wanted but much better and not limited to Python.
Even though this work turned out to be quite useless I learned some new things about monkey patching in Python. So, here it is.
Monkey patching is a process of dynamically replacing object/class/module methods and it can easily lead to chaos. But, nevertheless, it can be useful in some situations.
The goal here is to somehow specify some point in time and then let the time run normally (i.e. we don't want to "freeze" time). There are two time related modules in Python:
datetime and we'll need to take care of both of them.
/usr/lib/python3.7/datetime.py reveals that only methods related to getting the current time are
date.today(). All of them use
time.time() function so we only need to tell them to use our function. Problem with
datetime module is that it is built in so we can't just replace the method. But we can subclass the whole class and set the class name to point to our new class:
# define class class _mydatetime(datetime.datetime): @classmethod def now(cls, tz=None): t = time.time() return cls.fromtimestamp(t, tz) # replace it datetime.datetime = _mydatetime
The same would fo for other methods. Class
datetime also uses time module locally aliased as
_time so we need to patch the time module first and then replace
By looking at the documentation we can see that functions related to getting the current time are
ctime(). All of them need to be replaced (they all use
time.time() internally but our change will not "propagate" to the so we'll need to redefine those functions). I found it easiest to just store the original version of the function as a function's attribute and use it later. For example, the time() function looks like this:
def _mytime(offset=0): if not hasattr(_mytime, 'time'): _mytime.time = time.time _mytime.offset = offset return _mytime.time()-_mytime.offset time.time = _mytime
But there is one issue I encountered by accident. When testing I tried all those functions and also the
logging module configured to display
"%asctime" format. And it raised an exception telling me that asctime requires one argument but two were provided.
Traceback (most recent call last): ... File "/usr/lib/python3.7/logging/__init__.py", line 557, in formatTime ct = self.converter(record.created) TypeError: _mylocaltime() takes from 0 to 1 positional arguments but 2 were given
I checked the
logging module's code and saw that in the
Logging class it takes a reference to
time.asctime()). Andd the two arguments it complains about are the Logging class (hmmmm… self?) and the time. The thing I learned is following (anyone, please correct me if I got something wrong):
time module's functions are built-in functions (done in C) and they always stay that "type" (even after assigning to a class and instantiating an object from that class)
function I wrote is just a regular function. But when it's assigned to a class and an object is created from that class, the function becomes a bound method (i.e. object method) and when called, first passed argument is
If my function was defined as static this wouldn't happen. But I didn't found the way to define a function which should be static after assigning to class and when used as a regular function (
staticmethod() doesn't work here).
So, to get around that I made all functions accept variable number of arguments (actually two would be enough but…) and if two arguments are received the second one is used (first one is self).
def _mylocaltime(*args): if not hasattr(_mylocaltime, 'base'): _mylocaltime.base = time.localtime if len(args)==1: t=args elif len(args)==2: t=args else: t = time.time() return _mylocaltime.base(t)
The full code is at https://bitbucket.org/igor_b/timefake/