|
from six import PY2 |
|
|
|
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 |
|
""" |
|
if PY2: |
|
@wraps(namefunc) |
|
def adjust_encoding(*args, **kwargs): |
|
name = namefunc(*args, **kwargs) |
|
if name is not None: |
|
name = name.encode() |
|
|
|
return name |
|
|
|
return adjust_encoding |
|
else: |
|
return namefunc |
|
|
|
|
|
|
|
|
|
if hasattr(datetime, 'fold'): |
|
|
|
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__ = () |
|
|
|
def replace(self, *args, **kwargs): |
|
""" |
|
Return a datetime with the same attributes, except for those |
|
attributes given new values by whichever keyword arguments are |
|
specified. Note that tzinfo=None can be specified to create a naive |
|
datetime from an aware datetime with no conversion of date and time |
|
data. |
|
|
|
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will |
|
return a ``datetime.datetime`` even if ``fold`` is unchanged. |
|
""" |
|
argnames = ( |
|
'year', 'month', 'day', 'hour', 'minute', 'second', |
|
'microsecond', 'tzinfo' |
|
) |
|
|
|
for arg, argname in zip(args, argnames): |
|
if argname in kwargs: |
|
raise TypeError('Duplicate argument: {}'.format(argname)) |
|
|
|
kwargs[argname] = arg |
|
|
|
for argname in argnames: |
|
if argname not in kwargs: |
|
kwargs[argname] = getattr(self, argname) |
|
|
|
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime |
|
|
|
return dt_class(**kwargs) |
|
|
|
@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 |
|
occurrence, chronologically, of the ambiguous datetime). |
|
|
|
:param dt: |
|
A timezone-aware :class:`datetime.datetime` object. |
|
""" |
|
|
|
|
|
dtoff = dt.utcoffset() |
|
if dtoff is None: |
|
raise ValueError("fromutc() requires a non-None utcoffset() " |
|
"result") |
|
|
|
|
|
|
|
|
|
dtdst = dt.dst() |
|
if dtdst is None: |
|
raise ValueError("fromutc() requires a non-None dst() result") |
|
delta = dtoff - dtdst |
|
|
|
dt += delta |
|
|
|
|
|
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 |
|
occurrence, chronologically, of the ambiguous datetime). |
|
|
|
:param dt: |
|
A timezone-aware :class:`datetime.datetime` object. |
|
""" |
|
dt_wall = self._fromutc(dt) |
|
|
|
|
|
_fold = self._fold_status(dt, dt_wall) |
|
|
|
|
|
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") |
|
|
|
|
|
transitions = self.transitions(dt.year) |
|
if transitions is None: |
|
return dt + self.utcoffset(dt) |
|
|
|
|
|
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) |
|
|
|
|
|
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__ |
|
|