| | 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__ |
| |
|