From 86086b900fefc46e9a0303b9503ce59b37dccb71 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Tue, 4 Sep 2018 21:58:18 +0100 Subject: [PATCH] =?UTF-8?q?Update=20DateUtil=202.7.2=20(ff03c0f)=20?= =?UTF-8?q?=E2=86=92=202.7.2=20(49690ee)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/dateutil/parser/_parser.py | 76 ++++++-- lib/dateutil/parser/isoparser.py | 35 ++-- lib/dateutil/relativedelta.py | 72 +++++--- lib/dateutil/rrule.py | 84 +++++++-- lib/dateutil/tz/__init__.py | 2 + lib/dateutil/tz/_common.py | 18 +- lib/dateutil/tz/_factories.py | 6 +- lib/dateutil/tz/tz.py | 301 ++++++++++++++++++++++++------- lib/dateutil/tz/win.py | 45 ++++- lib/dateutil/utils.py | 6 + 10 files changed, 488 insertions(+), 157 deletions(-) diff --git a/lib/dateutil/parser/_parser.py b/lib/dateutil/parser/_parser.py index 014050c3..24a18695 100644 --- a/lib/dateutil/parser/_parser.py +++ b/lib/dateutil/parser/_parser.py @@ -364,13 +364,23 @@ class parserinfo(object): return self.TZOFFSET.get(name) def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + if year < 100 and not century_specified: + # assume current century to start year += self._century - if abs(year - self._year) >= 50: - if year < self._year: - year += 100 - else: - year -= 100 + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + return year def validate(self, res): @@ -448,10 +458,37 @@ class _ymd(list): raise ValueError('Year is already set') self.ystridx = len(self) - 1 + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + def resolve_ymd(self, yearfirst, dayfirst): len_ymd = len(self) year, month, day = (None, None, None) + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + mstridx = self.mstridx if len_ymd > 3: @@ -460,13 +497,17 @@ class _ymd(list): # One member, or two members with a month string if mstridx is not None: month = self[mstridx] - del self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] if len_ymd > 1 or mstridx is None: - if self[0] > 31: - year = self[0] + if other > 31: + year = other else: - day = self[0] + day = other elif len_ymd == 2: # Two members with numbers @@ -1115,16 +1156,14 @@ class parser(object): tzdata = tzinfos(tzname, tzoffset) else: tzdata = tzinfos.get(tzname) - - if isinstance(tzdata, datetime.tzinfo): + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: tzinfo = tzdata elif isinstance(tzdata, text_type): tzinfo = tz.tzstr(tzdata) elif isinstance(tzdata, integer_types): tzinfo = tz.tzoffset(tzname, tzdata) - else: - raise ValueError("Offset must be tzinfo subclass, " - "tz string, or int offset.") return tzinfo def _build_tzaware(self, naive, res, tzinfos): @@ -1160,7 +1199,7 @@ class parser(object): warnings.warn("tzname {tzname} identified but not understood. " "Pass `tzinfos` argument in order to correctly " "return a timezone-aware datetime. In a future " - "version, this raise an " + "version, this will raise an " "exception.".format(tzname=res.tzname), category=UnknownTimezoneWarning) aware = naive @@ -1202,10 +1241,15 @@ class parser(object): def _to_decimal(self, val): try: - return Decimal(val) + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") except Exception as e: msg = "Could not convert %s to decimal" % val six.raise_from(ValueError(msg), e) + else: + return decimal_value DEFAULTPARSER = parser() diff --git a/lib/dateutil/parser/isoparser.py b/lib/dateutil/parser/isoparser.py index b08eb711..528b3df2 100644 --- a/lib/dateutil/parser/isoparser.py +++ b/lib/dateutil/parser/isoparser.py @@ -4,6 +4,8 @@ This module offers a parser for ISO-8601 strings It is intended to support all valid date, time and datetime formats per the ISO-8601 specification. + +..versionadded:: 2.7.0 """ from datetime import datetime, timedelta, time, date import calendar @@ -86,10 +88,12 @@ class isoparser(object): - ``hh`` - ``hh:mm`` or ``hhmm`` - ``hh:mm:ss`` or ``hhmmss`` - - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) Midnight is a special case for `hh`, as the standard supports both - 00:00 and 24:00 as a representation. + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + .. caution:: @@ -124,6 +128,8 @@ class isoparser(object): currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not guaranteed to continue failing in future versions if they encode a valid date. + + .. versionadded:: 2.7.0 """ components, pos = self._parse_isodate(dt_str) @@ -133,6 +139,10 @@ class isoparser(object): else: raise ValueError('String contains unknown ISO components') + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + return datetime(*components) @_takes_ascii @@ -163,7 +173,10 @@ class isoparser(object): :return: Returns a :class:`datetime.time` object """ - return time(*self._parse_isotime(timestr)) + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) @_takes_ascii def parse_tzstr(self, tzstr, zero_as_utc=True): @@ -186,10 +199,9 @@ class isoparser(object): return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) # Constants - _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') _DATE_SEP = b'-' _TIME_SEP = b':' - _MICRO_SEP = b'.' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') def _parse_isodate(self, dt_str): try: @@ -344,16 +356,14 @@ class isoparser(object): pos += 1 if comp == 3: - # Microsecond - if timestr[pos:pos + 1] != self._MICRO_SEP: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: continue - pos += 1 - us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], - 1)[0] - + us_str = frac.group(1)[:6] # Truncate to microseconds components[comp] = int(us_str) * 10**(6 - len(us_str)) - pos += len(us_str) + pos += len(frac.group()) if pos < len_str: raise ValueError('Unused components in ISO string') @@ -362,7 +372,6 @@ class isoparser(object): # Standard supports 00:00 and 24:00 as representations of midnight if any(component != 0 for component in components[1:4]): raise ValueError('Hour may only be 24 at 24:00:00.000') - components[0] = 0 return components diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py index e4394707..ec7f66d3 100644 --- a/lib/dateutil/relativedelta.py +++ b/lib/dateutil/relativedelta.py @@ -17,8 +17,12 @@ __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] class relativedelta(object): """ - The relativedelta type is based on the specification of the excellent - work done by M.-A. Lemburg in his + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his `mx.DateTime `_ extension. However, notice that this type does *NOT* implement the same algorithm as his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. @@ -44,12 +48,16 @@ class relativedelta(object): the corresponding aritmetic operation on the original datetime value with the information in the relativedelta. - weekday: - One of the weekday instances (MO, TU, etc). These instances may - receive a parameter N, specifying the Nth weekday, which could - be positive or negative (like MO(+1) or MO(-2). Not specifying - it is the same as specifying +1. You can also use an integer, - where 0=MO. + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). leapdays: Will add given days to the date found, if year is a leap @@ -59,33 +67,39 @@ class relativedelta(object): Set the yearday or the non-leap year day (jump leap days). These are converted to day/month/leapdays information. - Here is the behavior of operations with relativedelta: + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). - 1. Calculate the absolute year, using the 'year' argument, or the - original datetime year, if the argument is not present. + The order of attributes considered when this relativedelta is + added to a datetime is: - 2. Add the relative 'years' argument to the absolute year. + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds - 3. Do steps 1 and 2 for month/months. + Finally, weekday is applied, using the rule described above. - 4. Calculate the absolute day, using the 'day' argument, or the - original datetime day, if the argument is not present. Then, - subtract from the day until it fits in the year and month - found after their operations. + For example - 5. Add the relative 'days' argument to the absolute day. Notice - that the 'weeks' argument is multiplied by 7 and added to - 'days'. + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) - 6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, - microsecond/microseconds. + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. - 7. If the 'weekday' argument is present, calculate the weekday, - with the given (wday, nth) tuple. wday is the index of the - weekday (0-6, 0=Mon), and nth is the number of weeks to add - forward or backward, depending on its signal. Notice that if - the calculated date is already Monday, for example, using - (0, 1) or (0, -1) won't change the day. """ def __init__(self, dt1=None, dt2=None, @@ -271,7 +285,7 @@ class relativedelta(object): values for the relative attributes. >>> relativedelta(days=1.5, hours=2).normalized() - relativedelta(days=1, hours=14) + relativedelta(days=+1, hours=+14) :return: Returns a :class:`dateutil.relativedelta.relativedelta` object. diff --git a/lib/dateutil/rrule.py b/lib/dateutil/rrule.py index ff66cd5b..13788650 100644 --- a/lib/dateutil/rrule.py +++ b/lib/dateutil/rrule.py @@ -337,10 +337,6 @@ class rrule(rrulebase): Additionally, it supports the following keyword arguments: - :param cache: - If given, it must be a boolean value specifying to enable or disable - caching of results. If you will use the same rrule instance multiple - times, enabling caching will improve the performance considerably. :param dtstart: The recurrence start. Besides being the base for the recurrence, missing parameters in the final recurrence instances will also be @@ -357,20 +353,26 @@ class rrule(rrulebase): from calendar.firstweekday(), and may be modified by calendar.setfirstweekday(). :param count: - How many occurrences will be generated. + If given, this determines how many occurrences will be generated. .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param until: - If given, this must be a datetime instance, that will specify the + If given, this must be a datetime instance specifying the upper-bound limit of the recurrence. The last recurrence in the rule is the greatest datetime that is less than or equal to the value specified in the ``until`` parameter. .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param bysetpos: If given, it must be either an integer, or a sequence of integers, positive or negative. Each given integer will specify an occurrence @@ -387,6 +389,11 @@ class rrule(rrulebase): :param byyearday: If given, it must be either an integer, or a sequence of integers, meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. :param byweekno: If given, it must be either an integer, or a sequence of integers, meaning the week numbers to apply the recurrence to. Week numbers @@ -412,11 +419,10 @@ class rrule(rrulebase): :param bysecond: If given, it must be either an integer, or a sequence of integers, meaning the seconds to apply the recurrence to. - :param byeaster: - If given, it must be either an integer, or a sequence of integers, - positive or negative. Each integer will define an offset from the - Easter Sunday. Passing the offset 0 to byeaster will yield the Easter - Sunday itself. This is an extension to the RFC specification. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. """ def __init__(self, freq, dtstart=None, interval=1, wkst=None, count=None, until=None, bysetpos=None, @@ -427,7 +433,10 @@ class rrule(rrulebase): super(rrule, self).__init__(cache) global easter if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) elif not isinstance(dtstart, datetime.datetime): dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) else: @@ -1404,6 +1413,49 @@ class rruleset(rrulebase): class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ _freq_map = {"YEARLY": YEARLY, "MONTHLY": MONTHLY, diff --git a/lib/dateutil/tz/__init__.py b/lib/dateutil/tz/__init__.py index 4212d07d..628e2c74 100644 --- a/lib/dateutil/tz/__init__.py +++ b/lib/dateutil/tz/__init__.py @@ -1,4 +1,6 @@ +# -*- coding: utf-8 -*- from .tz import * +from .tz import __doc__ #: Convenience constant providing a :class:`tzutc()` instance #: diff --git a/lib/dateutil/tz/_common.py b/lib/dateutil/tz/_common.py index e478b3ea..5f15e9a2 100644 --- a/lib/dateutil/tz/_common.py +++ b/lib/dateutil/tz/_common.py @@ -1,4 +1,4 @@ -from six import PY3 +from six import PY2 from functools import wraps @@ -16,14 +16,18 @@ def tzname_in_python2(namefunc): 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() + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() - return name + return name - return adjust_encoding + return adjust_encoding + else: + return namefunc # The following is adapted from Alexander Belopolsky's tz library diff --git a/lib/dateutil/tz/_factories.py b/lib/dateutil/tz/_factories.py index 88216f90..dbd3b28f 100644 --- a/lib/dateutil/tz/_factories.py +++ b/lib/dateutil/tz/_factories.py @@ -1,5 +1,5 @@ from datetime import timedelta - +import weakref class _TzSingleton(type): def __init__(cls, *args, **kwargs): @@ -19,7 +19,7 @@ class _TzFactory(type): class _TzOffsetFactory(_TzFactory): def __init__(cls, *args, **kwargs): - cls.__instances = {} + cls.__instances = weakref.WeakValueDictionary() def __call__(cls, name, offset): if isinstance(offset, timedelta): @@ -36,7 +36,7 @@ class _TzOffsetFactory(_TzFactory): class _TzStrFactory(_TzFactory): def __init__(cls, *args, **kwargs): - cls.__instances = {} + cls.__instances = weakref.WeakValueDictionary() def __call__(cls, s, posix_offset=False): key = (s, posix_offset) diff --git a/lib/dateutil/tz/tz.py b/lib/dateutil/tz/tz.py index 0a8016ac..c10bb0bd 100644 --- a/lib/dateutil/tz/tz.py +++ b/lib/dateutil/tz/tz.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """ This module offers timezone implementations subclassing the abstract -:py:`datetime.tzinfo` type. There are classes to handle tzfile format files -(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ -environment string (in all known formats), given ranges (with help from -relative deltas), local machine timezone, fixed offset timezone, and UTC +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC timezone. """ import datetime @@ -13,6 +13,7 @@ import time import sys import os import bisect +import weakref import six from six import string_types @@ -28,6 +29,9 @@ try: except ImportError: tzwin = tzwinlocal = None +# For warning about rounding tzinfo +from warnings import warn + ZERO = datetime.timedelta(0) EPOCH = datetime.datetime.utcfromtimestamp(0) EPOCHORDINAL = EPOCH.toordinal() @@ -137,7 +141,8 @@ class tzoffset(datetime.tzinfo): offset = offset.total_seconds() except (TypeError, AttributeError): pass - self._offset = datetime.timedelta(seconds=offset) + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) def utcoffset(self, dt): return self._offset @@ -387,10 +392,60 @@ class tzfile(_tzinfo): ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. See `Sources for Time Zone and Daylight Saving Time Data - `_ for more information. Time - zone files can be compiled from the `IANA Time Zone database files + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files `_ with the `zic time zone compiler `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + """ def __init__(self, fileobj, filename=None): @@ -410,7 +465,7 @@ class tzfile(_tzinfo): if fileobj is not None: if not file_opened_here: - fileobj = _ContextWrapper(fileobj) + fileobj = _nullcontext(fileobj) with fileobj as file_stream: tzobj = self._read_tzfile(file_stream) @@ -487,7 +542,7 @@ class tzfile(_tzinfo): if timecnt: out.trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) + fileobj.read(timecnt)) else: out.trans_idx = [] @@ -550,10 +605,7 @@ class tzfile(_tzinfo): out.ttinfo_list = [] for i in range(typecnt): gmtoff, isdst, abbrind = ttinfo[i] - # Round to full-minutes if that's not the case. Python's - # datetime doesn't accept sub-minute timezones. Check - # http://python.org/sf/1447945 for some information. - gmtoff = 60 * ((gmtoff + 30) // 60) + gmtoff = _get_supported_offset(gmtoff) tti = _ttinfo() tti.offset = gmtoff tti.dstoffset = datetime.timedelta(0) @@ -605,37 +657,44 @@ class tzfile(_tzinfo): # isgmt are off, so it should be in wall time. OTOH, it's # always in gmt time. Let me know if you have comments # about this. - laststdoffset = None + lastdst = None + lastoffset = None + lastdstoffset = None + lastbaseoffset = None out.trans_list = [] + for i, tti in enumerate(out.trans_idx): - if not tti.isdst: - offset = tti.offset - laststdoffset = offset - else: - if laststdoffset is not None: - # Store the DST offset as well and update it in the list - tti.dstoffset = tti.offset - laststdoffset - out.trans_idx[i] = tti + offset = tti.offset + dstoffset = 0 - offset = laststdoffset or 0 + if lastdst is not None: + if tti.isdst: + if not lastdst: + dstoffset = offset - lastoffset - out.trans_list.append(out.trans_list_utc[i] + offset) + if not dstoffset and lastdstoffset: + dstoffset = lastdstoffset - # In case we missed any DST offsets on the way in for some reason, make - # a second pass over the list, looking for the /next/ DST offset. - laststdoffset = None - for i in reversed(range(len(out.trans_idx))): - tti = out.trans_idx[i] - if tti.isdst: - if not (tti.dstoffset or laststdoffset is None): - tti.dstoffset = tti.offset - laststdoffset - else: - laststdoffset = tti.offset + tti.dstoffset = datetime.timedelta(seconds=dstoffset) + lastdstoffset = dstoffset - if not isinstance(tti.dstoffset, datetime.timedelta): - tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) + # If a time zone changes its base offset during a DST transition, + # then you need to adjust by the previous base offset to get the + # transition time in local time. Otherwise you use the current + # base offset. Ideally, I would have some mathematical proof of + # why this is true, but I haven't really thought about it enough. + baseoffset = offset - dstoffset + adjustment = baseoffset + if (lastbaseoffset is not None and baseoffset != lastbaseoffset + and tti.isdst != lastdst): + # The base DST has changed + adjustment = lastbaseoffset - out.trans_idx[i] = tti + lastdst = tti.isdst + lastoffset = offset + lastbaseoffset = baseoffset + + out.trans_list.append(out.trans_list_utc[i] + adjustment) out.trans_idx = tuple(out.trans_idx) out.trans_list = tuple(out.trans_list) @@ -840,8 +899,9 @@ class tzrange(tzrangebase): :param start: A :class:`relativedelta.relativedelta` object or equivalent specifying - the time and time of year that daylight savings time starts. To specify, - for example, that DST starts at 2AM on the 2nd Sunday in March, pass: + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` @@ -849,12 +909,12 @@ class tzrange(tzrangebase): value is 2 AM on the first Sunday in April. :param end: - A :class:`relativedelta.relativedelta` object or equivalent representing - the time and time of year that daylight savings time ends, with the - same specification method as in ``start``. One note is that this should - point to the first time in the *standard* zone, so if a transition - occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM, - set the `hours` parameter to +1. + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. **Examples:** @@ -985,8 +1045,9 @@ class tzstr(tzrange): :param s: A time zone string in ``TZ`` variable format. This can be a - :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`) - or a stream emitting unicode characters (e.g. :class:`StringIO`). + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). :param posix_offset: Optional. If set to ``True``, interpret strings such as ``GMT+3`` or @@ -1203,7 +1264,7 @@ class tzical(object): fileobj = open(fileobj, 'r') else: self._s = getattr(fileobj, 'name', repr(fileobj)) - fileobj = _ContextWrapper(fileobj) + fileobj = _nullcontext(fileobj) self._vtz = {} @@ -1398,15 +1459,85 @@ else: TZFILES = [] TZPATHS = [] + def __get_gettz(name, zoneinfo_priority=False): tzlocal_classes = (tzlocal,) if tzwinlocal is not None: tzlocal_classes += (tzwinlocal,) class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ def __init__(self, name, zoneinfo_priority=False): - self.__instances = {} + self.__instances = weakref.WeakValueDictionary() self._cache_lock = _thread.allocate_lock() def __call__(self, name=None, zoneinfo_priority=False): @@ -1415,17 +1546,22 @@ def __get_gettz(name, zoneinfo_priority=False): if rv is None: rv = self.nocache(name=name, zoneinfo_priority=zoneinfo_priority) - if not (name is None or isinstance(rv, tzlocal_classes)): + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): # tzlocal is slightly more complicated than the other # time zone providers because it depends on environment # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. self.__instances[name] = rv return rv def cache_clear(self): with self._cache_lock: - self.__instances = {} + self.__instances = weakref.WeakValueDictionary() @staticmethod def nocache(name=None, zoneinfo_priority=False): @@ -1492,7 +1628,10 @@ def __get_gettz(name, zoneinfo_priority=False): if not tz: for c in name: - # name must have at least one offset to be a tzstr + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit if c in "0123456789": try: tz = tzstr(name) @@ -1508,9 +1647,11 @@ def __get_gettz(name, zoneinfo_priority=False): return GettzFunc(name, zoneinfo_priority) + gettz = __get_gettz(name=None, zoneinfo_priority=False) del __get_gettz + def datetime_exists(dt, tz=None): """ Given a datetime and a time zone, determine whether or not a given datetime @@ -1525,9 +1666,10 @@ def datetime_exists(dt, tz=None): ``None`` or not provided, the datetime's own time zone will be used. :return: - Returns a boolean value whether or not the "wall time" exists in ``tz``. + Returns a boolean value whether or not the "wall time" exists in + ``tz``. - ..versionadded:: 2.7.0 + .. versionadded:: 2.7.0 """ if tz is None: if dt.tzinfo is None: @@ -1575,7 +1717,7 @@ def datetime_ambiguous(dt, tz=None): if is_ambiguous_fn is not None: try: return tz.is_ambiguous(dt) - except: + except Exception: pass # If it doesn't come out and tell us it's ambiguous, we'll just check if @@ -1598,7 +1740,8 @@ def resolve_imaginary(dt): wall time would be in a zone had the offset transition not occurred, so it will always fall forward by the transition's change in offset. - ..doctest:: + .. doctest:: + >>> from dateutil import tz >>> from datetime import datetime >>> NYC = tz.gettz('America/New_York') @@ -1623,7 +1766,7 @@ def resolve_imaginary(dt): imaginary, the datetime returned is guaranteed to be the same object passed to the function. - ..versionadded:: 2.7.0 + .. versionadded:: 2.7.0 """ if dt.tzinfo is not None and not datetime_exists(dt): @@ -1637,24 +1780,42 @@ def resolve_imaginary(dt): def _datetime_to_timestamp(dt): """ - Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds - since January 1, 1970, ignoring the time zone. + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. """ return (dt.replace(tzinfo=None) - EPOCH).total_seconds() -class _ContextWrapper(object): - """ - Class for wrapping contexts so that they are passed through in a - with statement. - """ - def __init__(self, context): - self.context = context +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset - def __enter__(self): - return self.context - def __exit__(*args, **kwargs): - pass +try: + # Python 3.7 feature + from contextmanager import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass # vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz/win.py b/lib/dateutil/tz/win.py index abd6b1da..52a65b79 100644 --- a/lib/dateutil/tz/win.py +++ b/lib/dateutil/tz/win.py @@ -1,3 +1,11 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" # This code was originally contributed by Jeffrey Harris. import datetime import struct @@ -39,7 +47,7 @@ TZKEYNAME = _settzkeyname() class tzres(object): """ - Class for accessing `tzres.dll`, which contains timezone name related + Class for accessing ``tzres.dll``, which contains timezone name related resources. .. versionadded:: 2.5.0 @@ -72,9 +80,10 @@ class tzres(object): :param offset: A positive integer value referring to a string from the tzres dll. - ..note: + .. note:: + Offsets found in the registry are generally of the form - `@tzres.dll,-114`. The offset in this case if 114, not -114. + ``@tzres.dll,-114``. The offset in this case is 114, not -114. """ resource = self.p_wchar() @@ -146,6 +155,9 @@ class tzwinbase(tzrangebase): return result def display(self): + """ + Return the display name of the time zone. + """ return self._display def transitions(self, year): @@ -188,6 +200,17 @@ class tzwinbase(tzrangebase): class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ def __init__(self, name): self._name = name @@ -234,6 +257,22 @@ class tzwin(tzwinbase): class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ def __init__(self): with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: diff --git a/lib/dateutil/utils.py b/lib/dateutil/utils.py index 8fc34bd5..12bb0458 100644 --- a/lib/dateutil/utils.py +++ b/lib/dateutil/utils.py @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" from __future__ import unicode_literals from datetime import datetime, time