Viewing file: _common.py (11.71 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
from six import PY3
from functools import wraps
from datetime import datetime, timedelta, tzinfo
ZERO = timedelta(0)
__all__ = ['tzname_in_python2', 'enfold']
def tzname_in_python2(namefunc): """Change unicode output into bytestrings in Python 2
tzname() API changed in Python 3. It used to return bytes, but was changed to unicode strings """ def adjust_encoding(*args, **kwargs): name = namefunc(*args, **kwargs) if name is not None and not PY3: name = name.encode()
return name
return adjust_encoding
# The following is adapted from Alexander Belopolsky's tz library # https://github.com/abalkin/tz if hasattr(datetime, 'fold'): # This is the pre-python 3.6 fold situation def enfold(dt, fold=1): """ Provides a unified interface for assigning the ``fold`` attribute to datetimes both before and after the implementation of PEP-495.
:param fold: The value for the ``fold`` attribute in the returned datetime. This should be either 0 or 1.
:return: Returns an object for which ``getattr(dt, 'fold', 0)`` returns ``fold`` for all versions of Python. In versions prior to Python 3.6, this is a ``_DatetimeWithFold`` object, which is a subclass of :py:class:`datetime.datetime` with the ``fold`` attribute added, if ``fold`` is 1.
.. versionadded:: 2.6.0 """ return dt.replace(fold=fold)
else: class _DatetimeWithFold(datetime): """ This is a class designed to provide a PEP 495-compliant interface for Python versions before 3.6. It is used only for dates in a fold, so the ``fold`` attribute is fixed at ``1``.
.. versionadded:: 2.6.0 """ __slots__ = ()
@property def fold(self): return 1
def enfold(dt, fold=1): """ Provides a unified interface for assigning the ``fold`` attribute to datetimes both before and after the implementation of PEP-495.
:param fold: The value for the ``fold`` attribute in the returned datetime. This should be either 0 or 1.
:return: Returns an object for which ``getattr(dt, 'fold', 0)`` returns ``fold`` for all versions of Python. In versions prior to Python 3.6, this is a ``_DatetimeWithFold`` object, which is a subclass of :py:class:`datetime.datetime` with the ``fold`` attribute added, if ``fold`` is 1.
.. versionadded:: 2.6.0 """ if getattr(dt, 'fold', 0) == fold: return dt
args = dt.timetuple()[:6] args += (dt.microsecond, dt.tzinfo)
if fold: return _DatetimeWithFold(*args) else: return datetime(*args)
def _validate_fromutc_inputs(f): """ The CPython version of ``fromutc`` checks that the input is a ``datetime`` object and that ``self`` is attached as its ``tzinfo``. """ @wraps(f) def fromutc(self, dt): if not isinstance(dt, datetime): raise TypeError("fromutc() requires a datetime argument") if dt.tzinfo is not self: raise ValueError("dt.tzinfo is not self")
return f(self, dt)
return fromutc
class _tzinfo(tzinfo): """ Base class for all ``dateutil`` ``tzinfo`` objects. """
def is_ambiguous(self, dt): """ Whether or not the "wall time" of a given datetime is ambiguous in this zone.
:param dt: A :py:class:`datetime.datetime`, naive or time zone aware.
:return: Returns ``True`` if ambiguous, ``False`` otherwise.
.. versionadded:: 2.6.0 """
dt = dt.replace(tzinfo=self)
wall_0 = enfold(dt, fold=0) wall_1 = enfold(dt, fold=1)
same_offset = wall_0.utcoffset() == wall_1.utcoffset() same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
return same_dt and not same_offset
def _fold_status(self, dt_utc, dt_wall): """ Determine the fold status of a "wall" datetime, given a representation of the same datetime as a (naive) UTC datetime. This is calculated based on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all datetimes, and that this offset is the actual number of hours separating ``dt_utc`` and ``dt_wall``.
:param dt_utc: Representation of the datetime as UTC
:param dt_wall: Representation of the datetime as "wall time". This parameter must either have a `fold` attribute or have a fold-naive :class:`datetime.tzinfo` attached, otherwise the calculation may fail. """ if self.is_ambiguous(dt_wall): delta_wall = dt_wall - dt_utc _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) else: _fold = 0
return _fold
def _fold(self, dt): return getattr(dt, 'fold', 0)
def _fromutc(self, dt): """ Given a timezone-aware datetime in a given timezone, calculates a timezone-aware datetime in a new timezone.
Since this is the one time that we *know* we have an unambiguous datetime object, we take this opportunity to determine whether the datetime is ambiguous and in a "fold" state (e.g. if it's the first occurence, chronologically, of the ambiguous datetime).
:param dt: A timezone-aware :class:`datetime.datetime` object. """
# Re-implement the algorithm from Python's datetime.py dtoff = dt.utcoffset() if dtoff is None: raise ValueError("fromutc() requires a non-None utcoffset() " "result")
# The original datetime.py code assumes that `dst()` defaults to # zero during ambiguous times. PEP 495 inverts this presumption, so # for pre-PEP 495 versions of python, we need to tweak the algorithm. dtdst = dt.dst() if dtdst is None: raise ValueError("fromutc() requires a non-None dst() result") delta = dtoff - dtdst
dt += delta # Set fold=1 so we can default to being in the fold for # ambiguous dates. dtdst = enfold(dt, fold=1).dst() if dtdst is None: raise ValueError("fromutc(): dt.dst gave inconsistent " "results; cannot convert") return dt + dtdst
@_validate_fromutc_inputs def fromutc(self, dt): """ Given a timezone-aware datetime in a given timezone, calculates a timezone-aware datetime in a new timezone.
Since this is the one time that we *know* we have an unambiguous datetime object, we take this opportunity to determine whether the datetime is ambiguous and in a "fold" state (e.g. if it's the first occurance, chronologically, of the ambiguous datetime).
:param dt: A timezone-aware :class:`datetime.datetime` object. """ dt_wall = self._fromutc(dt)
# Calculate the fold status given the two datetimes. _fold = self._fold_status(dt, dt_wall)
# Set the default fold value for ambiguous dates return enfold(dt_wall, fold=_fold)
class tzrangebase(_tzinfo): """ This is an abstract base class for time zones represented by an annual transition into and out of DST. Child classes should implement the following methods:
* ``__init__(self, *args, **kwargs)`` * ``transitions(self, year)`` - this is expected to return a tuple of datetimes representing the DST on and off transitions in standard time.
A fully initialized ``tzrangebase`` subclass should also provide the following attributes: * ``hasdst``: Boolean whether or not the zone uses DST. * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects representing the respective UTC offsets. * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short abbreviations in DST and STD, respectively. * ``_hasdst``: Whether or not the zone has DST.
.. versionadded:: 2.6.0 """ def __init__(self): raise NotImplementedError('tzrangebase is an abstract base class')
def utcoffset(self, dt): isdst = self._isdst(dt)
if isdst is None: return None elif isdst: return self._dst_offset else: return self._std_offset
def dst(self, dt): isdst = self._isdst(dt)
if isdst is None: return None elif isdst: return self._dst_base_offset else: return ZERO
@tzname_in_python2 def tzname(self, dt): if self._isdst(dt): return self._dst_abbr else: return self._std_abbr
def fromutc(self, dt): """ Given a datetime in UTC, return local time """ if not isinstance(dt, datetime): raise TypeError("fromutc() requires a datetime argument")
if dt.tzinfo is not self: raise ValueError("dt.tzinfo is not self")
# Get transitions - if there are none, fixed offset transitions = self.transitions(dt.year) if transitions is None: return dt + self.utcoffset(dt)
# Get the transition times in UTC dston, dstoff = transitions
dston -= self._std_offset dstoff -= self._std_offset
utc_transitions = (dston, dstoff) dt_utc = dt.replace(tzinfo=None)
isdst = self._naive_isdst(dt_utc, utc_transitions)
if isdst: dt_wall = dt + self._dst_offset else: dt_wall = dt + self._std_offset
_fold = int(not isdst and self.is_ambiguous(dt_wall))
return enfold(dt_wall, fold=_fold)
def is_ambiguous(self, dt): """ Whether or not the "wall time" of a given datetime is ambiguous in this zone.
:param dt: A :py:class:`datetime.datetime`, naive or time zone aware.
:return: Returns ``True`` if ambiguous, ``False`` otherwise.
.. versionadded:: 2.6.0 """ if not self.hasdst: return False
start, end = self.transitions(dt.year)
dt = dt.replace(tzinfo=None) return (end <= dt < end + self._dst_base_offset)
def _isdst(self, dt): if not self.hasdst: return False elif dt is None: return None
transitions = self.transitions(dt.year)
if transitions is None: return False
dt = dt.replace(tzinfo=None)
isdst = self._naive_isdst(dt, transitions)
# Handle ambiguous dates if not isdst and self.is_ambiguous(dt): return not self._fold(dt) else: return isdst
def _naive_isdst(self, dt, transitions): dston, dstoff = transitions
dt = dt.replace(tzinfo=None)
if dston < dstoff: isdst = dston <= dt < dstoff else: isdst = not dstoff <= dt < dston
return isdst
@property def _dst_base_offset(self): return self._dst_offset - self._std_offset
__hash__ = None
def __ne__(self, other): return not (self == other)
def __repr__(self): return "%s(...)" % self.__class__.__name__
__reduce__ = object.__reduce__
def _total_seconds(td): # Python 2.6 doesn't have a total_seconds() method on timedelta objects return ((td.seconds + td.days * 86400) * 1000000 + td.microseconds) // 1000000
_total_seconds = getattr(timedelta, 'total_seconds', _total_seconds)
|