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