123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- 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
- # 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)
- @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.
- """
- # 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
- 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
- @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__
|