Search code examples
pythonpython-importpython-module

How to make statement `import datetime` bind the datetime type instead of the datetime module?


After one too many times having accidentally typed import datetime when what was really needed was from datetime import datetime, I wondered whether it was possible to hack around and make the former do the latter.

That is, to recreate this behaviour (in a freshly opened interpreter session):

$ python -ic ''
>>> import datetime
>>> datetime(2016, 5, 27)
datetime.datetime(2016, 5, 27, 0, 0)

Came pretty close to faking a "callable module" below:

>>> import dt
>>> dt(2016, 5, 27)
datetime.datetime(2016, 5, 27, 0, 0)

Which was implemented like this:

# dt.py
import sys
import datetime

class CallableModule(object):
    def __init__(self, thing):
        self.thing = thing
    def __call__(self, *args, **kwargs):
        return self.thing.__call__(*args, **kwargs)

sys.modules['dt'] = CallableModule(datetime.datetime)

However it doesn't work if I try to use the filename datetime.py for the module, and I could not yet find any hack to get at the built-in datetime module when my own file was also called datetime.py.

How can we temporarily unhide a built-in or site-packages module, from within the shadowing module itself? Is there any indirect way to get at the core datetime under this situation (perhaps similar to how we can still access sys.__stdout__ even when sys.stdout has been redirected)?

Disclaimer: In no way suggesting that this is a sane idea - just interested if it's possible.


Solution

  • Here we go:

    datetime.py:

    import sys
    import imp
    import os
    
    path = [p for p in sys.path if p != os.path.dirname(__file__)]
    f, pathname, desc = imp.find_module('datetime', path)
    std_datetime = imp.load_module('datetime', f, pathname, desc)
    # if this^ is renamed to datetime, everything breaks!
    f.close()
    
    class CallableModule(object):
    
        def __init__(self, module, fn):
            self.module = module
            self.fn = fn
    
        def __call__(self, *args, **kwargs):
            return self.fn(*args, **kwargs)
    
        def __getattr__(self, item):
            try:
                return getattr(self.fn, item)
            except AttributeError:
                return getattr(self.module, item)
    
    sys.modules['datetime'] = CallableModule(std_datetime, std_datetime.datetime)
    

    test.py (lives next to datetime.py):

    import datetime
    
    print(datetime(1, 2, 3))
    print(datetime.timedelta(days=1))
    print(datetime.now())
    

    Run test.py, output:

    0001-02-03 00:00:00
    1 day, 0:00:00
    2016-05-27 23:16:30.954270
    

    It also works with from datetime import datetime, timedelta etc.

    This is especially hacky and fragile and will depend on your distribution. For example, apparently it doesn't work with IPython. You must import datetime.py before the standard library module.

    To understand just how weird things get with this, if the variable std_datetime (the datetime module object) is renamed to datetime, then datetime.datetime is no longer the class, but rather datetime is datetime.datetime is datetime.datetime.datetime .... If someone can explain why this happens, I'd love to hear it.

    (note that the first comment below is from before I got to the final version)