Spaces:
Running
Running
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: | |
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 | |
# 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__ = () | |
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) | |
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``. | |
""" | |
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. | |
""" | |
# 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 | |
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) | |
# 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 | |
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 | |
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__ | |