-
-
Save misakar/2edda53ad298ab404f973b812190447e to your computer and use it in GitHub Desktop.
Patching pytz with import hook to have usable generic timezones
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Import hook extending pytz package with usable, generic timezones: | |
GMT-14 up to GMT+12. | |
Note that pytz already has Etc/GMT+X timezones, but | |
(quoting Wikipedia): | |
"In order to conform with the POSIX style, those zones beginning with "Etc/GMT" | |
have their sign reversed from what most people expect. In this style, | |
zones west of GMT have a positive sign and those east have a negative sign." | |
Because this is insane, we are simply introducing our own zones | |
that will not have this fatal flaw. | |
""" | |
import sys | |
import imp | |
import functools | |
from datetime import timedelta | |
class PytzImporter(object): | |
"""Finder and loader class for the pytz module. | |
This is the main part of import hook that | |
will be executed specifically for pytz when it's | |
about to be imported. | |
When it happens, we inject our generic timezones | |
into all relevant places inside pytz so that they | |
can be freely used alongside others. | |
""" | |
def find_module(self, fullname, path=None): | |
"""Module finding method. It tells Python to use our hook | |
only for the pytz package. | |
""" | |
if fullname == 'pytz': | |
self.path = path | |
return self | |
return None | |
def load_module(self, name): | |
"""Module loading method. It imports pytz normally | |
and then enhances it with our generic timezones. | |
""" | |
if name != 'pytz': | |
raise ImportError("%s can only be used to import pytz!", | |
self.__class__.__name__) | |
if name in sys.modules: | |
return sys.modules[name] # already imported | |
file_obj, pathname, desc = imp.find_module(name, self.path) | |
try: | |
pytz = imp.load_module(name, file_obj, pathname, desc) | |
finally: | |
if file_obj: | |
file_obj.close() | |
pytz = self.__enhance_pytz(pytz) | |
sys.modules[name] = pytz | |
return pytz | |
def __enhance_pytz(self, pytz): | |
"""Adds support for generic timezones (GMT+X) to pytz module. | |
Patch includes changing all_timezones list and set, as well | |
as modifying timezone() function. | |
""" | |
generic_tz = self.__get_generic_timezones() | |
# add to various collections | |
pytz.generic_timezones = list(generic_tz.iterkeys()) | |
pytz.generic_timezones_set = set(generic_tz.iterkeys()) | |
pytz.all_timezones.extend(generic_tz) | |
pytz.all_timezones_set = set(pytz.all_timezones) # has to be recreated | |
# patch pytz.timezone() | |
old__pytz_timezone = pytz.timezone | |
@functools.wraps(old__pytz_timezone) | |
def pytz_timezone(zone): | |
try: | |
return old__pytz_timezone(zone) | |
except IOError: # when pytz doesn't find match in its data file | |
if zone not in pytz.generic_timezones_set: | |
raise | |
tz = pytz.FixedOffset(generic_tz[zone]) | |
pytz._tzinfo_cache[zone] = tz | |
return tz | |
pytz.timezone = pytz_timezone | |
return pytz | |
def __get_generic_timezones(self): | |
"""Returns dictionary mapping names of our generic | |
GMT timezones to their offsets from UTC in minutes. | |
""" | |
span = range(-12, 14 + 1) | |
span.remove(0) # pytz alrady has GMT | |
return dict(('GMT%(sign)s%(offset)s' % { | |
'sign': '+' if i > 0 else '-', | |
'offset': abs(i), | |
}, timedelta(hours=i).total_seconds() // 60) | |
for i in span) | |
sys.meta_path = [PytzImporter()] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment