diff --git a/CHANGES.md b/CHANGES.md
index cf767b7..c8a6dc1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -5,6 +5,7 @@
* Add soupsieve 1.9.1 (24859cc)
* Update CacheControl library 0.12.5 (0fedbba) to 0.12.5 (007e8ca)
* Update Certifi 2018.11.29 (10a1f8a) to 2019.03.09 (401100f)
+* Update dateutil 2.7.5 (e954819) to 2.8.0 (c90a30c)
[develop changelog]
diff --git a/lib/dateutil/parser/__init__.py b/lib/dateutil/parser/__init__.py
index c37ccc1..b593a5e 100644
--- a/lib/dateutil/parser/__init__.py
+++ b/lib/dateutil/parser/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from ._parser import parse, parser, parserinfo
+from ._parser import parse, parser, parserinfo, ParserError
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
from ._parser import UnknownTimezoneWarning
@@ -9,6 +9,7 @@ from .isoparser import isoparser, isoparse
__all__ = ['parse', 'parser', 'parserinfo',
'isoparse', 'isoparser',
+ 'ParserError',
'UnknownTimezoneWarning']
diff --git a/lib/dateutil/parser/_parser.py b/lib/dateutil/parser/_parser.py
index 24a1869..9b5e7eb 100644
--- a/lib/dateutil/parser/_parser.py
+++ b/lib/dateutil/parser/_parser.py
@@ -40,7 +40,7 @@ from calendar import monthrange
from io import StringIO
import six
-from six import binary_type, integer_types, text_type
+from six import integer_types, text_type
from decimal import Decimal
@@ -49,7 +49,7 @@ from warnings import warn
from .. import relativedelta
from .. import tz
-__all__ = ["parse", "parserinfo"]
+__all__ = ["parse", "parserinfo", "ParserError"]
# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth
@@ -63,7 +63,7 @@ class _timelex(object):
if six.PY2:
# In Python 2, we can't duck type properly because unicode has
# a 'decode' function, and we'd be double-decoding
- if isinstance(instream, (binary_type, bytearray)):
+ if isinstance(instream, (bytes, bytearray)):
instream = instream.decode()
else:
if getattr(instream, 'decode', None) is not None:
@@ -291,7 +291,7 @@ class parserinfo(object):
("s", "second", "seconds")]
AMPM = [("am", "a"),
("pm", "p")]
- UTCZONE = ["UTC", "GMT", "Z"]
+ UTCZONE = ["UTC", "GMT", "Z", "z"]
PERTAIN = ["of"]
TZOFFSET = {}
# TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
@@ -388,7 +388,8 @@ class parserinfo(object):
if res.year is not None:
res.year = self.convertyear(res.year, res.century_specified)
- if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
+ if ((res.tzoffset == 0 and not res.tzname) or
+ (res.tzname == 'Z' or res.tzname == 'z')):
res.tzname = "UTC"
res.tzoffset = 0
elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
@@ -625,7 +626,7 @@ class parser(object):
first element being a :class:`datetime.datetime` object, the second
a tuple containing the fuzzy tokens.
- :raises ValueError:
+ :raises ParserError:
Raised for invalid or unknown string format, if the provided
:class:`tzinfo` is not in a valid format, or if an invalid date
would be created.
@@ -645,12 +646,15 @@ class parser(object):
res, skipped_tokens = self._parse(timestr, **kwargs)
if res is None:
- raise ValueError("Unknown string format:", timestr)
+ raise ParserError("Unknown string format: %s", timestr)
if len(res) == 0:
- raise ValueError("String does not contain a date:", timestr)
+ raise ParserError("String does not contain a date: %s", timestr)
- ret = self._build_naive(res, default)
+ try:
+ ret = self._build_naive(res, default)
+ except ValueError as e:
+ six.raise_from(ParserError(e.args[0] + ": %s", timestr), e)
if not ignoretz:
ret = self._build_tzaware(ret, res, tzinfos)
@@ -1060,7 +1064,8 @@ class parser(object):
tzname is None and
tzoffset is None and
len(token) <= 5 and
- all(x in string.ascii_uppercase for x in token))
+ (all(x in string.ascii_uppercase for x in token)
+ or token in self.info.UTCZONE))
def _ampm_valid(self, hour, ampm, fuzzy):
"""
@@ -1109,14 +1114,6 @@ class parser(object):
second = int(60 * sec_remainder)
return (minute, second)
- def _parsems(self, value):
- """Parse a I[.F] seconds value into (seconds, microseconds)."""
- if "." not in value:
- return int(value), 0
- else:
- i, f = value.split(".")
- return int(i), int(f.ljust(6, "0")[:6])
-
def _parse_hms(self, idx, tokens, info, hms_idx):
# TODO: Is this going to admit a lot of false-positives for when we
# just happen to have digits and "h", "m" or "s" characters in non-date
@@ -1135,21 +1132,35 @@ class parser(object):
return (new_idx, hms)
- def _recombine_skipped(self, tokens, skipped_idxs):
- """
- >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
- >>> skipped_idxs = [0, 1, 2, 5]
- >>> _recombine_skipped(tokens, skipped_idxs)
- ["foo bar", "baz"]
- """
- skipped_tokens = []
- for i, idx in enumerate(sorted(skipped_idxs)):
- if i > 0 and idx - 1 == skipped_idxs[i - 1]:
- skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
- else:
- skipped_tokens.append(tokens[idx])
+ # ------------------------------------------------------------------
+ # Handling for individual tokens. These are kept as methods instead
+ # of functions for the sake of customizability via subclassing.
- return skipped_tokens
+ def _parsems(self, value):
+ """Parse a I[.F] seconds value into (seconds, microseconds)."""
+ if "." not in value:
+ return int(value), 0
+ else:
+ i, f = value.split(".")
+ return int(i), int(f.ljust(6, "0")[:6])
+
+ def _to_decimal(self, val):
+ try:
+ 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
+
+ # ------------------------------------------------------------------
+ # Post-Parsing construction of datetime output. These are kept as
+ # methods instead of functions for the sake of customizability via
+ # subclassing.
def _build_tzinfo(self, tzinfos, tzname, tzoffset):
if callable(tzinfos):
@@ -1164,6 +1175,9 @@ class parser(object):
tzinfo = tz.tzstr(tzdata)
elif isinstance(tzdata, integer_types):
tzinfo = tz.tzoffset(tzname, tzdata)
+ else:
+ raise TypeError("Offset must be tzinfo subclass, tz string, "
+ "or int offset.")
return tzinfo
def _build_tzaware(self, naive, res, tzinfos):
@@ -1181,10 +1195,10 @@ class parser(object):
# This is mostly relevant for winter GMT zones parsed in the UK
if (aware.tzname() != res.tzname and
res.tzname in self.info.UTCZONE):
- aware = aware.replace(tzinfo=tz.tzutc())
+ aware = aware.replace(tzinfo=tz.UTC)
elif res.tzoffset == 0:
- aware = naive.replace(tzinfo=tz.tzutc())
+ aware = naive.replace(tzinfo=tz.UTC)
elif res.tzoffset:
aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
@@ -1239,17 +1253,21 @@ class parser(object):
return dt
- def _to_decimal(self, val):
- try:
- 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
+ def _recombine_skipped(self, tokens, skipped_idxs):
+ """
+ >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
+ >>> skipped_idxs = [0, 1, 2, 5]
+ >>> _recombine_skipped(tokens, skipped_idxs)
+ ["foo bar", "baz"]
+ """
+ skipped_tokens = []
+ for i, idx in enumerate(sorted(skipped_idxs)):
+ if i > 0 and idx - 1 == skipped_idxs[i - 1]:
+ skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
+ else:
+ skipped_tokens.append(tokens[idx])
+
+ return skipped_tokens
DEFAULTPARSER = parser()
@@ -1573,6 +1591,19 @@ DEFAULTTZPARSER = _tzparser()
def _parsetz(tzstr):
return DEFAULTTZPARSER.parse(tzstr)
+
+class ParserError(ValueError):
+ """Error class for representing failure to parse a datetime string."""
+ def __str__(self):
+ try:
+ return self.args[0] % self.args[1:]
+ except (TypeError, IndexError):
+ return super(ParserError, self).__str__()
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, str(self))
+
+
class UnknownTimezoneWarning(RuntimeWarning):
"""Raised when the parser finds a timezone it cannot parse into a tzinfo"""
# vim:ts=4:sw=4:et
diff --git a/lib/dateutil/parser/isoparser.py b/lib/dateutil/parser/isoparser.py
index e8c8add..db5f8ed 100644
--- a/lib/dateutil/parser/isoparser.py
+++ b/lib/dateutil/parser/isoparser.py
@@ -88,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::
@@ -137,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
@@ -167,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):
@@ -190,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:
@@ -333,7 +341,7 @@ class isoparser(object):
while pos < len_str and comp < 5:
comp += 1
- if timestr[pos:pos + 1] in b'-+Z':
+ if timestr[pos:pos + 1] in b'-+Zz':
# Detect time zone boundary
components[-1] = self._parse_tzstr(timestr[pos:])
pos = len_str
@@ -348,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')
@@ -366,13 +372,12 @@ 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
def _parse_tzstr(self, tzstr, zero_as_utc=True):
- if tzstr == b'Z':
- return tz.tzutc()
+ if tzstr == b'Z' or tzstr == b'z':
+ return tz.UTC
if len(tzstr) not in {3, 5, 6}:
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
@@ -391,7 +396,7 @@ class isoparser(object):
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
if zero_as_utc and hours == 0 and minutes == 0:
- return tz.tzutc()
+ return tz.UTC
else:
if minutes > 59:
raise ValueError('Invalid minutes in time zone offset')
diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py
index 5491c43..ec7f66d 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.
@@ -45,13 +49,15 @@ class relativedelta(object):
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. Notice that
- if the calculated date is already Monday, for example,
- using MO(1) or MO(-1) won't change the day.
+ 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
@@ -82,9 +88,12 @@ class relativedelta(object):
For example
+ >>> 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))
- datetime(2018, 4, 2, 14, 37, 0)
+ >>> dt + delta
+ datetime.datetime(2018, 4, 2, 14, 37)
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
@@ -276,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 3d32700..7b54539 100644
--- a/lib/dateutil/rrule.py
+++ b/lib/dateutil/rrule.py
@@ -21,7 +21,6 @@ from six.moves import _thread, range
import heapq
from ._common import weekday as weekdaybase
-from .tz import tzutc, tzlocal
# For warning about deprecation of until and count
from warnings import warn
@@ -353,20 +352,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
@@ -429,7 +434,7 @@ class rrule(rrulebase):
if not dtstart:
if until and until.tzinfo:
dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
- else:
+ else:
dtstart = datetime.datetime.now().replace(microsecond=0)
elif not isinstance(dtstart, datetime.datetime):
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
@@ -1406,7 +1411,52 @@ class rruleset(rrulebase):
self._len = total
+
+
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,
@@ -1508,6 +1558,58 @@ class _rrulestr(object):
raise ValueError("invalid '%s': %s" % (name, value))
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+ def _parse_date_value(self, date_value, parms, rule_tzids,
+ ignoretz, tzids, tzinfos):
+ global parser
+ if not parser:
+ from dateutil import parser
+
+ datevals = []
+ value_found = False
+ TZID = None
+
+ for parm in parms:
+ if parm.startswith("TZID="):
+ try:
+ tzkey = rule_tzids[parm.split('TZID=')[-1]]
+ except KeyError:
+ continue
+ if tzids is None:
+ from . import tz
+ tzlookup = tz.gettz
+ elif callable(tzids):
+ tzlookup = tzids
+ else:
+ tzlookup = getattr(tzids, 'get', None)
+ if tzlookup is None:
+ msg = ('tzids must be a callable, mapping, or None, '
+ 'not %s' % tzids)
+ raise ValueError(msg)
+
+ TZID = tzlookup(tzkey)
+ continue
+
+ # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+ # only once.
+ if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+ raise ValueError("unsupported parm: " + parm)
+ else:
+ if value_found:
+ msg = ("Duplicate value parameter found in: " + parm)
+ raise ValueError(msg)
+ value_found = True
+
+ for datestr in date_value.split(','):
+ date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+ if TZID is not None:
+ if date.tzinfo is None:
+ date = date.replace(tzinfo=TZID)
+ else:
+ raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+ datevals.append(date)
+
+ return datevals
+
def _parse_rfc(self, s,
dtstart=None,
cache=False,
@@ -1580,54 +1682,18 @@ class _rrulestr(object):
raise ValueError("unsupported EXRULE parm: "+parm)
exrulevals.append(value)
elif name == "EXDATE":
- for parm in parms:
- if parm != "VALUE=DATE-TIME":
- raise ValueError("unsupported EXDATE parm: "+parm)
- exdatevals.append(value)
+ exdatevals.extend(
+ self._parse_date_value(value, parms,
+ TZID_NAMES, ignoretz,
+ tzids, tzinfos)
+ )
elif name == "DTSTART":
- # RFC 5445 3.8.2.4: The VALUE parameter is optional, but
- # may be found only once.
- value_found = False
- TZID = None
- valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
- for parm in parms:
- if parm.startswith("TZID="):
- try:
- tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
- except KeyError:
- continue
- if tzids is None:
- from . import tz
- tzlookup = tz.gettz
- elif callable(tzids):
- tzlookup = tzids
- else:
- tzlookup = getattr(tzids, 'get', None)
- if tzlookup is None:
- msg = ('tzids must be a callable, ' +
- 'mapping, or None, ' +
- 'not %s' % tzids)
- raise ValueError(msg)
-
- TZID = tzlookup(tzkey)
- continue
- if parm not in valid_values:
- raise ValueError("unsupported DTSTART parm: "+parm)
- else:
- if value_found:
- msg = ("Duplicate value parameter found in " +
- "DTSTART: " + parm)
- raise ValueError(msg)
- value_found = True
- if not parser:
- from dateutil import parser
- dtstart = parser.parse(value, ignoretz=ignoretz,
- tzinfos=tzinfos)
- if TZID is not None:
- if dtstart.tzinfo is None:
- dtstart = dtstart.replace(tzinfo=TZID)
- else:
- raise ValueError('DTSTART specifies multiple timezones')
+ dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+ ignoretz, tzids, tzinfos)
+ if len(dtvals) != 1:
+ raise ValueError("Multiple DTSTART values specified:" +
+ value)
+ dtstart = dtvals[0]
else:
raise ValueError("unsupported property: "+name)
if (forceset or len(rrulevals) > 1 or rdatevals
@@ -1649,10 +1715,7 @@ class _rrulestr(object):
ignoretz=ignoretz,
tzinfos=tzinfos))
for value in exdatevals:
- for datestr in value.split(','):
- rset.exdate(parser.parse(datestr,
- ignoretz=ignoretz,
- tzinfos=tzinfos))
+ rset.exdate(value)
if compatible and dtstart:
rset.rdate(dtstart)
return rset
diff --git a/lib/dateutil/tz/__init__.py b/lib/dateutil/tz/__init__.py
index 628e2c7..51be92f 100644
--- a/lib/dateutil/tz/__init__.py
+++ b/lib/dateutil/tz/__init__.py
@@ -2,11 +2,6 @@
from .tz import *
from .tz import __doc__
-#: Convenience constant providing a :class:`tzutc()` instance
-#:
-#: .. versionadded:: 2.7.0
-UTC = tzutc()
-
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
"enfold", "datetime_ambiguous", "datetime_exists",
diff --git a/lib/dateutil/tz/_common.py b/lib/dateutil/tz/_common.py
index e478b3e..5f15e9a 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()
-
- return name
-
- return adjust_encoding
+ 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
diff --git a/lib/dateutil/tz/_factories.py b/lib/dateutil/tz/_factories.py
index 88216f9..8a9ee58 100644
--- a/lib/dateutil/tz/_factories.py
+++ b/lib/dateutil/tz/_factories.py
@@ -1,4 +1,6 @@
from datetime import timedelta
+import weakref
+from collections import OrderedDict
class _TzSingleton(type):
@@ -11,6 +13,7 @@ class _TzSingleton(type):
cls.__instance = super(_TzSingleton, cls).__call__()
return cls.__instance
+
class _TzFactory(type):
def instance(cls, *args, **kwargs):
"""Alternate constructor that returns a fresh instance"""
@@ -19,7 +22,9 @@ class _TzFactory(type):
class _TzOffsetFactory(_TzFactory):
def __init__(cls, *args, **kwargs):
- cls.__instances = {}
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
def __call__(cls, name, offset):
if isinstance(offset, timedelta):
@@ -31,12 +36,22 @@ class _TzOffsetFactory(_TzFactory):
if instance is None:
instance = cls.__instances.setdefault(key,
cls.instance(name, offset))
+
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+ # Remove an item if the strong cache is overpopulated
+ # TODO: Maybe this should be under a lock?
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
+
return instance
class _TzStrFactory(_TzFactory):
def __init__(cls, *args, **kwargs):
- cls.__instances = {}
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
def __call__(cls, s, posix_offset=False):
key = (s, posix_offset)
@@ -45,5 +60,14 @@ class _TzStrFactory(_TzFactory):
if instance is None:
instance = cls.__instances.setdefault(key,
cls.instance(s, posix_offset))
+
+ cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
+
+
+ # Remove an item if the strong cache is overpopulated
+ # TODO: Maybe this should be under a lock?
+ if len(cls.__strong_cache) > cls.__strong_cache_size:
+ cls.__strong_cache.popitem(last=False)
+
return instance
diff --git a/lib/dateutil/tz/tz.py b/lib/dateutil/tz/tz.py
index 8a85e5e..5aa7e5d 100644
--- a/lib/dateutil/tz/tz.py
+++ b/lib/dateutil/tz/tz.py
@@ -13,6 +13,8 @@ import time
import sys
import os
import bisect
+import weakref
+from collections import OrderedDict
import six
from six import string_types
@@ -28,6 +30,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()
@@ -118,6 +123,12 @@ class tzutc(datetime.tzinfo):
__reduce__ = object.__reduce__
+#: Convenience constant providing a :class:`tzutc()` instance
+#:
+#: .. versionadded:: 2.7.0
+UTC = tzutc()
+
+
@six.add_metaclass(_TzOffsetFactory)
class tzoffset(datetime.tzinfo):
"""
@@ -137,7 +148,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
@@ -460,7 +472,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)
@@ -600,10 +612,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)
@@ -655,37 +664,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 = laststdoffset or 0
-
- out.trans_list.append(out.trans_list_utc[i] + offset)
-
- # 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
- if not isinstance(tti.dstoffset, datetime.timedelta):
- tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset)
-
- out.trans_idx[i] = tti
+ for i, tti in enumerate(out.trans_idx):
+ offset = tti.offset
+ dstoffset = 0
+
+ if lastdst is not None:
+ if tti.isdst:
+ if not lastdst:
+ dstoffset = offset - lastoffset
+
+ if not dstoffset and lastdstoffset:
+ dstoffset = lastdstoffset
+
+ tti.dstoffset = datetime.timedelta(seconds=dstoffset)
+ lastdstoffset = 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
+
+ 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)
@@ -1255,7 +1271,7 @@ class tzical(object):
fileobj = open(fileobj, 'r')
else:
self._s = getattr(fileobj, 'name', repr(fileobj))
- fileobj = _ContextWrapper(fileobj)
+ fileobj = _nullcontext(fileobj)
self._vtz = {}
@@ -1528,7 +1544,9 @@ def __get_gettz(name, zoneinfo_priority=False):
"""
def __init__(self, name, zoneinfo_priority=False):
- self.__instances = {}
+ self.__instances = weakref.WeakValueDictionary()
+ self.__strong_cache_size = 8
+ self.__strong_cache = OrderedDict()
self._cache_lock = _thread.allocate_lock()
def __call__(self, name=None, zoneinfo_priority=False):
@@ -1537,17 +1555,37 @@ 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
+ else:
+ # No need for strong caching, return immediately
+ return rv
+
+ self.__strong_cache[name] = self.__strong_cache.pop(name, rv)
+
+ if len(self.__strong_cache) > self.__strong_cache_size:
+ self.__strong_cache.popitem(last=False)
return rv
+ def set_cache_size(self, size):
+ with self._cache_lock:
+ self.__strong_cache_size = size
+ while len(self.__strong_cache) > size:
+ self.__strong_cache.popitem(last=False)
+
def cache_clear(self):
with self._cache_lock:
- self.__instances = {}
+ self.__instances = weakref.WeakValueDictionary()
+ self.__strong_cache.clear()
@staticmethod
def nocache(name=None, zoneinfo_priority=False):
@@ -1605,7 +1643,8 @@ def __get_gettz(name, zoneinfo_priority=False):
if tzwin is not None:
try:
tz = tzwin(name)
- except WindowsError:
+ except (WindowsError, UnicodeEncodeError):
+ # UnicodeEncodeError is for Python 2.7 compat
tz = None
if not zoneinfo_priority and not tz:
@@ -1626,15 +1665,15 @@ def __get_gettz(name, zoneinfo_priority=False):
break
else:
if name in ("GMT", "UTC"):
- tz = tzutc()
+ tz = UTC
elif name in time.tzname:
tz = tzlocal()
return tz
return GettzFunc(name, zoneinfo_priority)
-
gettz = __get_gettz(name=None, zoneinfo_priority=False)
+
del __get_gettz
@@ -1666,7 +1705,7 @@ def datetime_exists(dt, tz=None):
# This is essentially a test of whether or not the datetime can survive
# a round trip to UTC.
- dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz)
+ dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz)
dt_rt = dt_rt.replace(tzinfo=None)
return dt == dt_rt
@@ -1772,18 +1811,36 @@ def _datetime_to_timestamp(dt):
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 abd6b1d..52a65b7 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: