mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
Update datutil-2.8.2
This commit is contained in:
parent
439ca8ebb8
commit
3b645cf6c3
37 changed files with 16696 additions and 2664 deletions
|
@ -1,2 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
__version__ = "2.4.2"
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = 'unknown'
|
||||
|
||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||
'utils', 'zoneinfo']
|
||||
|
|
43
lib/dateutil/_common.py
Normal file
43
lib/dateutil/_common.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
Common code used in multiple modules.
|
||||
"""
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.n,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
# vim:ts=4:sw=4:et
|
5
lib/dateutil/_version.py
Normal file
5
lib/dateutil/_version.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
# coding: utf-8
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
version = '2.8.2'
|
||||
version_tuple = (2, 8, 2)
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a generic easter computing method for any given year, using
|
||||
This module offers a generic Easter computing method for any given year, using
|
||||
Western, Orthodox or Julian algorithms.
|
||||
"""
|
||||
|
||||
|
@ -21,31 +21,31 @@ def easter(year, method=EASTER_WESTERN):
|
|||
quoted in "Explanatory Supplement to the Astronomical
|
||||
Almanac", P. Kenneth Seidelmann, editor.
|
||||
|
||||
This algorithm implements three different easter
|
||||
This algorithm implements three different Easter
|
||||
calculation methods:
|
||||
|
||||
1 - Original calculation in Julian calendar, valid in
|
||||
dates after 326 AD
|
||||
2 - Original method, with date converted to Gregorian
|
||||
calendar, valid in years 1583 to 4099
|
||||
3 - Revised method, in Gregorian calendar, valid in
|
||||
years 1583 to 4099 as well
|
||||
1. Original calculation in Julian calendar, valid in
|
||||
dates after 326 AD
|
||||
2. Original method, with date converted to Gregorian
|
||||
calendar, valid in years 1583 to 4099
|
||||
3. Revised method, in Gregorian calendar, valid in
|
||||
years 1583 to 4099 as well
|
||||
|
||||
These methods are represented by the constants:
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
* ``EASTER_JULIAN = 1``
|
||||
* ``EASTER_ORTHODOX = 2``
|
||||
* ``EASTER_WESTERN = 3``
|
||||
|
||||
The default method is method 3.
|
||||
|
||||
More about the algorithm may be found at:
|
||||
|
||||
http://users.chariot.net.au/~gmarts/eastalg.htm
|
||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||
|
||||
and
|
||||
|
||||
http://www.tondering.dk/claus/calendar.html
|
||||
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||
|
||||
"""
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
61
lib/dateutil/parser/__init__.py
Normal file
61
lib/dateutil/parser/__init__.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ._parser import parse, parser, parserinfo, ParserError
|
||||
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||
from ._parser import UnknownTimezoneWarning
|
||||
|
||||
from ._parser import __doc__
|
||||
|
||||
from .isoparser import isoparser, isoparse
|
||||
|
||||
__all__ = ['parse', 'parser', 'parserinfo',
|
||||
'isoparse', 'isoparser',
|
||||
'ParserError',
|
||||
'UnknownTimezoneWarning']
|
||||
|
||||
|
||||
###
|
||||
# Deprecate portions of the private interface so that downstream code that
|
||||
# is improperly relying on it is given *some* notice.
|
||||
|
||||
|
||||
def __deprecated_private_func(f):
|
||||
from functools import wraps
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private function and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=f.__name__)
|
||||
|
||||
@wraps(f)
|
||||
def deprecated_func(*args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return deprecated_func
|
||||
|
||||
def __deprecate_private_class(c):
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private class and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=c.__name__)
|
||||
|
||||
class private_class(c):
|
||||
__doc__ = c.__doc__
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
super(private_class, self).__init__(*args, **kwargs)
|
||||
|
||||
private_class.__name__ = c.__name__
|
||||
|
||||
return private_class
|
||||
|
||||
|
||||
from ._parser import _timelex, _resultbase
|
||||
from ._parser import _tzparser, _parsetz
|
||||
|
||||
_timelex = __deprecate_private_class(_timelex)
|
||||
_tzparser = __deprecate_private_class(_tzparser)
|
||||
_resultbase = __deprecate_private_class(_resultbase)
|
||||
_parsetz = __deprecated_private_func(_parsetz)
|
1613
lib/dateutil/parser/_parser.py
Normal file
1613
lib/dateutil/parser/_parser.py
Normal file
File diff suppressed because it is too large
Load diff
416
lib/dateutil/parser/isoparser.py
Normal file
416
lib/dateutil/parser/isoparser.py
Normal file
|
@ -0,0 +1,416 @@
|
|||
# -*- coding: utf-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
|
||||
from dateutil import tz
|
||||
|
||||
from functools import wraps
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
__all__ = ["isoparse", "isoparser"]
|
||||
|
||||
|
||||
def _takes_ascii(f):
|
||||
@wraps(f)
|
||||
def func(self, str_in, *args, **kwargs):
|
||||
# If it's a stream, read the whole thing
|
||||
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||
|
||||
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||
if isinstance(str_in, six.text_type):
|
||||
# ASCII is the same in UTF-8
|
||||
try:
|
||||
str_in = str_in.encode('ascii')
|
||||
except UnicodeEncodeError as e:
|
||||
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||
six.raise_from(ValueError(msg), e)
|
||||
|
||||
return f(self, str_in, *args, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class isoparser(object):
|
||||
def __init__(self, sep=None):
|
||||
"""
|
||||
:param sep:
|
||||
A single character that separates date and time portions. If
|
||||
``None``, the parser will accept any single character.
|
||||
For strict ISO-8601 adherence, pass ``'T'``.
|
||||
"""
|
||||
if sep is not None:
|
||||
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||
raise ValueError('Separator must be a single, non-numeric ' +
|
||||
'ASCII character')
|
||||
|
||||
sep = sep.encode('ascii')
|
||||
|
||||
self._sep = sep
|
||||
|
||||
@_takes_ascii
|
||||
def isoparse(self, dt_str):
|
||||
"""
|
||||
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||
|
||||
An ISO-8601 datetime string consists of a date portion, followed
|
||||
optionally by a time portion - the date and time portions are separated
|
||||
by a single character separator, which is ``T`` in the official
|
||||
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||
combined with a time portion.
|
||||
|
||||
Supported date formats are:
|
||||
|
||||
Common:
|
||||
|
||||
- ``YYYY``
|
||||
- ``YYYY-MM`` or ``YYYYMM``
|
||||
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||
|
||||
Uncommon:
|
||||
|
||||
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||
|
||||
The ISO week and day numbering follows the same logic as
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
Supported time formats are:
|
||||
|
||||
- ``hh``
|
||||
- ``hh:mm`` or ``hhmm``
|
||||
- ``hh:mm:ss`` or ``hhmmss``
|
||||
- ``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. The decimal separator can be
|
||||
either a dot or a comma.
|
||||
|
||||
|
||||
.. caution::
|
||||
|
||||
Support for fractional components other than seconds is part of the
|
||||
ISO-8601 standard, but is not currently implemented in this parser.
|
||||
|
||||
Supported time zone offset formats are:
|
||||
|
||||
- `Z` (UTC)
|
||||
- `±HH:MM`
|
||||
- `±HHMM`
|
||||
- `±HH`
|
||||
|
||||
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||
with the exception of UTC, which will be represented as
|
||||
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||
|
||||
:param dt_str:
|
||||
A string or stream containing only an ISO-8601 datetime string
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.datetime` representing the string.
|
||||
Unspecified components default to their lowest value.
|
||||
|
||||
.. warning::
|
||||
|
||||
As of version 2.7.0, the strictness of the parser should not be
|
||||
considered a stable part of the contract. Any valid ISO-8601 string
|
||||
that parses correctly with the default settings will continue to
|
||||
parse correctly in future versions, but invalid strings that
|
||||
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)
|
||||
|
||||
if len(dt_str) > pos:
|
||||
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||
components += self._parse_isotime(dt_str[pos + 1:])
|
||||
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
|
||||
def parse_isodate(self, datestr):
|
||||
"""
|
||||
Parse the date portion of an ISO string.
|
||||
|
||||
:param datestr:
|
||||
The string portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date` object
|
||||
"""
|
||||
components, pos = self._parse_isodate(datestr)
|
||||
if pos < len(datestr):
|
||||
raise ValueError('String contains unknown ISO ' +
|
||||
'components: {!r}'.format(datestr.decode('ascii')))
|
||||
return date(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_isotime(self, timestr):
|
||||
"""
|
||||
Parse the time portion of an ISO string.
|
||||
|
||||
:param timestr:
|
||||
The time portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.time` object
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Parse a valid ISO time zone string.
|
||||
|
||||
See :func:`isoparser.isoparse` for details on supported formats.
|
||||
|
||||
:param tzstr:
|
||||
A string representing an ISO time zone offset
|
||||
|
||||
:param zero_as_utc:
|
||||
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||
|
||||
:return:
|
||||
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||
specified) offsets equivalent to UTC.
|
||||
"""
|
||||
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||
|
||||
# Constants
|
||||
_DATE_SEP = b'-'
|
||||
_TIME_SEP = b':'
|
||||
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
||||
|
||||
def _parse_isodate(self, dt_str):
|
||||
try:
|
||||
return self._parse_isodate_common(dt_str)
|
||||
except ValueError:
|
||||
return self._parse_isodate_uncommon(dt_str)
|
||||
|
||||
def _parse_isodate_common(self, dt_str):
|
||||
len_str = len(dt_str)
|
||||
components = [1, 1, 1]
|
||||
|
||||
if len_str < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# Year
|
||||
components[0] = int(dt_str[0:4])
|
||||
pos = 4
|
||||
if pos >= len_str:
|
||||
return components, pos
|
||||
|
||||
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||
if has_sep:
|
||||
pos += 1
|
||||
|
||||
# Month
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common month')
|
||||
|
||||
components[1] = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if pos >= len_str:
|
||||
if has_sep:
|
||||
return components, pos
|
||||
else:
|
||||
raise ValueError('Invalid ISO format')
|
||||
|
||||
if has_sep:
|
||||
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||
raise ValueError('Invalid separator in ISO string')
|
||||
pos += 1
|
||||
|
||||
# Day
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common day')
|
||||
components[2] = int(dt_str[pos:pos + 2])
|
||||
return components, pos + 2
|
||||
|
||||
def _parse_isodate_uncommon(self, dt_str):
|
||||
if len(dt_str) < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# All ISO formats start with the year
|
||||
year = int(dt_str[0:4])
|
||||
|
||||
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||
|
||||
pos = 4 + has_sep # Skip '-' if it's there
|
||||
if dt_str[pos:pos + 1] == b'W':
|
||||
# YYYY-?Www-?D?
|
||||
pos += 1
|
||||
weekno = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
dayno = 1
|
||||
if len(dt_str) > pos:
|
||||
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||
raise ValueError('Inconsistent use of dash separator')
|
||||
|
||||
pos += has_sep
|
||||
|
||||
dayno = int(dt_str[pos:pos + 1])
|
||||
pos += 1
|
||||
|
||||
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||
else:
|
||||
# YYYYDDD or YYYY-DDD
|
||||
if len(dt_str) - pos < 3:
|
||||
raise ValueError('Invalid ordinal day')
|
||||
|
||||
ordinal_day = int(dt_str[pos:pos + 3])
|
||||
pos += 3
|
||||
|
||||
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||
raise ValueError('Invalid ordinal day' +
|
||||
' {} for year {}'.format(ordinal_day, year))
|
||||
|
||||
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||
|
||||
components = [base_date.year, base_date.month, base_date.day]
|
||||
return components, pos
|
||||
|
||||
def _calculate_weekdate(self, year, week, day):
|
||||
"""
|
||||
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||
|
||||
This function is effectively the inverse of
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
:param year:
|
||||
The year in the ISO calendar
|
||||
|
||||
:param week:
|
||||
The week in the ISO calendar - range is [1, 53]
|
||||
|
||||
:param day:
|
||||
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date`
|
||||
"""
|
||||
if not 0 < week < 54:
|
||||
raise ValueError('Invalid week: {}'.format(week))
|
||||
|
||||
if not 0 < day < 8: # Range is 1-7
|
||||
raise ValueError('Invalid weekday: {}'.format(day))
|
||||
|
||||
# Get week 1 for the specific year:
|
||||
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||
|
||||
# Now add the specific number of weeks and days to get what we want
|
||||
week_offset = (week - 1) * 7 + (day - 1)
|
||||
return week_1 + timedelta(days=week_offset)
|
||||
|
||||
def _parse_isotime(self, timestr):
|
||||
len_str = len(timestr)
|
||||
components = [0, 0, 0, 0, None]
|
||||
pos = 0
|
||||
comp = -1
|
||||
|
||||
if len_str < 2:
|
||||
raise ValueError('ISO time too short')
|
||||
|
||||
has_sep = False
|
||||
|
||||
while pos < len_str and comp < 5:
|
||||
comp += 1
|
||||
|
||||
if timestr[pos:pos + 1] in b'-+Zz':
|
||||
# Detect time zone boundary
|
||||
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||
pos = len_str
|
||||
break
|
||||
|
||||
if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
|
||||
has_sep = True
|
||||
pos += 1
|
||||
elif comp == 2 and has_sep:
|
||||
if timestr[pos:pos+1] != self._TIME_SEP:
|
||||
raise ValueError('Inconsistent use of colon separator')
|
||||
pos += 1
|
||||
|
||||
if comp < 3:
|
||||
# Hour, minute, second
|
||||
components[comp] = int(timestr[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if comp == 3:
|
||||
# Fraction of a second
|
||||
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
||||
if not frac:
|
||||
continue
|
||||
|
||||
us_str = frac.group(1)[:6] # Truncate to microseconds
|
||||
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||
pos += len(frac.group())
|
||||
|
||||
if pos < len_str:
|
||||
raise ValueError('Unused components in ISO string')
|
||||
|
||||
if components[0] == 24:
|
||||
# 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')
|
||||
|
||||
return components
|
||||
|
||||
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||
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')
|
||||
|
||||
if tzstr[0:1] == b'-':
|
||||
mult = -1
|
||||
elif tzstr[0:1] == b'+':
|
||||
mult = 1
|
||||
else:
|
||||
raise ValueError('Time zone offset requires sign')
|
||||
|
||||
hours = int(tzstr[1:3])
|
||||
if len(tzstr) == 3:
|
||||
minutes = 0
|
||||
else:
|
||||
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.UTC
|
||||
else:
|
||||
if minutes > 59:
|
||||
raise ValueError('Invalid minutes in time zone offset')
|
||||
|
||||
if hours > 23:
|
||||
raise ValueError('Invalid hours in time zone offset')
|
||||
|
||||
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||
|
||||
|
||||
DEFAULT_ISOPARSER = isoparser()
|
||||
isoparse = DEFAULT_ISOPARSER.isoparse
|
|
@ -2,113 +2,104 @@
|
|||
import datetime
|
||||
import calendar
|
||||
|
||||
import operator
|
||||
from math import copysign
|
||||
|
||||
from six import integer_types
|
||||
from warnings import warn
|
||||
|
||||
from ._common import weekday
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
|
||||
class relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is based on the specification of the excellent
|
||||
work done by M.-A. Lemburg in his
|
||||
`mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ 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.
|
||||
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.
|
||||
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||
in his
|
||||
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ 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.
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information (argument is singular); adding or subtracting a
|
||||
relativedelta with absolute information does not perform an aritmetic
|
||||
operation, but rather REPLACES the corresponding value in the
|
||||
original datetime with the value(s) in relativedelta.
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative (argument is plural); adding
|
||||
or subtracting a relativedelta with relative information performs
|
||||
the corresponding aritmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information (argument is singular); adding or subtracting a
|
||||
relativedelta with absolute information does not perform an arithmetic
|
||||
operation, but rather REPLACES the corresponding value in the
|
||||
original datetime with the value(s) in 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.
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative (argument is plural); adding
|
||||
or subtracting a relativedelta with relative information performs
|
||||
the corresponding arithmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
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).
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
Here is the behavior of operations with relativedelta:
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
1. Calculate the absolute year, using the 'year' argument, or the
|
||||
original datetime year, if the argument is not present.
|
||||
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).
|
||||
|
||||
2. Add the relative 'years' argument to the absolute year.
|
||||
The order of attributes considered when this relativedelta is
|
||||
added to a datetime is:
|
||||
|
||||
3. Do steps 1 and 2 for month/months.
|
||||
1. Year
|
||||
2. Month
|
||||
3. Day
|
||||
4. Hours
|
||||
5. Minutes
|
||||
6. Seconds
|
||||
7. Microseconds
|
||||
|
||||
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.
|
||||
Finally, weekday is applied, using the rule described above.
|
||||
|
||||
5. Add the relative 'days' argument to the absolute day. Notice
|
||||
that the 'weeks' argument is multiplied by 7 and added to
|
||||
'days'.
|
||||
For example
|
||||
|
||||
6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
|
||||
microsecond/microseconds.
|
||||
>>> 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)
|
||||
|
||||
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,
|
||||
|
@ -117,11 +108,13 @@ Here is the behavior of operations with relativedelta:
|
|||
year=None, month=None, day=None, weekday=None,
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
|
||||
if dt1 and dt2:
|
||||
# datetime is a subclass of date. So both must be date
|
||||
if not (isinstance(dt1, datetime.date) and
|
||||
isinstance(dt2, datetime.date)):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
|
||||
# We allow two dates, or two datetimes, so we coerce them to be
|
||||
# of the same type
|
||||
if (isinstance(dt1, datetime.datetime) !=
|
||||
|
@ -130,6 +123,7 @@ Here is the behavior of operations with relativedelta:
|
|||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||
|
||||
self.years = 0
|
||||
self.months = 0
|
||||
self.days = 0
|
||||
|
@ -148,31 +142,48 @@ Here is the behavior of operations with relativedelta:
|
|||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
|
||||
# Get year / month delta between the two
|
||||
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||
self._set_months(months)
|
||||
|
||||
# Remove the year/month delta so the timedelta is just well-defined
|
||||
# time units (seconds, days and microseconds)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# If we've overshot our target, make an adjustment
|
||||
if dt1 < dt2:
|
||||
while dt1 > dtm:
|
||||
months += 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
compare = operator.gt
|
||||
increment = 1
|
||||
else:
|
||||
while dt1 < dtm:
|
||||
months -= 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
compare = operator.lt
|
||||
increment = -1
|
||||
|
||||
while compare(dt1, dtm):
|
||||
months += increment
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# Get the timedelta between the "months-adjusted" date and dt1
|
||||
delta = dt1 - dtm
|
||||
self.seconds = delta.seconds+delta.days*86400
|
||||
self.seconds = delta.seconds + delta.days * 86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
self.years = years
|
||||
self.months = months
|
||||
self.days = days+weeks*7
|
||||
# Check for non-integer values in integer-only quantities
|
||||
if any(x is not None and x != int(x) for x in (years, months)):
|
||||
raise ValueError("Non-integer years and months are "
|
||||
"ambiguous and not currently supported.")
|
||||
|
||||
# Relative information
|
||||
self.years = int(years)
|
||||
self.months = int(months)
|
||||
self.days = days + weeks * 7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self.seconds = seconds
|
||||
self.microseconds = microseconds
|
||||
|
||||
# Absolute information
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
|
@ -181,6 +192,14 @@ Here is the behavior of operations with relativedelta:
|
|||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if any(x is not None and int(x) != x
|
||||
for x in (year, month, day, hour,
|
||||
minute, second, microsecond)):
|
||||
# For now we'll deprecate floats - later it'll be an error.
|
||||
warn("Non-integer value passed as absolute information. " +
|
||||
"This is not a well-defined condition and will raise " +
|
||||
"errors in future versions.", DeprecationWarning)
|
||||
|
||||
if isinstance(weekday, integer_types):
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
|
@ -211,30 +230,30 @@ Here is the behavior of operations with relativedelta:
|
|||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
s = self.microseconds//abs(self.microseconds)
|
||||
div, mod = divmod(self.microseconds*s, 1000000)
|
||||
self.microseconds = mod*s
|
||||
self.seconds += div*s
|
||||
s = _sign(self.microseconds)
|
||||
div, mod = divmod(self.microseconds * s, 1000000)
|
||||
self.microseconds = mod * s
|
||||
self.seconds += div * s
|
||||
if abs(self.seconds) > 59:
|
||||
s = self.seconds//abs(self.seconds)
|
||||
div, mod = divmod(self.seconds*s, 60)
|
||||
self.seconds = mod*s
|
||||
self.minutes += div*s
|
||||
s = _sign(self.seconds)
|
||||
div, mod = divmod(self.seconds * s, 60)
|
||||
self.seconds = mod * s
|
||||
self.minutes += div * s
|
||||
if abs(self.minutes) > 59:
|
||||
s = self.minutes//abs(self.minutes)
|
||||
div, mod = divmod(self.minutes*s, 60)
|
||||
self.minutes = mod*s
|
||||
self.hours += div*s
|
||||
s = _sign(self.minutes)
|
||||
div, mod = divmod(self.minutes * s, 60)
|
||||
self.minutes = mod * s
|
||||
self.hours += div * s
|
||||
if abs(self.hours) > 23:
|
||||
s = self.hours//abs(self.hours)
|
||||
div, mod = divmod(self.hours*s, 24)
|
||||
self.hours = mod*s
|
||||
self.days += div*s
|
||||
s = _sign(self.hours)
|
||||
div, mod = divmod(self.hours * s, 24)
|
||||
self.hours = mod * s
|
||||
self.days += div * s
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years += div*s
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years += div * s
|
||||
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||
or self.hour is not None or self.minute is not None or
|
||||
self.second is not None or self.microsecond is not None):
|
||||
|
@ -242,38 +261,106 @@ Here is the behavior of operations with relativedelta:
|
|||
else:
|
||||
self._has_time = 0
|
||||
|
||||
@property
|
||||
def weeks(self):
|
||||
return int(self.days / 7.0)
|
||||
|
||||
@weeks.setter
|
||||
def weeks(self, value):
|
||||
self.days = self.days - (self.weeks * 7) + value * 7
|
||||
|
||||
def _set_months(self, months):
|
||||
self.months = months
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years = div*s
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years = div * s
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def normalized(self):
|
||||
"""
|
||||
Return a version of this object represented entirely using integer
|
||||
values for the relative attributes.
|
||||
|
||||
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||
relativedelta(days=+1, hours=+14)
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||
"""
|
||||
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||
days = int(self.days)
|
||||
|
||||
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||
hours = int(hours_f)
|
||||
|
||||
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||
minutes = int(minutes_f)
|
||||
|
||||
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||
seconds = int(seconds_f)
|
||||
|
||||
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||
|
||||
# Constructor carries overflow back up with call to _fix()
|
||||
return self.__class__(years=self.years, months=self.months,
|
||||
days=days, hours=hours, minutes=minutes,
|
||||
seconds=seconds, microseconds=microseconds,
|
||||
leapdays=self.leapdays, year=self.year,
|
||||
month=self.month, day=self.day,
|
||||
weekday=self.weekday, hour=self.hour,
|
||||
minute=self.minute, second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, relativedelta):
|
||||
return relativedelta(years=other.years+self.years,
|
||||
months=other.months+self.months,
|
||||
days=other.days+self.days,
|
||||
hours=other.hours+self.hours,
|
||||
minutes=other.minutes+self.minutes,
|
||||
seconds=other.seconds+self.seconds,
|
||||
return self.__class__(years=other.years + self.years,
|
||||
months=other.months + self.months,
|
||||
days=other.days + self.days,
|
||||
hours=other.hours + self.hours,
|
||||
minutes=other.minutes + self.minutes,
|
||||
seconds=other.seconds + self.seconds,
|
||||
microseconds=(other.microseconds +
|
||||
self.microseconds),
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=(other.microsecond or
|
||||
year=(other.year if other.year is not None
|
||||
else self.year),
|
||||
month=(other.month if other.month is not None
|
||||
else self.month),
|
||||
day=(other.day if other.day is not None
|
||||
else self.day),
|
||||
weekday=(other.weekday if other.weekday is not None
|
||||
else self.weekday),
|
||||
hour=(other.hour if other.hour is not None
|
||||
else self.hour),
|
||||
minute=(other.minute if other.minute is not None
|
||||
else self.minute),
|
||||
second=(other.second if other.second is not None
|
||||
else self.second),
|
||||
microsecond=(other.microsecond if other.microsecond
|
||||
is not None else
|
||||
self.microsecond))
|
||||
if isinstance(other, datetime.timedelta):
|
||||
return self.__class__(years=self.years,
|
||||
months=self.months,
|
||||
days=self.days + other.days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds + other.seconds,
|
||||
microseconds=self.microseconds + other.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
if not isinstance(other, datetime.date):
|
||||
raise TypeError("unsupported type for add operation")
|
||||
return NotImplemented
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
|
@ -305,11 +392,11 @@ Here is the behavior of operations with relativedelta:
|
|||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth)-1)*7
|
||||
jumpdays = (abs(nth) - 1) * 7
|
||||
if nth > 0:
|
||||
jumpdays += (7-ret.weekday()+weekday) % 7
|
||||
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||
else:
|
||||
jumpdays += (ret.weekday()-weekday) % 7
|
||||
jumpdays += (ret.weekday() - weekday) % 7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
@ -322,26 +409,53 @@ Here is the behavior of operations with relativedelta:
|
|||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError("unsupported type for sub operation")
|
||||
return relativedelta(years=self.years-other.years,
|
||||
months=self.months-other.months,
|
||||
days=self.days-other.days,
|
||||
hours=self.hours-other.hours,
|
||||
minutes=self.minutes-other.minutes,
|
||||
seconds=self.seconds-other.seconds,
|
||||
microseconds=self.microseconds-other.microseconds,
|
||||
return NotImplemented # In case the other object defines __rsub__
|
||||
return self.__class__(years=self.years - other.years,
|
||||
months=self.months - other.months,
|
||||
days=self.days - other.days,
|
||||
hours=self.hours - other.hours,
|
||||
minutes=self.minutes - other.minutes,
|
||||
seconds=self.seconds - other.seconds,
|
||||
microseconds=self.microseconds - other.microseconds,
|
||||
leapdays=self.leapdays or other.leapdays,
|
||||
year=self.year or other.year,
|
||||
month=self.month or other.month,
|
||||
day=self.day or other.day,
|
||||
weekday=self.weekday or other.weekday,
|
||||
hour=self.hour or other.hour,
|
||||
minute=self.minute or other.minute,
|
||||
second=self.second or other.second,
|
||||
microsecond=self.microsecond or other.microsecond)
|
||||
year=(self.year if self.year is not None
|
||||
else other.year),
|
||||
month=(self.month if self.month is not None else
|
||||
other.month),
|
||||
day=(self.day if self.day is not None else
|
||||
other.day),
|
||||
weekday=(self.weekday if self.weekday is not None else
|
||||
other.weekday),
|
||||
hour=(self.hour if self.hour is not None else
|
||||
other.hour),
|
||||
minute=(self.minute if self.minute is not None else
|
||||
other.minute),
|
||||
second=(self.second if self.second is not None else
|
||||
other.second),
|
||||
microsecond=(self.microsecond if self.microsecond
|
||||
is not None else
|
||||
other.microsecond))
|
||||
|
||||
def __abs__(self):
|
||||
return self.__class__(years=abs(self.years),
|
||||
months=abs(self.months),
|
||||
days=abs(self.days),
|
||||
hours=abs(self.hours),
|
||||
minutes=abs(self.minutes),
|
||||
seconds=abs(self.seconds),
|
||||
microseconds=abs(self.microseconds),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return relativedelta(years=-self.years,
|
||||
return self.__class__(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
|
@ -379,14 +493,18 @@ Here is the behavior of operations with relativedelta:
|
|||
__nonzero__ = __bool__
|
||||
|
||||
def __mul__(self, other):
|
||||
f = float(other)
|
||||
return relativedelta(years=int(self.years*f),
|
||||
months=int(self.months*f),
|
||||
days=int(self.days*f),
|
||||
hours=int(self.hours*f),
|
||||
minutes=int(self.minutes*f),
|
||||
seconds=int(self.seconds*f),
|
||||
microseconds=int(self.microseconds*f),
|
||||
try:
|
||||
f = float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__class__(years=int(self.years * f),
|
||||
months=int(self.months * f),
|
||||
days=int(self.days * f),
|
||||
hours=int(self.hours * f),
|
||||
minutes=int(self.minutes * f),
|
||||
seconds=int(self.seconds * f),
|
||||
microseconds=int(self.microseconds * f),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
|
@ -401,7 +519,7 @@ Here is the behavior of operations with relativedelta:
|
|||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return False
|
||||
return NotImplemented
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
|
@ -416,6 +534,7 @@ Here is the behavior of operations with relativedelta:
|
|||
self.hours == other.hours and
|
||||
self.minutes == other.minutes and
|
||||
self.seconds == other.seconds and
|
||||
self.microseconds == other.microseconds and
|
||||
self.leapdays == other.leapdays and
|
||||
self.year == other.year and
|
||||
self.month == other.month and
|
||||
|
@ -425,11 +544,36 @@ Here is the behavior of operations with relativedelta:
|
|||
self.second == other.second and
|
||||
self.microsecond == other.microsecond)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.years,
|
||||
self.months,
|
||||
self.days,
|
||||
self.hours,
|
||||
self.minutes,
|
||||
self.seconds,
|
||||
self.microseconds,
|
||||
self.leapdays,
|
||||
self.year,
|
||||
self.month,
|
||||
self.day,
|
||||
self.hour,
|
||||
self.minute,
|
||||
self.second,
|
||||
self.microsecond,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __div__(self, other):
|
||||
return self.__mul__(1/float(other))
|
||||
try:
|
||||
reciprocal = 1 / float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__mul__(reciprocal)
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
|
@ -439,12 +583,17 @@ Here is the behavior of operations with relativedelta:
|
|||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if value:
|
||||
l.append("%s=%+d" % (attr, value))
|
||||
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||
for attr in ["year", "month", "day", "weekday",
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||
attrs=", ".join(l))
|
||||
|
||||
|
||||
def _sign(x):
|
||||
return int(copysign(1, x))
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
|
|
|
@ -2,18 +2,29 @@
|
|||
"""
|
||||
The rrule module offers a small, complete, and very fast, implementation of
|
||||
the recurrence rules documented in the
|
||||
`iCalendar RFC <http://www.ietf.org/rfc/rfc2445.txt>`_,
|
||||
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
||||
including support for caching of results.
|
||||
"""
|
||||
import itertools
|
||||
import datetime
|
||||
import calendar
|
||||
import datetime
|
||||
import heapq
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
|
||||
from fractions import gcd
|
||||
from functools import wraps
|
||||
# For warning about deprecation of until and count
|
||||
from warnings import warn
|
||||
|
||||
from six import advance_iterator, integer_types
|
||||
from six.moves import _thread
|
||||
|
||||
from six.moves import _thread, range
|
||||
|
||||
from ._common import weekday as weekdaybase
|
||||
|
||||
try:
|
||||
from math import gcd
|
||||
except ImportError:
|
||||
from fractions import gcd
|
||||
|
||||
__all__ = ["rrule", "rruleset", "rrulestr",
|
||||
"YEARLY", "MONTHLY", "WEEKLY", "DAILY",
|
||||
|
@ -37,6 +48,8 @@ del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
|
|||
MDAY365MASK = tuple(MDAY365MASK)
|
||||
M365MASK = tuple(M365MASK)
|
||||
|
||||
FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
|
||||
|
||||
(YEARLY,
|
||||
MONTHLY,
|
||||
WEEKLY,
|
||||
|
@ -50,37 +63,32 @@ easter = None
|
|||
parser = None
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
class weekday(weekdaybase):
|
||||
"""
|
||||
This version of weekday does not allow n = 0.
|
||||
"""
|
||||
def __init__(self, wkday, n=None):
|
||||
if n == 0:
|
||||
raise ValueError("Can't create weekday with n == 0")
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
raise ValueError("Can't create weekday with n==0")
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
super(weekday, self).__init__(wkday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
def _invalidates_cache(f):
|
||||
"""
|
||||
Decorator for rruleset methods which may invalidate the
|
||||
cached length.
|
||||
"""
|
||||
@wraps(f)
|
||||
def inner_func(self, *args, **kwargs):
|
||||
rv = f(self, *args, **kwargs)
|
||||
self._invalidate_cache()
|
||||
return rv
|
||||
|
||||
return inner_func
|
||||
|
||||
|
||||
class rrulebase(object):
|
||||
|
@ -88,12 +96,11 @@ class rrulebase(object):
|
|||
if cache:
|
||||
self._cache = []
|
||||
self._cache_lock = _thread.allocate_lock()
|
||||
self._cache_gen = self._iter()
|
||||
self._cache_complete = False
|
||||
self._invalidate_cache()
|
||||
else:
|
||||
self._cache = None
|
||||
self._cache_complete = False
|
||||
self._len = None
|
||||
self._len = None
|
||||
|
||||
def __iter__(self):
|
||||
if self._cache_complete:
|
||||
|
@ -103,6 +110,17 @@ class rrulebase(object):
|
|||
else:
|
||||
return self._iter_cached()
|
||||
|
||||
def _invalidate_cache(self):
|
||||
if self._cache is not None:
|
||||
self._cache = []
|
||||
self._cache_complete = False
|
||||
self._cache_gen = self._iter()
|
||||
|
||||
if self._cache_lock.locked():
|
||||
self._cache_lock.release()
|
||||
|
||||
self._len = None
|
||||
|
||||
def _iter_cached(self):
|
||||
i = 0
|
||||
gen = self._cache_gen
|
||||
|
@ -161,7 +179,7 @@ class rrulebase(object):
|
|||
return False
|
||||
return False
|
||||
|
||||
# __len__() introduces a large performance penality.
|
||||
# __len__() introduces a large performance penalty.
|
||||
def count(self):
|
||||
""" Returns the number of recurrences in this set. It will have go
|
||||
trough the whole recurrence, if this hasn't been done before. """
|
||||
|
@ -209,7 +227,48 @@ class rrulebase(object):
|
|||
return i
|
||||
return None
|
||||
|
||||
def between(self, after, before, inc=False):
|
||||
def xafter(self, dt, count=None, inc=False):
|
||||
"""
|
||||
Generator which yields up to `count` recurrences after the given
|
||||
datetime instance, equivalent to `after`.
|
||||
|
||||
:param dt:
|
||||
The datetime at which to start generating recurrences.
|
||||
|
||||
:param count:
|
||||
The maximum number of recurrences to generate. If `None` (default),
|
||||
dates are generated until the recurrence rule is exhausted.
|
||||
|
||||
:param inc:
|
||||
If `dt` is an instance of the rule and `inc` is `True`, it is
|
||||
included in the output.
|
||||
|
||||
:yields: Yields a sequence of `datetime` objects.
|
||||
"""
|
||||
|
||||
if self._cache_complete:
|
||||
gen = self._cache
|
||||
else:
|
||||
gen = self
|
||||
|
||||
# Select the comparison function
|
||||
if inc:
|
||||
comp = lambda dc, dtc: dc >= dtc
|
||||
else:
|
||||
comp = lambda dc, dtc: dc > dtc
|
||||
|
||||
# Generate dates
|
||||
n = 0
|
||||
for d in gen:
|
||||
if comp(d, dt):
|
||||
if count is not None:
|
||||
n += 1
|
||||
if n > count:
|
||||
break
|
||||
|
||||
yield d
|
||||
|
||||
def between(self, after, before, inc=False, count=1):
|
||||
""" Returns all the occurrences of the rrule between after and before.
|
||||
The inc keyword defines what happens if after and/or before are
|
||||
themselves occurrences. With inc=True, they will be included in the
|
||||
|
@ -254,12 +313,31 @@ class rrule(rrulebase):
|
|||
Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
|
||||
or SECONDLY.
|
||||
|
||||
.. note::
|
||||
Per RFC section 3.3.10, recurrence instances falling on invalid dates
|
||||
and times are ignored rather than coerced:
|
||||
|
||||
Recurrence rules may generate recurrence instances with an invalid
|
||||
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
|
||||
on a day where the local time is moved forward by an hour at 1:00
|
||||
AM). Such recurrence instances MUST be ignored and MUST NOT be
|
||||
counted as part of the recurrence set.
|
||||
|
||||
This can lead to possibly surprising behavior when, for example, the
|
||||
start date occurs at the end of the month:
|
||||
|
||||
>>> from dateutil.rrule import rrule, MONTHLY
|
||||
>>> from datetime import datetime
|
||||
>>> start_date = datetime(2014, 12, 31)
|
||||
>>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
|
||||
... # doctest: +NORMALIZE_WHITESPACE
|
||||
[datetime.datetime(2014, 12, 31, 0, 0),
|
||||
datetime.datetime(2015, 1, 31, 0, 0),
|
||||
datetime.datetime(2015, 3, 31, 0, 0),
|
||||
datetime.datetime(2015, 5, 31, 0, 0)]
|
||||
|
||||
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
|
||||
|
@ -276,12 +354,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 keyword ``until`` in conjunction
|
||||
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
||||
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
||||
html/rfc5545#section-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
|
||||
limit of the recurrence. If a recurrence instance happens to be the
|
||||
same as the datetime instance given in the until keyword, this will
|
||||
be the last occurrence.
|
||||
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 keyword ``until`` in conjunction
|
||||
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
||||
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
||||
html/rfc5545#section-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
|
||||
|
@ -298,6 +390,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
|
||||
|
@ -323,11 +420,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,
|
||||
|
@ -338,7 +434,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:
|
||||
|
@ -349,10 +448,35 @@ class rrule(rrulebase):
|
|||
self._interval = interval
|
||||
self._count = count
|
||||
|
||||
# Cache the original byxxx rules, if they are provided, as the _byxxx
|
||||
# attributes do not necessarily map to the inputs, and this can be
|
||||
# a problem in generating the strings. Only store things if they've
|
||||
# been supplied (the string retrieval will just use .get())
|
||||
self._original_rule = {}
|
||||
|
||||
if until and not isinstance(until, datetime.datetime):
|
||||
until = datetime.datetime.fromordinal(until.toordinal())
|
||||
self._until = until
|
||||
|
||||
if self._dtstart and self._until:
|
||||
if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
|
||||
# According to RFC5545 Section 3.3.10:
|
||||
# https://tools.ietf.org/html/rfc5545#section-3.3.10
|
||||
#
|
||||
# > If the "DTSTART" property is specified as a date with UTC
|
||||
# > time or a date with local time and time zone reference,
|
||||
# > then the UNTIL rule part MUST be specified as a date with
|
||||
# > UTC time.
|
||||
raise ValueError(
|
||||
'RRULE UNTIL values must be specified in UTC when DTSTART '
|
||||
'is timezone-aware'
|
||||
)
|
||||
|
||||
if count is not None and until:
|
||||
warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
||||
" and has been deprecated in dateutil. Future versions will "
|
||||
"raise an error.", DeprecationWarning)
|
||||
|
||||
if wkst is None:
|
||||
self._wkst = calendar.firstweekday()
|
||||
elif isinstance(wkst, integer_types):
|
||||
|
@ -374,16 +498,23 @@ class rrule(rrulebase):
|
|||
raise ValueError("bysetpos must be between 1 and 366, "
|
||||
"or between -366 and -1")
|
||||
|
||||
if self._bysetpos:
|
||||
self._original_rule['bysetpos'] = self._bysetpos
|
||||
|
||||
if (byweekno is None and byyearday is None and bymonthday is None and
|
||||
byweekday is None and byeaster is None):
|
||||
if freq == YEARLY:
|
||||
if bymonth is None:
|
||||
bymonth = dtstart.month
|
||||
self._original_rule['bymonth'] = None
|
||||
bymonthday = dtstart.day
|
||||
self._original_rule['bymonthday'] = None
|
||||
elif freq == MONTHLY:
|
||||
bymonthday = dtstart.day
|
||||
self._original_rule['bymonthday'] = None
|
||||
elif freq == WEEKLY:
|
||||
byweekday = dtstart.weekday()
|
||||
self._original_rule['byweekday'] = None
|
||||
|
||||
# bymonth
|
||||
if bymonth is None:
|
||||
|
@ -394,6 +525,9 @@ class rrule(rrulebase):
|
|||
|
||||
self._bymonth = tuple(sorted(set(bymonth)))
|
||||
|
||||
if 'bymonth' not in self._original_rule:
|
||||
self._original_rule['bymonth'] = self._bymonth
|
||||
|
||||
# byyearday
|
||||
if byyearday is None:
|
||||
self._byyearday = None
|
||||
|
@ -402,6 +536,7 @@ class rrule(rrulebase):
|
|||
byyearday = (byyearday,)
|
||||
|
||||
self._byyearday = tuple(sorted(set(byyearday)))
|
||||
self._original_rule['byyearday'] = self._byyearday
|
||||
|
||||
# byeaster
|
||||
if byeaster is not None:
|
||||
|
@ -411,10 +546,12 @@ class rrule(rrulebase):
|
|||
self._byeaster = (byeaster,)
|
||||
else:
|
||||
self._byeaster = tuple(sorted(byeaster))
|
||||
|
||||
self._original_rule['byeaster'] = self._byeaster
|
||||
else:
|
||||
self._byeaster = None
|
||||
|
||||
# bymonthay
|
||||
# bymonthday
|
||||
if bymonthday is None:
|
||||
self._bymonthday = ()
|
||||
self._bynmonthday = ()
|
||||
|
@ -422,8 +559,15 @@ class rrule(rrulebase):
|
|||
if isinstance(bymonthday, integer_types):
|
||||
bymonthday = (bymonthday,)
|
||||
|
||||
self._bymonthday = tuple(sorted(set([x for x in bymonthday if x > 0])))
|
||||
self._bynmonthday = tuple(sorted(set([x for x in bymonthday if x < 0])))
|
||||
bymonthday = set(bymonthday) # Ensure it's unique
|
||||
|
||||
self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
|
||||
self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
|
||||
|
||||
# Storing positive numbers first, then negative numbers
|
||||
if 'bymonthday' not in self._original_rule:
|
||||
self._original_rule['bymonthday'] = tuple(
|
||||
itertools.chain(self._bymonthday, self._bynmonthday))
|
||||
|
||||
# byweekno
|
||||
if byweekno is None:
|
||||
|
@ -434,6 +578,8 @@ class rrule(rrulebase):
|
|||
|
||||
self._byweekno = tuple(sorted(set(byweekno)))
|
||||
|
||||
self._original_rule['byweekno'] = self._byweekno
|
||||
|
||||
# byweekday / bynweekday
|
||||
if byweekday is None:
|
||||
self._byweekday = None
|
||||
|
@ -462,14 +608,24 @@ class rrule(rrulebase):
|
|||
|
||||
if self._byweekday is not None:
|
||||
self._byweekday = tuple(sorted(self._byweekday))
|
||||
orig_byweekday = [weekday(x) for x in self._byweekday]
|
||||
else:
|
||||
orig_byweekday = ()
|
||||
|
||||
if self._bynweekday is not None:
|
||||
self._bynweekday = tuple(sorted(self._bynweekday))
|
||||
orig_bynweekday = [weekday(*x) for x in self._bynweekday]
|
||||
else:
|
||||
orig_bynweekday = ()
|
||||
|
||||
if 'byweekday' not in self._original_rule:
|
||||
self._original_rule['byweekday'] = tuple(itertools.chain(
|
||||
orig_byweekday, orig_bynweekday))
|
||||
|
||||
# byhour
|
||||
if byhour is None:
|
||||
if freq < HOURLY:
|
||||
self._byhour = set((dtstart.hour,))
|
||||
self._byhour = {dtstart.hour}
|
||||
else:
|
||||
self._byhour = None
|
||||
else:
|
||||
|
@ -484,11 +640,12 @@ class rrule(rrulebase):
|
|||
self._byhour = set(byhour)
|
||||
|
||||
self._byhour = tuple(sorted(self._byhour))
|
||||
self._original_rule['byhour'] = self._byhour
|
||||
|
||||
# byminute
|
||||
if byminute is None:
|
||||
if freq < MINUTELY:
|
||||
self._byminute = set((dtstart.minute,))
|
||||
self._byminute = {dtstart.minute}
|
||||
else:
|
||||
self._byminute = None
|
||||
else:
|
||||
|
@ -503,6 +660,7 @@ class rrule(rrulebase):
|
|||
self._byminute = set(byminute)
|
||||
|
||||
self._byminute = tuple(sorted(self._byminute))
|
||||
self._original_rule['byminute'] = self._byminute
|
||||
|
||||
# bysecond
|
||||
if bysecond is None:
|
||||
|
@ -524,6 +682,7 @@ class rrule(rrulebase):
|
|||
self._bysecond = set(bysecond)
|
||||
|
||||
self._bysecond = tuple(sorted(self._bysecond))
|
||||
self._original_rule['bysecond'] = self._bysecond
|
||||
|
||||
if self._freq >= HOURLY:
|
||||
self._timeset = None
|
||||
|
@ -538,6 +697,82 @@ class rrule(rrulebase):
|
|||
self._timeset.sort()
|
||||
self._timeset = tuple(self._timeset)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Output a string that would generate this RRULE if passed to rrulestr.
|
||||
This is mostly compatible with RFC5545, except for the
|
||||
dateutil-specific extension BYEASTER.
|
||||
"""
|
||||
|
||||
output = []
|
||||
h, m, s = [None] * 3
|
||||
if self._dtstart:
|
||||
output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
|
||||
h, m, s = self._dtstart.timetuple()[3:6]
|
||||
|
||||
parts = ['FREQ=' + FREQNAMES[self._freq]]
|
||||
if self._interval != 1:
|
||||
parts.append('INTERVAL=' + str(self._interval))
|
||||
|
||||
if self._wkst:
|
||||
parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
|
||||
|
||||
if self._count is not None:
|
||||
parts.append('COUNT=' + str(self._count))
|
||||
|
||||
if self._until:
|
||||
parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
|
||||
|
||||
if self._original_rule.get('byweekday') is not None:
|
||||
# The str() method on weekday objects doesn't generate
|
||||
# RFC5545-compliant strings, so we should modify that.
|
||||
original_rule = dict(self._original_rule)
|
||||
wday_strings = []
|
||||
for wday in original_rule['byweekday']:
|
||||
if wday.n:
|
||||
wday_strings.append('{n:+d}{wday}'.format(
|
||||
n=wday.n,
|
||||
wday=repr(wday)[0:2]))
|
||||
else:
|
||||
wday_strings.append(repr(wday))
|
||||
|
||||
original_rule['byweekday'] = wday_strings
|
||||
else:
|
||||
original_rule = self._original_rule
|
||||
|
||||
partfmt = '{name}={vals}'
|
||||
for name, key in [('BYSETPOS', 'bysetpos'),
|
||||
('BYMONTH', 'bymonth'),
|
||||
('BYMONTHDAY', 'bymonthday'),
|
||||
('BYYEARDAY', 'byyearday'),
|
||||
('BYWEEKNO', 'byweekno'),
|
||||
('BYDAY', 'byweekday'),
|
||||
('BYHOUR', 'byhour'),
|
||||
('BYMINUTE', 'byminute'),
|
||||
('BYSECOND', 'bysecond'),
|
||||
('BYEASTER', 'byeaster')]:
|
||||
value = original_rule.get(key)
|
||||
if value:
|
||||
parts.append(partfmt.format(name=name, vals=(','.join(str(v)
|
||||
for v in value))))
|
||||
|
||||
output.append('RRULE:' + ';'.join(parts))
|
||||
return '\n'.join(output)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""Return new rrule with same attributes except for those attributes given new
|
||||
values by whichever keyword arguments are specified."""
|
||||
new_kwargs = {"interval": self._interval,
|
||||
"count": self._count,
|
||||
"dtstart": self._dtstart,
|
||||
"freq": self._freq,
|
||||
"until": self._until,
|
||||
"wkst": self._wkst,
|
||||
"cache": False if self._cache is None else True }
|
||||
new_kwargs.update(self._original_rule)
|
||||
new_kwargs.update(kwargs)
|
||||
return rrule(**new_kwargs)
|
||||
|
||||
def _iter(self):
|
||||
year, month, day, hour, minute, second, weekday, yearday, _ = \
|
||||
self._dtstart.timetuple()
|
||||
|
@ -636,31 +871,32 @@ class rrule(rrulebase):
|
|||
self._len = total
|
||||
return
|
||||
elif res >= self._dtstart:
|
||||
total += 1
|
||||
yield res
|
||||
if count:
|
||||
if count is not None:
|
||||
count -= 1
|
||||
if not count:
|
||||
if count < 0:
|
||||
self._len = total
|
||||
return
|
||||
total += 1
|
||||
yield res
|
||||
else:
|
||||
for i in dayset[start:end]:
|
||||
if i is not None:
|
||||
date = datetime.date.fromordinal(ii.yearordinal+i)
|
||||
date = datetime.date.fromordinal(ii.yearordinal + i)
|
||||
for time in timeset:
|
||||
res = datetime.datetime.combine(date, time)
|
||||
if until and res > until:
|
||||
self._len = total
|
||||
return
|
||||
elif res >= self._dtstart:
|
||||
total += 1
|
||||
yield res
|
||||
if count:
|
||||
if count is not None:
|
||||
count -= 1
|
||||
if not count:
|
||||
if count < 0:
|
||||
self._len = total
|
||||
return
|
||||
|
||||
total += 1
|
||||
yield res
|
||||
|
||||
# Handle frequency and interval
|
||||
fixday = False
|
||||
if freq == YEARLY:
|
||||
|
@ -743,10 +979,10 @@ class rrule(rrulebase):
|
|||
elif freq == SECONDLY:
|
||||
if filtered:
|
||||
# Jump to one iteration before next day
|
||||
second += (((86399-(hour*3600+minute*60+second))
|
||||
// interval)*interval)
|
||||
second += (((86399 - (hour * 3600 + minute * 60 + second))
|
||||
// interval) * interval)
|
||||
|
||||
rep_rate = (24*3600)
|
||||
rep_rate = (24 * 3600)
|
||||
valid = False
|
||||
for j in range(0, rep_rate // gcd(interval, rep_rate)):
|
||||
if bysecond:
|
||||
|
@ -809,9 +1045,9 @@ class rrule(rrulebase):
|
|||
|
||||
:param start:
|
||||
Specifies the starting position.
|
||||
:param byxxx:
|
||||
:param byxxx:
|
||||
An iterable containing the list of allowed values.
|
||||
:param base:
|
||||
:param base:
|
||||
The largest allowable value for the specified frequency (e.g.
|
||||
24 hours, 60 minutes).
|
||||
|
||||
|
@ -846,9 +1082,9 @@ class rrule(rrulebase):
|
|||
specified along with a `BYXXX` parameter at the same "level"
|
||||
(e.g. `HOURLY` specified with `BYHOUR`).
|
||||
|
||||
:param value:
|
||||
:param value:
|
||||
The old value of the component.
|
||||
:param byxxx:
|
||||
:param byxxx:
|
||||
The `BYXXX` set, which should have been generated by
|
||||
`rrule._construct_byset`, or something else which checks that a
|
||||
valid rule is present.
|
||||
|
@ -888,8 +1124,8 @@ class _iterinfo(object):
|
|||
# Every mask is 7 days longer to handle cross-year weekly periods.
|
||||
rr = self.rrule
|
||||
if year != self.lastyear:
|
||||
self.yearlen = 365+calendar.isleap(year)
|
||||
self.nextyearlen = 365+calendar.isleap(year+1)
|
||||
self.yearlen = 365 + calendar.isleap(year)
|
||||
self.nextyearlen = 365 + calendar.isleap(year + 1)
|
||||
firstyday = datetime.date(year, 1, 1)
|
||||
self.yearordinal = firstyday.toordinal()
|
||||
self.yearweekday = firstyday.weekday()
|
||||
|
@ -1040,10 +1276,10 @@ class _iterinfo(object):
|
|||
return dset, start, i
|
||||
|
||||
def ddayset(self, year, month, day):
|
||||
dset = [None]*self.yearlen
|
||||
i = datetime.date(year, month, day).toordinal()-self.yearordinal
|
||||
dset = [None] * self.yearlen
|
||||
i = datetime.date(year, month, day).toordinal() - self.yearordinal
|
||||
dset[i] = i
|
||||
return dset, i, i+1
|
||||
return dset, i, i + 1
|
||||
|
||||
def htimeset(self, hour, minute, second):
|
||||
tset = []
|
||||
|
@ -1051,7 +1287,7 @@ class _iterinfo(object):
|
|||
for minute in rr._byminute:
|
||||
for second in rr._bysecond:
|
||||
tset.append(datetime.time(hour, minute, second,
|
||||
tzinfo=rr._tzinfo))
|
||||
tzinfo=rr._tzinfo))
|
||||
tset.sort()
|
||||
return tset
|
||||
|
||||
|
@ -1090,7 +1326,11 @@ class rruleset(rrulebase):
|
|||
try:
|
||||
self.dt = advance_iterator(self.gen)
|
||||
except StopIteration:
|
||||
self.genlist.remove(self)
|
||||
if self.genlist[0] is self:
|
||||
heapq.heappop(self.genlist)
|
||||
else:
|
||||
self.genlist.remove(self)
|
||||
heapq.heapify(self.genlist)
|
||||
|
||||
next = __next__
|
||||
|
||||
|
@ -1113,16 +1353,19 @@ class rruleset(rrulebase):
|
|||
self._exrule = []
|
||||
self._exdate = []
|
||||
|
||||
@_invalidates_cache
|
||||
def rrule(self, rrule):
|
||||
""" Include the given :py:class:`rrule` instance in the recurrence set
|
||||
generation. """
|
||||
self._rrule.append(rrule)
|
||||
|
||||
@_invalidates_cache
|
||||
def rdate(self, rdate):
|
||||
""" Include the given :py:class:`datetime` instance in the recurrence
|
||||
set generation. """
|
||||
self._rdate.append(rdate)
|
||||
|
||||
@_invalidates_cache
|
||||
def exrule(self, exrule):
|
||||
""" Include the given rrule instance in the recurrence set exclusion
|
||||
list. Dates which are part of the given recurrence rules will not
|
||||
|
@ -1130,6 +1373,7 @@ class rruleset(rrulebase):
|
|||
"""
|
||||
self._exrule.append(exrule)
|
||||
|
||||
@_invalidates_cache
|
||||
def exdate(self, exdate):
|
||||
""" Include the given datetime instance in the recurrence set
|
||||
exclusion list. Dates included that way will not be generated,
|
||||
|
@ -1142,31 +1386,79 @@ class rruleset(rrulebase):
|
|||
self._genitem(rlist, iter(self._rdate))
|
||||
for gen in [iter(x) for x in self._rrule]:
|
||||
self._genitem(rlist, gen)
|
||||
rlist.sort()
|
||||
exlist = []
|
||||
self._exdate.sort()
|
||||
self._genitem(exlist, iter(self._exdate))
|
||||
for gen in [iter(x) for x in self._exrule]:
|
||||
self._genitem(exlist, gen)
|
||||
exlist.sort()
|
||||
lastdt = None
|
||||
total = 0
|
||||
heapq.heapify(rlist)
|
||||
heapq.heapify(exlist)
|
||||
while rlist:
|
||||
ritem = rlist[0]
|
||||
if not lastdt or lastdt != ritem.dt:
|
||||
while exlist and exlist[0] < ritem:
|
||||
advance_iterator(exlist[0])
|
||||
exlist.sort()
|
||||
exitem = exlist[0]
|
||||
advance_iterator(exitem)
|
||||
if exlist and exlist[0] is exitem:
|
||||
heapq.heapreplace(exlist, exitem)
|
||||
if not exlist or ritem != exlist[0]:
|
||||
total += 1
|
||||
yield ritem.dt
|
||||
lastdt = ritem.dt
|
||||
advance_iterator(ritem)
|
||||
rlist.sort()
|
||||
if rlist and rlist[0] is ritem:
|
||||
heapq.heapreplace(rlist, ritem)
|
||||
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,
|
||||
|
@ -1214,16 +1506,29 @@ class _rrulestr(object):
|
|||
def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
||||
rrkwargs["wkst"] = self._weekday_map[value]
|
||||
|
||||
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg):
|
||||
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
|
||||
"""
|
||||
Two ways to specify this: +1MO or MO(+1)
|
||||
"""
|
||||
l = []
|
||||
for wday in value.split(','):
|
||||
for i in range(len(wday)):
|
||||
if wday[i] not in '+-0123456789':
|
||||
break
|
||||
n = wday[:i] or None
|
||||
w = wday[i:]
|
||||
if n:
|
||||
n = int(n)
|
||||
if '(' in wday:
|
||||
# If it's of the form TH(+1), etc.
|
||||
splt = wday.split('(')
|
||||
w = splt[0]
|
||||
n = int(splt[1][:-1])
|
||||
elif len(wday):
|
||||
# If it's of the form +1MO
|
||||
for i in range(len(wday)):
|
||||
if wday[i] not in '+-0123456789':
|
||||
break
|
||||
n = wday[:i] or None
|
||||
w = wday[i:]
|
||||
if n:
|
||||
n = int(n)
|
||||
else:
|
||||
raise ValueError("Invalid (empty) BYDAY specification.")
|
||||
|
||||
l.append(weekdays[self._weekday_map[w]](n))
|
||||
rrkwargs["byweekday"] = l
|
||||
|
||||
|
@ -1255,6 +1560,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,
|
||||
|
@ -1262,11 +1619,17 @@ class _rrulestr(object):
|
|||
forceset=False,
|
||||
compatible=False,
|
||||
ignoretz=False,
|
||||
tzids=None,
|
||||
tzinfos=None):
|
||||
global parser
|
||||
if compatible:
|
||||
forceset = True
|
||||
unfold = True
|
||||
|
||||
TZID_NAMES = dict(map(
|
||||
lambda x: (x.upper(), x),
|
||||
re.findall('TZID=(?P<name>[^:]+):', s)
|
||||
))
|
||||
s = s.upper()
|
||||
if not s.strip():
|
||||
raise ValueError("empty string")
|
||||
|
@ -1321,17 +1684,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 RDATE parm: "+parm)
|
||||
exdatevals.append(value)
|
||||
exdatevals.extend(
|
||||
self._parse_date_value(value, parms,
|
||||
TZID_NAMES, ignoretz,
|
||||
tzids, tzinfos)
|
||||
)
|
||||
elif name == "DTSTART":
|
||||
for parm in parms:
|
||||
raise ValueError("unsupported DTSTART parm: "+parm)
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
dtstart = parser.parse(value, ignoretz=ignoretz,
|
||||
tzinfos=tzinfos)
|
||||
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
|
||||
|
@ -1353,10 +1717,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
|
||||
|
@ -1370,6 +1731,7 @@ class _rrulestr(object):
|
|||
def __call__(self, s, **kwargs):
|
||||
return self._parse_rfc(s, **kwargs)
|
||||
|
||||
|
||||
rrulestr = _rrulestr()
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
|
|
0
lib/dateutil/test/__init__.py
Normal file
0
lib/dateutil/test/__init__.py
Normal file
233
lib/dateutil/test/_common.py
Normal file
233
lib/dateutil/test/_common.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
from __future__ import unicode_literals
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import warnings
|
||||
import tempfile
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class PicklableMixin(object):
|
||||
def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs):
|
||||
"""
|
||||
Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads``
|
||||
"""
|
||||
pkl = pickle.dumps(obj, **dump_kwargs)
|
||||
return pickle.loads(pkl, **load_kwargs)
|
||||
|
||||
def _get_nobj_file(self, obj, dump_kwargs, load_kwargs):
|
||||
"""
|
||||
Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on
|
||||
a temporary file.
|
||||
"""
|
||||
with tempfile.TemporaryFile('w+b') as pkl:
|
||||
pickle.dump(obj, pkl, **dump_kwargs)
|
||||
pkl.seek(0) # Reset the file to the beginning to read it
|
||||
nobj = pickle.load(pkl, **load_kwargs)
|
||||
|
||||
return nobj
|
||||
|
||||
def assertPicklable(self, obj, singleton=False, asfile=False,
|
||||
dump_kwargs=None, load_kwargs=None):
|
||||
"""
|
||||
Assert that an object can be pickled and unpickled. This assertion
|
||||
assumes that the desired behavior is that the unpickled object compares
|
||||
equal to the original object, but is not the same object.
|
||||
"""
|
||||
get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes
|
||||
dump_kwargs = dump_kwargs or {}
|
||||
load_kwargs = load_kwargs or {}
|
||||
|
||||
nobj = get_nobj(obj, dump_kwargs, load_kwargs)
|
||||
if not singleton:
|
||||
self.assertIsNot(obj, nobj)
|
||||
self.assertEqual(obj, nobj)
|
||||
|
||||
|
||||
class TZContextBase(object):
|
||||
"""
|
||||
Base class for a context manager which allows changing of time zones.
|
||||
|
||||
Subclasses may define a guard variable to either block or or allow time
|
||||
zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``.
|
||||
The default is that the guard variable must be affirmatively set.
|
||||
|
||||
Subclasses must define ``get_current_tz`` and ``set_current_tz``.
|
||||
"""
|
||||
_guard_var_name = "DATEUTIL_MAY_CHANGE_TZ"
|
||||
_guard_allows_change = True
|
||||
|
||||
def __init__(self, tzval):
|
||||
self.tzval = tzval
|
||||
self._old_tz = None
|
||||
|
||||
@classmethod
|
||||
def tz_change_allowed(cls):
|
||||
"""
|
||||
Class method used to query whether or not this class allows time zone
|
||||
changes.
|
||||
"""
|
||||
guard = bool(os.environ.get(cls._guard_var_name, False))
|
||||
|
||||
# _guard_allows_change gives the "default" behavior - if True, the
|
||||
# guard is overcoming a block. If false, the guard is causing a block.
|
||||
# Whether tz_change is allowed is therefore the XNOR of the two.
|
||||
return guard == cls._guard_allows_change
|
||||
|
||||
@classmethod
|
||||
def tz_change_disallowed_message(cls):
|
||||
""" Generate instructions on how to allow tz changes """
|
||||
msg = ('Changing time zone not allowed. Set {envar} to {gval} '
|
||||
'if you would like to allow this behavior')
|
||||
|
||||
return msg.format(envar=cls._guard_var_name,
|
||||
gval=cls._guard_allows_change)
|
||||
|
||||
def __enter__(self):
|
||||
if not self.tz_change_allowed():
|
||||
msg = self.tz_change_disallowed_message()
|
||||
pytest.skip(msg)
|
||||
|
||||
# If this is used outside of a test suite, we still want an error.
|
||||
raise ValueError(msg) # pragma: no cover
|
||||
|
||||
self._old_tz = self.get_current_tz()
|
||||
self.set_current_tz(self.tzval)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self._old_tz is not None:
|
||||
self.set_current_tz(self._old_tz)
|
||||
|
||||
self._old_tz = None
|
||||
|
||||
def get_current_tz(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_current_tz(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TZEnvContext(TZContextBase):
|
||||
"""
|
||||
Context manager that temporarily sets the `TZ` variable (for use on
|
||||
*nix-like systems). Because the effect is local to the shell anyway, this
|
||||
will apply *unless* a guard is set.
|
||||
|
||||
If you do not want the TZ environment variable set, you may set the
|
||||
``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value.
|
||||
"""
|
||||
_guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR"
|
||||
_guard_allows_change = False
|
||||
|
||||
def get_current_tz(self):
|
||||
return os.environ.get('TZ', UnsetTz)
|
||||
|
||||
def set_current_tz(self, tzval):
|
||||
if tzval is UnsetTz and 'TZ' in os.environ:
|
||||
del os.environ['TZ']
|
||||
else:
|
||||
os.environ['TZ'] = tzval
|
||||
|
||||
time.tzset()
|
||||
|
||||
|
||||
class TZWinContext(TZContextBase):
|
||||
"""
|
||||
Context manager for changing local time zone on Windows.
|
||||
|
||||
Because the effect of this is system-wide and global, it may have
|
||||
unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment
|
||||
variable to a truthy value before using this context manager.
|
||||
"""
|
||||
def get_current_tz(self):
|
||||
p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE)
|
||||
|
||||
ctzname, err = p.communicate()
|
||||
ctzname = ctzname.decode() # Popen returns
|
||||
|
||||
if p.returncode:
|
||||
raise OSError('Failed to get current time zone: ' + err)
|
||||
|
||||
return ctzname
|
||||
|
||||
def set_current_tz(self, tzname):
|
||||
p = subprocess.Popen('tzutil /s "' + tzname + '"')
|
||||
|
||||
out, err = p.communicate()
|
||||
|
||||
if p.returncode:
|
||||
raise OSError('Failed to set current time zone: ' +
|
||||
(err or 'Unknown error.'))
|
||||
|
||||
|
||||
###
|
||||
# Utility classes
|
||||
class NotAValueClass(object):
|
||||
"""
|
||||
A class analogous to NaN that has operations defined for any type.
|
||||
"""
|
||||
def _op(self, other):
|
||||
return self # Operation with NotAValue returns NotAValue
|
||||
|
||||
def _cmp(self, other):
|
||||
return False
|
||||
|
||||
__add__ = __radd__ = _op
|
||||
__sub__ = __rsub__ = _op
|
||||
__mul__ = __rmul__ = _op
|
||||
__div__ = __rdiv__ = _op
|
||||
__truediv__ = __rtruediv__ = _op
|
||||
__floordiv__ = __rfloordiv__ = _op
|
||||
|
||||
__lt__ = __rlt__ = _op
|
||||
__gt__ = __rgt__ = _op
|
||||
__eq__ = __req__ = _op
|
||||
__le__ = __rle__ = _op
|
||||
__ge__ = __rge__ = _op
|
||||
|
||||
|
||||
NotAValue = NotAValueClass()
|
||||
|
||||
|
||||
class ComparesEqualClass(object):
|
||||
"""
|
||||
A class that is always equal to whatever you compare it to.
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
def __le__(self, other):
|
||||
return True
|
||||
|
||||
def __ge__(self, other):
|
||||
return True
|
||||
|
||||
def __lt__(self, other):
|
||||
return False
|
||||
|
||||
def __gt__(self, other):
|
||||
return False
|
||||
|
||||
__req__ = __eq__
|
||||
__rne__ = __ne__
|
||||
__rle__ = __le__
|
||||
__rge__ = __ge__
|
||||
__rlt__ = __lt__
|
||||
__rgt__ = __gt__
|
||||
|
||||
|
||||
ComparesEqual = ComparesEqualClass()
|
||||
|
||||
|
||||
class UnsetTzClass(object):
|
||||
""" Sentinel class for unset time zone variable """
|
||||
pass
|
||||
|
||||
|
||||
UnsetTz = UnsetTzClass()
|
41
lib/dateutil/test/conftest.py
Normal file
41
lib/dateutil/test/conftest.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
# Configure pytest to ignore xfailing tests
|
||||
# See: https://stackoverflow.com/a/53198349/467366
|
||||
def pytest_collection_modifyitems(items):
|
||||
for item in items:
|
||||
marker_getter = getattr(item, 'get_closest_marker', None)
|
||||
|
||||
# Python 3.3 support
|
||||
if marker_getter is None:
|
||||
marker_getter = item.get_marker
|
||||
|
||||
marker = marker_getter('xfail')
|
||||
|
||||
# Need to query the args because conditional xfail tests still have
|
||||
# the xfail mark even if they are not expected to fail
|
||||
if marker and (not marker.args or marker.args[0]):
|
||||
item.add_marker(pytest.mark.no_cover)
|
||||
|
||||
|
||||
def set_tzpath():
|
||||
"""
|
||||
Sets the TZPATH variable if it's specified in an environment variable.
|
||||
"""
|
||||
tzpath = os.environ.get('DATEUTIL_TZPATH', None)
|
||||
|
||||
if tzpath is None:
|
||||
return
|
||||
|
||||
path_components = tzpath.split(':')
|
||||
|
||||
print("Setting TZPATH to {}".format(path_components))
|
||||
|
||||
from dateutil import tz
|
||||
tz.TZPATHS.clear()
|
||||
tz.TZPATHS.extend(path_components)
|
||||
|
||||
|
||||
set_tzpath()
|
27
lib/dateutil/test/property/test_isoparse_prop.py
Normal file
27
lib/dateutil/test/property/test_isoparse_prop.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from hypothesis import given, assume
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from dateutil import tz
|
||||
from dateutil.parser import isoparse
|
||||
|
||||
import pytest
|
||||
|
||||
# Strategies
|
||||
TIME_ZONE_STRATEGY = st.sampled_from([None, tz.UTC] +
|
||||
[tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific',
|
||||
'Australia/Sydney', 'Europe/London')])
|
||||
ASCII_STRATEGY = st.characters(max_codepoint=127)
|
||||
|
||||
|
||||
@pytest.mark.isoparser
|
||||
@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY)
|
||||
def test_timespec_auto(dt, sep):
|
||||
if dt.tzinfo is not None:
|
||||
# Assume offset has no sub-second components
|
||||
assume(dt.utcoffset().total_seconds() % 60 == 0)
|
||||
|
||||
sep = str(sep) # Python 2.7 requires bytes
|
||||
dtstr = dt.isoformat(sep=sep)
|
||||
dt_rt = isoparse(dtstr)
|
||||
|
||||
assert dt_rt == dt
|
22
lib/dateutil/test/property/test_parser_prop.py
Normal file
22
lib/dateutil/test/property/test_parser_prop.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from hypothesis.strategies import integers
|
||||
from hypothesis import given
|
||||
|
||||
import pytest
|
||||
|
||||
from dateutil.parser import parserinfo
|
||||
|
||||
|
||||
@pytest.mark.parserinfo
|
||||
@given(integers(min_value=100, max_value=9999))
|
||||
def test_convertyear(n):
|
||||
assert n == parserinfo().convertyear(n)
|
||||
|
||||
|
||||
@pytest.mark.parserinfo
|
||||
@given(integers(min_value=-50,
|
||||
max_value=49))
|
||||
def test_convertyear_no_specified_century(n):
|
||||
p = parserinfo()
|
||||
new_year = p._year + n
|
||||
result = p.convertyear(new_year % 100, century_specified=False)
|
||||
assert result == new_year
|
35
lib/dateutil/test/property/test_tz_prop.py
Normal file
35
lib/dateutil/test/property/test_tz_prop.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
import six
|
||||
from hypothesis import assume, given
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from dateutil import tz as tz
|
||||
|
||||
EPOCHALYPSE = datetime.fromtimestamp(2147483647)
|
||||
NEGATIVE_EPOCHALYPSE = datetime.fromtimestamp(0) - timedelta(seconds=2147483648)
|
||||
|
||||
|
||||
@pytest.mark.gettz
|
||||
@pytest.mark.parametrize("gettz_arg", [None, ""])
|
||||
# TODO: Remove bounds when GH #590 is resolved
|
||||
@given(
|
||||
dt=st.datetimes(
|
||||
min_value=NEGATIVE_EPOCHALYPSE, max_value=EPOCHALYPSE, timezones=st.just(tz.UTC),
|
||||
)
|
||||
)
|
||||
def test_gettz_returns_local(gettz_arg, dt):
|
||||
act_tz = tz.gettz(gettz_arg)
|
||||
if isinstance(act_tz, tz.tzlocal):
|
||||
return
|
||||
|
||||
dt_act = dt.astimezone(tz.gettz(gettz_arg))
|
||||
if six.PY2:
|
||||
dt_exp = dt.astimezone(tz.tzlocal())
|
||||
else:
|
||||
dt_exp = dt.astimezone()
|
||||
|
||||
assert dt_act == dt_exp
|
||||
assert dt_act.tzname() == dt_exp.tzname()
|
||||
assert dt_act.utcoffset() == dt_exp.utcoffset()
|
93
lib/dateutil/test/test_easter.py
Normal file
93
lib/dateutil/test/test_easter.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
from dateutil.easter import easter
|
||||
from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN
|
||||
|
||||
from datetime import date
|
||||
import pytest
|
||||
|
||||
# List of easters between 1990 and 2050
|
||||
western_easter_dates = [
|
||||
date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11),
|
||||
date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30),
|
||||
date(1998, 4, 12), date(1999, 4, 4),
|
||||
|
||||
date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20),
|
||||
date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8),
|
||||
date(2008, 3, 23), date(2009, 4, 12),
|
||||
|
||||
date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31),
|
||||
date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16),
|
||||
date(2018, 4, 1), date(2019, 4, 21),
|
||||
|
||||
date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9),
|
||||
date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28),
|
||||
date(2028, 4, 16), date(2029, 4, 1),
|
||||
|
||||
date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17),
|
||||
date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5),
|
||||
date(2038, 4, 25), date(2039, 4, 10),
|
||||
|
||||
date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29),
|
||||
date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14),
|
||||
date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10)
|
||||
]
|
||||
|
||||
orthodox_easter_dates = [
|
||||
date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18),
|
||||
date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27),
|
||||
date(1998, 4, 19), date(1999, 4, 11),
|
||||
|
||||
date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27),
|
||||
date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8),
|
||||
date(2008, 4, 27), date(2009, 4, 19),
|
||||
|
||||
date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5),
|
||||
date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16),
|
||||
date(2018, 4, 8), date(2019, 4, 28),
|
||||
|
||||
date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16),
|
||||
date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2),
|
||||
date(2028, 4, 16), date(2029, 4, 8),
|
||||
|
||||
date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24),
|
||||
date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5),
|
||||
date(2038, 4, 25), date(2039, 4, 17),
|
||||
|
||||
date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3),
|
||||
date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21),
|
||||
date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17)
|
||||
]
|
||||
|
||||
# A random smattering of Julian dates.
|
||||
# Pulled values from http://www.kevinlaughery.com/east4099.html
|
||||
julian_easter_dates = [
|
||||
date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31),
|
||||
date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19),
|
||||
date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8),
|
||||
date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14),
|
||||
date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6),
|
||||
date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30),
|
||||
date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24),
|
||||
date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17),
|
||||
date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3),
|
||||
date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("easter_date", western_easter_dates)
|
||||
def test_easter_western(easter_date):
|
||||
assert easter_date == easter(easter_date.year, EASTER_WESTERN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("easter_date", orthodox_easter_dates)
|
||||
def test_easter_orthodox(easter_date):
|
||||
assert easter_date == easter(easter_date.year, EASTER_ORTHODOX)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("easter_date", julian_easter_dates)
|
||||
def test_easter_julian(easter_date):
|
||||
assert easter_date == easter(easter_date.year, EASTER_JULIAN)
|
||||
|
||||
|
||||
def test_easter_bad_method():
|
||||
with pytest.raises(ValueError):
|
||||
easter(1975, 4)
|
33
lib/dateutil/test/test_import_star.py
Normal file
33
lib/dateutil/test/test_import_star.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""Test for the "import *" functionality.
|
||||
|
||||
As import * can be only done at module level, it has been added in a separate file
|
||||
"""
|
||||
import pytest
|
||||
|
||||
prev_locals = list(locals())
|
||||
from dateutil import *
|
||||
new_locals = {name:value for name,value in locals().items()
|
||||
if name not in prev_locals}
|
||||
new_locals.pop('prev_locals')
|
||||
|
||||
|
||||
@pytest.mark.import_star
|
||||
def test_imported_modules():
|
||||
""" Test that `from dateutil import *` adds modules in __all__ locally """
|
||||
import dateutil.easter
|
||||
import dateutil.parser
|
||||
import dateutil.relativedelta
|
||||
import dateutil.rrule
|
||||
import dateutil.tz
|
||||
import dateutil.utils
|
||||
import dateutil.zoneinfo
|
||||
|
||||
assert dateutil.easter == new_locals.pop("easter")
|
||||
assert dateutil.parser == new_locals.pop("parser")
|
||||
assert dateutil.relativedelta == new_locals.pop("relativedelta")
|
||||
assert dateutil.rrule == new_locals.pop("rrule")
|
||||
assert dateutil.tz == new_locals.pop("tz")
|
||||
assert dateutil.utils == new_locals.pop("utils")
|
||||
assert dateutil.zoneinfo == new_locals.pop("zoneinfo")
|
||||
|
||||
assert not new_locals
|
176
lib/dateutil/test/test_imports.py
Normal file
176
lib/dateutil/test/test_imports.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
import sys
|
||||
import pytest
|
||||
|
||||
|
||||
HOST_IS_WINDOWS = sys.platform.startswith('win')
|
||||
|
||||
|
||||
def test_import_version_str():
|
||||
""" Test that dateutil.__version__ can be imported"""
|
||||
from dateutil import __version__
|
||||
|
||||
|
||||
def test_import_version_root():
|
||||
import dateutil
|
||||
assert hasattr(dateutil, '__version__')
|
||||
|
||||
|
||||
# Test that dateutil.easter-related imports work properly
|
||||
def test_import_easter_direct():
|
||||
import dateutil.easter
|
||||
|
||||
|
||||
def test_import_easter_from():
|
||||
from dateutil import easter
|
||||
|
||||
|
||||
def test_import_easter_start():
|
||||
from dateutil.easter import easter
|
||||
|
||||
|
||||
# Test that dateutil.parser-related imports work properly
|
||||
def test_import_parser_direct():
|
||||
import dateutil.parser
|
||||
|
||||
|
||||
def test_import_parser_from():
|
||||
from dateutil import parser
|
||||
|
||||
|
||||
def test_import_parser_all():
|
||||
# All interface
|
||||
from dateutil.parser import parse
|
||||
from dateutil.parser import parserinfo
|
||||
|
||||
# Other public classes
|
||||
from dateutil.parser import parser
|
||||
|
||||
for var in (parse, parserinfo, parser):
|
||||
assert var is not None
|
||||
|
||||
|
||||
# Test that dateutil.relativedelta-related imports work properly
|
||||
def test_import_relative_delta_direct():
|
||||
import dateutil.relativedelta
|
||||
|
||||
|
||||
def test_import_relative_delta_from():
|
||||
from dateutil import relativedelta
|
||||
|
||||
def test_import_relative_delta_all():
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
|
||||
|
||||
for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU):
|
||||
assert var is not None
|
||||
|
||||
# In the public interface but not in all
|
||||
from dateutil.relativedelta import weekday
|
||||
assert weekday is not None
|
||||
|
||||
|
||||
# Test that dateutil.rrule related imports work properly
|
||||
def test_import_rrule_direct():
|
||||
import dateutil.rrule
|
||||
|
||||
|
||||
def test_import_rrule_from():
|
||||
from dateutil import rrule
|
||||
|
||||
|
||||
def test_import_rrule_all():
|
||||
from dateutil.rrule import rrule
|
||||
from dateutil.rrule import rruleset
|
||||
from dateutil.rrule import rrulestr
|
||||
from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY
|
||||
from dateutil.rrule import HOURLY, MINUTELY, SECONDLY
|
||||
from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU
|
||||
|
||||
rr_all = (rrule, rruleset, rrulestr,
|
||||
YEARLY, MONTHLY, WEEKLY, DAILY,
|
||||
HOURLY, MINUTELY, SECONDLY,
|
||||
MO, TU, WE, TH, FR, SA, SU)
|
||||
|
||||
for var in rr_all:
|
||||
assert var is not None
|
||||
|
||||
# In the public interface but not in all
|
||||
from dateutil.rrule import weekday
|
||||
assert weekday is not None
|
||||
|
||||
|
||||
# Test that dateutil.tz related imports work properly
|
||||
def test_import_tztest_direct():
|
||||
import dateutil.tz
|
||||
|
||||
|
||||
def test_import_tz_from():
|
||||
from dateutil import tz
|
||||
|
||||
|
||||
def test_import_tz_all():
|
||||
from dateutil.tz import tzutc
|
||||
from dateutil.tz import tzoffset
|
||||
from dateutil.tz import tzlocal
|
||||
from dateutil.tz import tzfile
|
||||
from dateutil.tz import tzrange
|
||||
from dateutil.tz import tzstr
|
||||
from dateutil.tz import tzical
|
||||
from dateutil.tz import gettz
|
||||
from dateutil.tz import tzwin
|
||||
from dateutil.tz import tzwinlocal
|
||||
from dateutil.tz import UTC
|
||||
from dateutil.tz import datetime_ambiguous
|
||||
from dateutil.tz import datetime_exists
|
||||
from dateutil.tz import resolve_imaginary
|
||||
|
||||
tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "gettz", "datetime_ambiguous",
|
||||
"datetime_exists", "resolve_imaginary", "UTC"]
|
||||
|
||||
tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else []
|
||||
lvars = locals()
|
||||
|
||||
for var in tz_all:
|
||||
assert lvars[var] is not None
|
||||
|
||||
# Test that dateutil.tzwin related imports work properly
|
||||
@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
|
||||
def test_import_tz_windows_direct():
|
||||
import dateutil.tzwin
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
|
||||
def test_import_tz_windows_from():
|
||||
from dateutil import tzwin
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows")
|
||||
def test_import_tz_windows_star():
|
||||
from dateutil.tzwin import tzwin
|
||||
from dateutil.tzwin import tzwinlocal
|
||||
|
||||
tzwin_all = [tzwin, tzwinlocal]
|
||||
|
||||
for var in tzwin_all:
|
||||
assert var is not None
|
||||
|
||||
|
||||
# Test imports of Zone Info
|
||||
def test_import_zone_info_direct():
|
||||
import dateutil.zoneinfo
|
||||
|
||||
|
||||
def test_import_zone_info_from():
|
||||
from dateutil import zoneinfo
|
||||
|
||||
|
||||
def test_import_zone_info_star():
|
||||
from dateutil.zoneinfo import gettz
|
||||
from dateutil.zoneinfo import gettz_db_metadata
|
||||
from dateutil.zoneinfo import rebuild
|
||||
|
||||
zi_all = (gettz, gettz_db_metadata, rebuild)
|
||||
|
||||
for var in zi_all:
|
||||
assert var is not None
|
91
lib/dateutil/test/test_internals.py
Normal file
91
lib/dateutil/test/test_internals.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for implementation details, not necessarily part of the user-facing
|
||||
API.
|
||||
|
||||
The motivating case for these tests is #483, where we want to smoke-test
|
||||
code that may be difficult to reach through the standard API calls.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from dateutil.parser._parser import _ymd
|
||||
from dateutil import tz
|
||||
|
||||
IS_PY32 = sys.version_info[0:2] == (3, 2)
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_YMD_could_be_day():
|
||||
ymd = _ymd('foo bar 124 baz')
|
||||
|
||||
ymd.append(2, 'M')
|
||||
assert ymd.has_month
|
||||
assert not ymd.has_year
|
||||
assert ymd.could_be_day(4)
|
||||
assert not ymd.could_be_day(-6)
|
||||
assert not ymd.could_be_day(32)
|
||||
|
||||
# Assumes leap year
|
||||
assert ymd.could_be_day(29)
|
||||
|
||||
ymd.append(1999)
|
||||
assert ymd.has_year
|
||||
assert not ymd.could_be_day(29)
|
||||
|
||||
ymd.append(16, 'D')
|
||||
assert ymd.has_day
|
||||
assert not ymd.could_be_day(1)
|
||||
|
||||
ymd = _ymd('foo bar 124 baz')
|
||||
ymd.append(1999)
|
||||
assert ymd.could_be_day(31)
|
||||
|
||||
|
||||
###
|
||||
# Test that private interfaces in _parser are deprecated properly
|
||||
@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
|
||||
def test_parser_private_warns():
|
||||
from dateutil.parser import _timelex, _tzparser
|
||||
from dateutil.parser import _parsetz
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
_tzparser()
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
_timelex('2014-03-03')
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
_parsetz('+05:00')
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2')
|
||||
def test_parser_parser_private_not_warns():
|
||||
from dateutil.parser._parser import _timelex, _tzparser
|
||||
from dateutil.parser._parser import _parsetz
|
||||
|
||||
with pytest.warns(None) as recorder:
|
||||
_tzparser()
|
||||
assert len(recorder) == 0
|
||||
|
||||
with pytest.warns(None) as recorder:
|
||||
_timelex('2014-03-03')
|
||||
|
||||
assert len(recorder) == 0
|
||||
|
||||
with pytest.warns(None) as recorder:
|
||||
_parsetz('+05:00')
|
||||
assert len(recorder) == 0
|
||||
|
||||
|
||||
@pytest.mark.tzstr
|
||||
def test_tzstr_internal_timedeltas():
|
||||
with pytest.warns(tz.DeprecatedTzFormatWarning):
|
||||
tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200")
|
||||
|
||||
with pytest.warns(tz.DeprecatedTzFormatWarning):
|
||||
tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200")
|
||||
|
||||
assert tz1._start_delta != tz2._start_delta
|
||||
assert tz1._end_delta != tz2._end_delta
|
509
lib/dateutil/test/test_isoparser.py
Normal file
509
lib/dateutil/test/test_isoparser.py
Normal file
|
@ -0,0 +1,509 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta, date, time
|
||||
import itertools as it
|
||||
|
||||
from dateutil import tz
|
||||
from dateutil.tz import UTC
|
||||
from dateutil.parser import isoparser, isoparse
|
||||
|
||||
import pytest
|
||||
import six
|
||||
|
||||
|
||||
def _generate_tzoffsets(limited):
|
||||
def _mkoffset(hmtuple, fmt):
|
||||
h, m = hmtuple
|
||||
m_td = (-1 if h < 0 else 1) * m
|
||||
|
||||
tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
|
||||
return tzo, fmt.format(h, m)
|
||||
|
||||
out = []
|
||||
if not limited:
|
||||
# The subset that's just hours
|
||||
hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
|
||||
out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])
|
||||
|
||||
# Ones that have hours and minutes
|
||||
hm_out = [] + hm_out_h
|
||||
hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
|
||||
else:
|
||||
hm_out = [(-5, -0)]
|
||||
|
||||
fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
|
||||
out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]
|
||||
|
||||
# Also add in UTC and naive
|
||||
out.append((UTC, 'Z'))
|
||||
out.append((None, ''))
|
||||
|
||||
return out
|
||||
|
||||
FULL_TZOFFSETS = _generate_tzoffsets(False)
|
||||
FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
|
||||
TZOFFSETS = _generate_tzoffsets(True)
|
||||
|
||||
DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
|
||||
@pytest.mark.parametrize('dt', tuple(DATES))
|
||||
def test_year_only(dt):
|
||||
dtstr = dt.strftime('%Y')
|
||||
|
||||
assert isoparse(dtstr) == dt
|
||||
|
||||
DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
|
||||
@pytest.mark.parametrize('dt', tuple(DATES))
|
||||
def test_year_month(dt):
|
||||
fmt = '%Y-%m'
|
||||
dtstr = dt.strftime(fmt)
|
||||
|
||||
assert isoparse(dtstr) == dt
|
||||
|
||||
DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
|
||||
YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
|
||||
@pytest.mark.parametrize('dt', tuple(DATES))
|
||||
@pytest.mark.parametrize('fmt', YMD_FMTS)
|
||||
def test_year_month_day(dt, fmt):
|
||||
dtstr = dt.strftime(fmt)
|
||||
|
||||
assert isoparse(dtstr) == dt
|
||||
|
||||
def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
|
||||
microsecond_precision=None):
|
||||
tzi, offset_str = tzoffset
|
||||
fmt = date_fmt + 'T' + time_fmt
|
||||
dt = dt.replace(tzinfo=tzi)
|
||||
dtstr = dt.strftime(fmt)
|
||||
|
||||
if microsecond_precision is not None:
|
||||
if not fmt.endswith('%f'): # pragma: nocover
|
||||
raise ValueError('Time format has no microseconds!')
|
||||
|
||||
if microsecond_precision != 6:
|
||||
dtstr = dtstr[:-(6 - microsecond_precision)]
|
||||
elif microsecond_precision > 6: # pragma: nocover
|
||||
raise ValueError('Precision must be 1-6')
|
||||
|
||||
dtstr += offset_str
|
||||
|
||||
assert isoparse(dtstr) == dt
|
||||
|
||||
DATETIMES = [datetime(1998, 4, 16, 12),
|
||||
datetime(2019, 11, 18, 23),
|
||||
datetime(2014, 12, 16, 4)]
|
||||
@pytest.mark.parametrize('dt', tuple(DATETIMES))
|
||||
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
|
||||
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
|
||||
def test_ymd_h(dt, date_fmt, tzoffset):
|
||||
_isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)
|
||||
|
||||
DATETIMES = [datetime(2012, 1, 6, 9, 37)]
|
||||
@pytest.mark.parametrize('dt', tuple(DATETIMES))
|
||||
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
|
||||
@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
|
||||
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
|
||||
def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
|
||||
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
|
||||
|
||||
DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
|
||||
datetime(2003, 8, 8, 14, 9, 14),
|
||||
datetime(2003, 4, 7, 6, 14, 59)]
|
||||
HMS_FMTS = ('%H%M%S', '%H:%M:%S')
|
||||
@pytest.mark.parametrize('dt', tuple(DATETIMES))
|
||||
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
|
||||
@pytest.mark.parametrize('time_fmt', HMS_FMTS)
|
||||
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
|
||||
def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
|
||||
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
|
||||
|
||||
DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
|
||||
@pytest.mark.parametrize('dt', tuple(DATETIMES))
|
||||
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
|
||||
@pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS
|
||||
for sep in '.,'))
|
||||
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
|
||||
@pytest.mark.parametrize('precision', list(range(3, 7)))
|
||||
def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
|
||||
# Truncate the microseconds to the desired precision for the representation
|
||||
dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))
|
||||
|
||||
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
|
||||
|
||||
###
|
||||
# Truncation of extra digits beyond microsecond precision
|
||||
@pytest.mark.parametrize('dt_str', [
|
||||
'2018-07-03T14:07:00.123456000001',
|
||||
'2018-07-03T14:07:00.123456999999',
|
||||
])
|
||||
def test_extra_subsecond_digits(dt_str):
|
||||
assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456)
|
||||
|
||||
@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
|
||||
def test_full_tzoffsets(tzoffset):
|
||||
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
|
||||
date_fmt = '%Y-%m-%d'
|
||||
time_fmt = '%H:%M:%S.%f'
|
||||
|
||||
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
|
||||
|
||||
@pytest.mark.parametrize('dt_str', [
|
||||
'2014-04-11T00',
|
||||
'2014-04-10T24',
|
||||
'2014-04-11T00:00',
|
||||
'2014-04-10T24:00',
|
||||
'2014-04-11T00:00:00',
|
||||
'2014-04-10T24:00:00',
|
||||
'2014-04-11T00:00:00.000',
|
||||
'2014-04-10T24:00:00.000',
|
||||
'2014-04-11T00:00:00.000000',
|
||||
'2014-04-10T24:00:00.000000']
|
||||
)
|
||||
def test_datetime_midnight(dt_str):
|
||||
assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
|
||||
|
||||
@pytest.mark.parametrize('datestr', [
|
||||
'2014-01-01',
|
||||
'20140101',
|
||||
])
|
||||
@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
|
||||
def test_isoparse_sep_none(datestr, sep):
|
||||
isostr = datestr + sep + '14:33:09'
|
||||
assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)
|
||||
|
||||
##
|
||||
# Uncommon date formats
|
||||
TIME_ARGS = ('time_args',
|
||||
((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
|
||||
for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
|
||||
TZOFFSETS)))
|
||||
|
||||
@pytest.mark.parametrize('isocal,dt_expected',[
|
||||
((2017, 10), datetime(2017, 3, 6)),
|
||||
((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year
|
||||
((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014
|
||||
])
|
||||
def test_isoweek(isocal, dt_expected):
|
||||
# TODO: Figure out how to parametrize this on formats, too
|
||||
for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
|
||||
dtstr = fmt.format(*isocal)
|
||||
assert isoparse(dtstr) == dt_expected
|
||||
|
||||
@pytest.mark.parametrize('isocal,dt_expected',[
|
||||
((2016, 13, 7), datetime(2016, 4, 3)),
|
||||
((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year
|
||||
((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year
|
||||
((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year
|
||||
])
|
||||
def test_isoweek_day(isocal, dt_expected):
|
||||
# TODO: Figure out how to parametrize this on formats, too
|
||||
for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
|
||||
dtstr = fmt.format(*isocal)
|
||||
assert isoparse(dtstr) == dt_expected
|
||||
|
||||
@pytest.mark.parametrize('isoord,dt_expected', [
|
||||
((2004, 1), datetime(2004, 1, 1)),
|
||||
((2016, 60), datetime(2016, 2, 29)),
|
||||
((2017, 60), datetime(2017, 3, 1)),
|
||||
((2016, 366), datetime(2016, 12, 31)),
|
||||
((2017, 365), datetime(2017, 12, 31))
|
||||
])
|
||||
def test_iso_ordinal(isoord, dt_expected):
|
||||
for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
|
||||
dtstr = fmt.format(*isoord)
|
||||
|
||||
assert isoparse(dtstr) == dt_expected
|
||||
|
||||
|
||||
###
|
||||
# Acceptance of bytes
|
||||
@pytest.mark.parametrize('isostr,dt', [
|
||||
(b'2014', datetime(2014, 1, 1)),
|
||||
(b'20140204', datetime(2014, 2, 4)),
|
||||
(b'2014-02-04', datetime(2014, 2, 4)),
|
||||
(b'2014-02-04T12', datetime(2014, 2, 4, 12)),
|
||||
(b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
|
||||
(b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
|
||||
(b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
|
||||
(b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
|
||||
(b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
|
||||
UTC)),
|
||||
(b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000,
|
||||
UTC)),
|
||||
(b'2014-02-04T12:30:15.224+05:00',
|
||||
datetime(2014, 2, 4, 12, 30, 15, 224000,
|
||||
tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
|
||||
def test_bytes(isostr, dt):
|
||||
assert isoparse(isostr) == dt
|
||||
|
||||
|
||||
###
|
||||
# Invalid ISO strings
|
||||
@pytest.mark.parametrize('isostr,exception', [
|
||||
('201', ValueError), # ISO string too short
|
||||
('2012-0425', ValueError), # Inconsistent date separators
|
||||
('201204-25', ValueError), # Inconsistent date separators
|
||||
('20120425T0120:00', ValueError), # Inconsistent time separators
|
||||
('20120425T01:2000', ValueError), # Inconsistent time separators
|
||||
('14:3015', ValueError), # Inconsistent time separator
|
||||
('20120425T012500-334', ValueError), # Wrong microsecond separator
|
||||
('2001-1', ValueError), # YYYY-M not valid
|
||||
('2012-04-9', ValueError), # YYYY-MM-D not valid
|
||||
('201204', ValueError), # YYYYMM not valid
|
||||
('20120411T03:30+', ValueError), # Time zone too short
|
||||
('20120411T03:30+1234567', ValueError), # Time zone too long
|
||||
('20120411T03:30-25:40', ValueError), # Time zone invalid
|
||||
('2012-1a', ValueError), # Invalid month
|
||||
('20120411T03:30+00:60', ValueError), # Time zone invalid minutes
|
||||
('20120411T03:30+00:61', ValueError), # Time zone invalid minutes
|
||||
('20120411T033030.123456012:00', # No sign in time zone
|
||||
ValueError),
|
||||
('2012-W00', ValueError), # Invalid ISO week
|
||||
('2012-W55', ValueError), # Invalid ISO week
|
||||
('2012-W01-0', ValueError), # Invalid ISO week day
|
||||
('2012-W01-8', ValueError), # Invalid ISO week day
|
||||
('2013-000', ValueError), # Invalid ordinal day
|
||||
('2013-366', ValueError), # Invalid ordinal day
|
||||
('2013366', ValueError), # Invalid ordinal day
|
||||
('2014-03-12Т12:30:14', ValueError), # Cyrillic T
|
||||
('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight
|
||||
('2014_W01-1', ValueError), # Invalid separator
|
||||
('2014W01-1', ValueError), # Inconsistent use of dashes
|
||||
('2014-W011', ValueError), # Inconsistent use of dashes
|
||||
|
||||
])
|
||||
def test_iso_raises(isostr, exception):
|
||||
with pytest.raises(exception):
|
||||
isoparse(isostr)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sep_act, valid_sep, exception', [
|
||||
('T', 'C', ValueError),
|
||||
('C', 'T', ValueError),
|
||||
])
|
||||
def test_iso_with_sep_raises(sep_act, valid_sep, exception):
|
||||
parser = isoparser(sep=valid_sep)
|
||||
isostr = '2012-04-25' + sep_act + '01:25:00'
|
||||
with pytest.raises(exception):
|
||||
parser.isoparse(isostr)
|
||||
|
||||
|
||||
###
|
||||
# Test ISOParser constructor
|
||||
@pytest.mark.parametrize('sep', [' ', '9', '🍛'])
|
||||
def test_isoparser_invalid_sep(sep):
|
||||
with pytest.raises(ValueError):
|
||||
isoparser(sep=sep)
|
||||
|
||||
|
||||
# This only fails on Python 3
|
||||
@pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only")
|
||||
def test_isoparser_byte_sep():
|
||||
dt = datetime(2017, 12, 6, 12, 30, 45)
|
||||
dt_str = dt.isoformat(sep=str('T'))
|
||||
|
||||
dt_rt = isoparser(sep=b'T').isoparse(dt_str)
|
||||
|
||||
assert dt == dt_rt
|
||||
|
||||
|
||||
###
|
||||
# Test parse_tzstr
|
||||
@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
|
||||
def test_parse_tzstr(tzoffset):
|
||||
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
|
||||
date_fmt = '%Y-%m-%d'
|
||||
time_fmt = '%H:%M:%S.%f'
|
||||
|
||||
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tzstr', [
|
||||
'-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
|
||||
])
|
||||
@pytest.mark.parametrize('zero_as_utc', [True, False])
|
||||
def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
|
||||
tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||
assert tzi == UTC
|
||||
assert (type(tzi) == tz.tzutc) == zero_as_utc
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tzstr,exception', [
|
||||
('00:00', ValueError), # No sign
|
||||
('05:00', ValueError), # No sign
|
||||
('_00:00', ValueError), # Invalid sign
|
||||
('+25:00', ValueError), # Offset too large
|
||||
('00:0000', ValueError), # String too long
|
||||
])
|
||||
def test_parse_tzstr_fails(tzstr, exception):
|
||||
with pytest.raises(exception):
|
||||
isoparser().parse_tzstr(tzstr)
|
||||
|
||||
###
|
||||
# Test parse_isodate
|
||||
def __make_date_examples():
|
||||
dates_no_day = [
|
||||
date(1999, 12, 1),
|
||||
date(2016, 2, 1)
|
||||
]
|
||||
|
||||
if not six.PY2:
|
||||
# strftime does not support dates before 1900 in Python 2
|
||||
dates_no_day.append(date(1000, 11, 1))
|
||||
|
||||
# Only one supported format for dates with no day
|
||||
o = zip(dates_no_day, it.repeat('%Y-%m'))
|
||||
|
||||
dates_w_day = [
|
||||
date(1969, 12, 31),
|
||||
date(1900, 1, 1),
|
||||
date(2016, 2, 29),
|
||||
date(2017, 11, 14)
|
||||
]
|
||||
|
||||
dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
|
||||
o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))
|
||||
|
||||
return list(o)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
|
||||
@pytest.mark.parametrize('as_bytes', [True, False])
|
||||
def test_parse_isodate(d, dt_fmt, as_bytes):
|
||||
d_str = d.strftime(dt_fmt)
|
||||
if isinstance(d_str, six.text_type) and as_bytes:
|
||||
d_str = d_str.encode('ascii')
|
||||
elif isinstance(d_str, bytes) and not as_bytes:
|
||||
d_str = d_str.decode('ascii')
|
||||
|
||||
iparser = isoparser()
|
||||
assert iparser.parse_isodate(d_str) == d
|
||||
|
||||
|
||||
@pytest.mark.parametrize('isostr,exception', [
|
||||
('243', ValueError), # ISO string too short
|
||||
('2014-0423', ValueError), # Inconsistent date separators
|
||||
('201404-23', ValueError), # Inconsistent date separators
|
||||
('2014日03月14', ValueError), # Not ASCII
|
||||
('2013-02-29', ValueError), # Not a leap year
|
||||
('2014/12/03', ValueError), # Wrong separators
|
||||
('2014-04-19T', ValueError), # Unknown components
|
||||
('201202', ValueError), # Invalid format
|
||||
])
|
||||
def test_isodate_raises(isostr, exception):
|
||||
with pytest.raises(exception):
|
||||
isoparser().parse_isodate(isostr)
|
||||
|
||||
|
||||
def test_parse_isodate_error_text():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
isoparser().parse_isodate('2014-0423')
|
||||
|
||||
# ensure the error message does not contain b' prefixes
|
||||
if six.PY2:
|
||||
expected_error = "String contains unknown ISO components: u'2014-0423'"
|
||||
else:
|
||||
expected_error = "String contains unknown ISO components: '2014-0423'"
|
||||
assert expected_error == str(excinfo.value)
|
||||
|
||||
|
||||
###
|
||||
# Test parse_isotime
|
||||
def __make_time_examples():
|
||||
outputs = []
|
||||
|
||||
# HH
|
||||
time_h = [time(0), time(8), time(22)]
|
||||
time_h_fmts = ['%H']
|
||||
|
||||
outputs.append(it.product(time_h, time_h_fmts))
|
||||
|
||||
# HHMM / HH:MM
|
||||
time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
|
||||
time_hm_fmts = ['%H%M', '%H:%M']
|
||||
|
||||
outputs.append(it.product(time_hm, time_hm_fmts))
|
||||
|
||||
# HHMMSS / HH:MM:SS
|
||||
time_hms = [time(0, 0, 0), time(0, 15, 30),
|
||||
time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]
|
||||
|
||||
time_hms_fmts = ['%H%M%S', '%H:%M:%S']
|
||||
|
||||
outputs.append(it.product(time_hms, time_hms_fmts))
|
||||
|
||||
# HHMMSS.ffffff / HH:MM:SS.ffffff
|
||||
time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
|
||||
time(14, 21, 59, 948730),
|
||||
time(23, 59, 59, 999999)]
|
||||
|
||||
time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']
|
||||
|
||||
outputs.append(it.product(time_hmsu, time_hmsu_fmts))
|
||||
|
||||
outputs = list(map(list, outputs))
|
||||
|
||||
# Time zones
|
||||
ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
|
||||
o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr))
|
||||
o = ((t.replace(tzinfo=tzi), fmt + off_str)
|
||||
for (t, fmt), (tzi, off_str) in o)
|
||||
|
||||
outputs.append(o)
|
||||
|
||||
return list(it.chain.from_iterable(outputs))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
|
||||
@pytest.mark.parametrize('as_bytes', [True, False])
|
||||
def test_isotime(time_val, time_fmt, as_bytes):
|
||||
tstr = time_val.strftime(time_fmt)
|
||||
if isinstance(tstr, six.text_type) and as_bytes:
|
||||
tstr = tstr.encode('ascii')
|
||||
elif isinstance(tstr, bytes) and not as_bytes:
|
||||
tstr = tstr.decode('ascii')
|
||||
|
||||
iparser = isoparser()
|
||||
|
||||
assert iparser.parse_isotime(tstr) == time_val
|
||||
|
||||
|
||||
@pytest.mark.parametrize('isostr', [
|
||||
'24:00',
|
||||
'2400',
|
||||
'24:00:00',
|
||||
'240000',
|
||||
'24:00:00.000',
|
||||
'24:00:00,000',
|
||||
'24:00:00.000000',
|
||||
'24:00:00,000000',
|
||||
])
|
||||
def test_isotime_midnight(isostr):
|
||||
iparser = isoparser()
|
||||
assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('isostr,exception', [
|
||||
('3', ValueError), # ISO string too short
|
||||
('14時30分15秒', ValueError), # Not ASCII
|
||||
('14_30_15', ValueError), # Invalid separators
|
||||
('1430:15', ValueError), # Inconsistent separator use
|
||||
('25', ValueError), # Invalid hours
|
||||
('25:15', ValueError), # Invalid hours
|
||||
('14:60', ValueError), # Invalid minutes
|
||||
('14:59:61', ValueError), # Invalid seconds
|
||||
('14:30:15.34468305:00', ValueError), # No sign in time zone
|
||||
('14:30:15+', ValueError), # Time zone too short
|
||||
('14:30:15+1234567', ValueError), # Time zone invalid
|
||||
('14:59:59+25:00', ValueError), # Invalid tz hours
|
||||
('14:59:59+12:62', ValueError), # Invalid tz minutes
|
||||
('14:59:30_344583', ValueError), # Invalid microsecond separator
|
||||
('24:01', ValueError), # 24 used for non-midnight time
|
||||
('24:00:01', ValueError), # 24 used for non-midnight time
|
||||
('24:00:00.001', ValueError), # 24 used for non-midnight time
|
||||
('24:00:00.000001', ValueError), # 24 used for non-midnight time
|
||||
])
|
||||
def test_isotime_raises(isostr, exception):
|
||||
iparser = isoparser()
|
||||
with pytest.raises(exception):
|
||||
iparser.parse_isotime(isostr)
|
964
lib/dateutil/test/test_parser.py
Normal file
964
lib/dateutil/test/test_parser.py
Normal file
|
@ -0,0 +1,964 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
from datetime import datetime, timedelta
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
from dateutil import tz
|
||||
from dateutil.tz import tzoffset
|
||||
from dateutil.parser import parse, parserinfo
|
||||
from dateutil.parser import ParserError
|
||||
from dateutil.parser import UnknownTimezoneWarning
|
||||
|
||||
from ._common import TZEnvContext
|
||||
|
||||
from six import assertRaisesRegex, PY2
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
# Platform info
|
||||
IS_WIN = sys.platform.startswith('win')
|
||||
|
||||
PLATFORM_HAS_DASH_D = False
|
||||
try:
|
||||
if datetime.now().strftime('%-d'):
|
||||
PLATFORM_HAS_DASH_D = True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(params=[True, False])
|
||||
def fuzzy(request):
|
||||
"""Fixture to pass fuzzy=True or fuzzy=False to parse"""
|
||||
return request.param
|
||||
|
||||
|
||||
# Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message)
|
||||
PARSER_TEST_CASES = [
|
||||
("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"),
|
||||
("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"),
|
||||
("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"),
|
||||
("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"),
|
||||
("2003-09-25", datetime(2003, 9, 25), "iso format strip"),
|
||||
("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"),
|
||||
("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"),
|
||||
("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"),
|
||||
("20030925", datetime(2003, 9, 25), "iso stripped format strip"),
|
||||
("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"),
|
||||
("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"),
|
||||
("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"),
|
||||
("09-25-2003", datetime(2003, 9, 25), "date with dash"),
|
||||
("25-09-2003", datetime(2003, 9, 25), "date with dash"),
|
||||
("10-09-2003", datetime(2003, 10, 9), "date with dash"),
|
||||
("10-09-03", datetime(2003, 10, 9), "date with dash"),
|
||||
("2003.09.25", datetime(2003, 9, 25), "date with dot"),
|
||||
("09.25.2003", datetime(2003, 9, 25), "date with dot"),
|
||||
("25.09.2003", datetime(2003, 9, 25), "date with dot"),
|
||||
("10.09.2003", datetime(2003, 10, 9), "date with dot"),
|
||||
("10.09.03", datetime(2003, 10, 9), "date with dot"),
|
||||
("2003/09/25", datetime(2003, 9, 25), "date with slash"),
|
||||
("09/25/2003", datetime(2003, 9, 25), "date with slash"),
|
||||
("25/09/2003", datetime(2003, 9, 25), "date with slash"),
|
||||
("10/09/2003", datetime(2003, 10, 9), "date with slash"),
|
||||
("10/09/03", datetime(2003, 10, 9), "date with slash"),
|
||||
("2003 09 25", datetime(2003, 9, 25), "date with space"),
|
||||
("09 25 2003", datetime(2003, 9, 25), "date with space"),
|
||||
("25 09 2003", datetime(2003, 9, 25), "date with space"),
|
||||
("10 09 2003", datetime(2003, 10, 9), "date with space"),
|
||||
("10 09 03", datetime(2003, 10, 9), "date with space"),
|
||||
("25 09 03", datetime(2003, 9, 25), "date with space"),
|
||||
("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"),
|
||||
("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"),
|
||||
(" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"),
|
||||
("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"),
|
||||
("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"),
|
||||
("July 4, 1976", datetime(1976, 7, 4), "random format"),
|
||||
("7 4 1976", datetime(1976, 7, 4), "random format"),
|
||||
("4 jul 1976", datetime(1976, 7, 4), "random format"),
|
||||
("4 Jul 1976", datetime(1976, 7, 4), "'%-d %b %Y' format"),
|
||||
("7-4-76", datetime(1976, 7, 4), "random format"),
|
||||
("19760704", datetime(1976, 7, 4), "random format"),
|
||||
("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"),
|
||||
("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"),
|
||||
("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"),
|
||||
("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"),
|
||||
("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"),
|
||||
("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"),
|
||||
("3rd of May 2001", datetime(2001, 5, 3), "random format"),
|
||||
("5th of March 2001", datetime(2001, 3, 5), "random format"),
|
||||
("1st of May 2003", datetime(2003, 5, 1), "random format"),
|
||||
('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"),
|
||||
('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"),
|
||||
("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"),
|
||||
('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"),
|
||||
('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"),
|
||||
('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)"),
|
||||
|
||||
# Cases with legacy h/m/s format, candidates for deprecation (GH#886)
|
||||
("2016-12-21 04.2h", datetime(2016, 12, 21, 4, 12), "Fractional Hours"),
|
||||
]
|
||||
# Check that we don't have any duplicates
|
||||
assert len(set([x[0] for x in PARSER_TEST_CASES])) == len(PARSER_TEST_CASES)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES)
|
||||
def test_parser(parsable_text, expected_datetime, assertion_message):
|
||||
assert parse(parsable_text) == expected_datetime, assertion_message
|
||||
|
||||
|
||||
# Parser test cases using datetime(2003, 9, 25) as a default.
|
||||
# Format: (parsable_text, expected_datetime, assertion_message)
|
||||
PARSER_DEFAULT_TEST_CASES = [
|
||||
("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"),
|
||||
("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"),
|
||||
("Sep 2003", datetime(2003, 9, 25), "date command format strip"),
|
||||
("Sep", datetime(2003, 9, 25), "date command format strip"),
|
||||
("2003", datetime(2003, 9, 25), "date command format strip"),
|
||||
("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"),
|
||||
("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"),
|
||||
("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"),
|
||||
("10h", datetime(2003, 9, 25, 10), "hour with letters strip"),
|
||||
("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"),
|
||||
("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"),
|
||||
("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"),
|
||||
("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"),
|
||||
("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"),
|
||||
("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"),
|
||||
("10h am", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10h pm", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("10am", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10pm", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("10:00am", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"),
|
||||
("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"),
|
||||
("Wed", datetime(2003, 10, 1), "weekday alone"),
|
||||
("Wednesday", datetime(2003, 10, 1), "long weekday"),
|
||||
("October", datetime(2003, 10, 25), "long month"),
|
||||
("31-Dec-00", datetime(2000, 12, 31), "zero year"),
|
||||
("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"),
|
||||
("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"),
|
||||
("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"),
|
||||
("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"),
|
||||
("01h02", datetime(2003, 9, 25, 1, 2), "random format"),
|
||||
("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"),
|
||||
("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"),
|
||||
("01m02h", datetime(2003, 9, 25, 2, 1), "random format"),
|
||||
("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format")
|
||||
]
|
||||
# Check that we don't have any duplicates
|
||||
assert len(set([x[0] for x in PARSER_DEFAULT_TEST_CASES])) == len(PARSER_DEFAULT_TEST_CASES)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES)
|
||||
def test_parser_default(parsable_text, expected_datetime, assertion_message):
|
||||
assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sep', ['-', '.', '/', ' '])
|
||||
def test_parse_dayfirst(sep):
|
||||
expected = datetime(2003, 9, 10)
|
||||
fmt = sep.join(['%d', '%m', '%Y'])
|
||||
dstr = expected.strftime(fmt)
|
||||
result = parse(dstr, dayfirst=True)
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sep', ['-', '.', '/', ' '])
|
||||
def test_parse_yearfirst(sep):
|
||||
expected = datetime(2010, 9, 3)
|
||||
fmt = sep.join(['%Y', '%m', '%d'])
|
||||
dstr = expected.strftime(fmt)
|
||||
result = parse(dstr, yearfirst=True)
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dstr,expected', [
|
||||
("Thu Sep 25 10:36:28 BRST 2003", datetime(2003, 9, 25, 10, 36, 28)),
|
||||
("1996.07.10 AD at 15:08:56 PDT", datetime(1996, 7, 10, 15, 8, 56)),
|
||||
("Tuesday, April 12, 1952 AD 3:30:42pm PST",
|
||||
datetime(1952, 4, 12, 15, 30, 42)),
|
||||
("November 5, 1994, 8:15:30 am EST", datetime(1994, 11, 5, 8, 15, 30)),
|
||||
("1994-11-05T08:15:30-05:00", datetime(1994, 11, 5, 8, 15, 30)),
|
||||
("1994-11-05T08:15:30Z", datetime(1994, 11, 5, 8, 15, 30)),
|
||||
("1976-07-04T00:01:02Z", datetime(1976, 7, 4, 0, 1, 2)),
|
||||
("1986-07-05T08:15:30z", datetime(1986, 7, 5, 8, 15, 30)),
|
||||
("Tue Apr 4 00:22:12 PDT 1995", datetime(1995, 4, 4, 0, 22, 12)),
|
||||
])
|
||||
def test_parse_ignoretz(dstr, expected):
|
||||
result = parse(dstr, ignoretz=True)
|
||||
assert result == expected
|
||||
|
||||
|
||||
_brsttz = tzoffset("BRST", -10800)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dstr,expected', [
|
||||
("20030925T104941-0300",
|
||||
datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
|
||||
("Thu, 25 Sep 2003 10:49:41 -0300",
|
||||
datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
|
||||
("2003-09-25T10:49:41.5-03:00",
|
||||
datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)),
|
||||
("2003-09-25T10:49:41-03:00",
|
||||
datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)),
|
||||
("20030925T104941.5-0300",
|
||||
datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)),
|
||||
])
|
||||
def test_parse_with_tzoffset(dstr, expected):
|
||||
# In these cases, we are _not_ passing a tzinfos arg
|
||||
result = parse(dstr)
|
||||
assert result == expected
|
||||
|
||||
|
||||
class TestFormat(object):
|
||||
|
||||
def test_ybd(self):
|
||||
# If we have a 4-digit year, a non-numeric month (abbreviated or not),
|
||||
# and a day (1 or 2 digits), then there is no ambiguity as to which
|
||||
# token is a year/month/day. This holds regardless of what order the
|
||||
# terms are in and for each of the separators below.
|
||||
|
||||
seps = ['-', ' ', '/', '.']
|
||||
|
||||
year_tokens = ['%Y']
|
||||
month_tokens = ['%b', '%B']
|
||||
day_tokens = ['%d']
|
||||
if PLATFORM_HAS_DASH_D:
|
||||
day_tokens.append('%-d')
|
||||
|
||||
prods = itertools.product(year_tokens, month_tokens, day_tokens)
|
||||
perms = [y for x in prods for y in itertools.permutations(x)]
|
||||
unambig_fmts = [sep.join(perm) for sep in seps for perm in perms]
|
||||
|
||||
actual = datetime(2003, 9, 25)
|
||||
|
||||
for fmt in unambig_fmts:
|
||||
dstr = actual.strftime(fmt)
|
||||
res = parse(dstr)
|
||||
assert res == actual
|
||||
|
||||
# TODO: some redundancy with PARSER_TEST_CASES cases
|
||||
@pytest.mark.parametrize("fmt,dstr", [
|
||||
("%a %b %d %Y", "Thu Sep 25 2003"),
|
||||
("%b %d %Y", "Sep 25 2003"),
|
||||
("%Y-%m-%d", "2003-09-25"),
|
||||
("%Y%m%d", "20030925"),
|
||||
("%Y-%b-%d", "2003-Sep-25"),
|
||||
("%d-%b-%Y", "25-Sep-2003"),
|
||||
("%b-%d-%Y", "Sep-25-2003"),
|
||||
("%m-%d-%Y", "09-25-2003"),
|
||||
("%d-%m-%Y", "25-09-2003"),
|
||||
("%Y.%m.%d", "2003.09.25"),
|
||||
("%Y.%b.%d", "2003.Sep.25"),
|
||||
("%d.%b.%Y", "25.Sep.2003"),
|
||||
("%b.%d.%Y", "Sep.25.2003"),
|
||||
("%m.%d.%Y", "09.25.2003"),
|
||||
("%d.%m.%Y", "25.09.2003"),
|
||||
("%Y/%m/%d", "2003/09/25"),
|
||||
("%Y/%b/%d", "2003/Sep/25"),
|
||||
("%d/%b/%Y", "25/Sep/2003"),
|
||||
("%b/%d/%Y", "Sep/25/2003"),
|
||||
("%m/%d/%Y", "09/25/2003"),
|
||||
("%d/%m/%Y", "25/09/2003"),
|
||||
("%Y %m %d", "2003 09 25"),
|
||||
("%Y %b %d", "2003 Sep 25"),
|
||||
("%d %b %Y", "25 Sep 2003"),
|
||||
("%m %d %Y", "09 25 2003"),
|
||||
("%d %m %Y", "25 09 2003"),
|
||||
("%y %d %b", "03 25 Sep",),
|
||||
])
|
||||
def test_strftime_formats_2003Sep25(self, fmt, dstr):
|
||||
expected = datetime(2003, 9, 25)
|
||||
|
||||
# First check that the format strings behave as expected
|
||||
# (not strictly necessary, but nice to have)
|
||||
assert expected.strftime(fmt) == dstr
|
||||
|
||||
res = parse(dstr)
|
||||
assert res == expected
|
||||
|
||||
|
||||
class TestInputTypes(object):
|
||||
def test_empty_string_invalid(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse('')
|
||||
|
||||
def test_none_invalid(self):
|
||||
with pytest.raises(TypeError):
|
||||
parse(None)
|
||||
|
||||
def test_int_invalid(self):
|
||||
with pytest.raises(TypeError):
|
||||
parse(13)
|
||||
|
||||
def test_duck_typing(self):
|
||||
# We want to support arbitrary classes that implement the stream
|
||||
# interface.
|
||||
|
||||
class StringPassThrough(object):
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
return self.stream.read(*args, **kwargs)
|
||||
|
||||
dstr = StringPassThrough(StringIO('2014 January 19'))
|
||||
|
||||
res = parse(dstr)
|
||||
expected = datetime(2014, 1, 19)
|
||||
assert res == expected
|
||||
|
||||
def test_parse_stream(self):
|
||||
dstr = StringIO('2014 January 19')
|
||||
|
||||
res = parse(dstr)
|
||||
expected = datetime(2014, 1, 19)
|
||||
assert res == expected
|
||||
|
||||
def test_parse_str(self):
|
||||
# Parser should be able to handle bytestring and unicode
|
||||
uni_str = '2014-05-01 08:00:00'
|
||||
bytes_str = uni_str.encode()
|
||||
|
||||
res = parse(bytes_str)
|
||||
expected = parse(uni_str)
|
||||
assert res == expected
|
||||
|
||||
def test_parse_bytes(self):
|
||||
res = parse(b'2014 January 19')
|
||||
expected = datetime(2014, 1, 19)
|
||||
assert res == expected
|
||||
|
||||
def test_parse_bytearray(self):
|
||||
# GH#417
|
||||
res = parse(bytearray(b'2014 January 19'))
|
||||
expected = datetime(2014, 1, 19)
|
||||
assert res == expected
|
||||
|
||||
|
||||
class TestTzinfoInputTypes(object):
|
||||
def assert_equal_same_tz(self, dt1, dt2):
|
||||
assert dt1 == dt2
|
||||
assert dt1.tzinfo is dt2.tzinfo
|
||||
|
||||
def test_tzinfo_dict_could_return_none(self):
|
||||
dstr = "2017-02-03 12:40 BRST"
|
||||
result = parse(dstr, tzinfos={"BRST": None})
|
||||
expected = datetime(2017, 2, 3, 12, 40)
|
||||
self.assert_equal_same_tz(result, expected)
|
||||
|
||||
def test_tzinfos_callable_could_return_none(self):
|
||||
dstr = "2017-02-03 12:40 BRST"
|
||||
result = parse(dstr, tzinfos=lambda *args: None)
|
||||
expected = datetime(2017, 2, 3, 12, 40)
|
||||
self.assert_equal_same_tz(result, expected)
|
||||
|
||||
def test_invalid_tzinfo_input(self):
|
||||
dstr = "2014 January 19 09:00 UTC"
|
||||
# Pass an absurd tzinfos object
|
||||
tzinfos = {"UTC": ValueError}
|
||||
with pytest.raises(TypeError):
|
||||
parse(dstr, tzinfos=tzinfos)
|
||||
|
||||
def test_valid_tzinfo_tzinfo_input(self):
|
||||
dstr = "2014 January 19 09:00 UTC"
|
||||
tzinfos = {"UTC": tz.UTC}
|
||||
expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC)
|
||||
res = parse(dstr, tzinfos=tzinfos)
|
||||
self.assert_equal_same_tz(res, expected)
|
||||
|
||||
def test_valid_tzinfo_unicode_input(self):
|
||||
dstr = "2014 January 19 09:00 UTC"
|
||||
tzinfos = {u"UTC": u"UTC+0"}
|
||||
expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0"))
|
||||
res = parse(dstr, tzinfos=tzinfos)
|
||||
self.assert_equal_same_tz(res, expected)
|
||||
|
||||
def test_valid_tzinfo_callable_input(self):
|
||||
dstr = "2014 January 19 09:00 UTC"
|
||||
|
||||
def tzinfos(*args, **kwargs):
|
||||
return u"UTC+0"
|
||||
|
||||
expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0"))
|
||||
res = parse(dstr, tzinfos=tzinfos)
|
||||
self.assert_equal_same_tz(res, expected)
|
||||
|
||||
def test_valid_tzinfo_int_input(self):
|
||||
dstr = "2014 January 19 09:00 UTC"
|
||||
tzinfos = {u"UTC": -28800}
|
||||
expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800))
|
||||
res = parse(dstr, tzinfos=tzinfos)
|
||||
self.assert_equal_same_tz(res, expected)
|
||||
|
||||
|
||||
class ParserTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.tzinfos = {"BRST": -10800}
|
||||
cls.brsttz = tzoffset("BRST", -10800)
|
||||
cls.default = datetime(2003, 9, 25)
|
||||
|
||||
# Parser should be able to handle bytestring and unicode
|
||||
cls.uni_str = '2014-05-01 08:00:00'
|
||||
cls.str_str = cls.uni_str.encode()
|
||||
|
||||
def testParserParseStr(self):
|
||||
from dateutil.parser import parser
|
||||
|
||||
assert parser().parse(self.str_str) == parser().parse(self.uni_str)
|
||||
|
||||
def testParseUnicodeWords(self):
|
||||
|
||||
class rus_parserinfo(parserinfo):
|
||||
MONTHS = [("янв", "Январь"),
|
||||
("фев", "Февраль"),
|
||||
("мар", "Март"),
|
||||
("апр", "Апрель"),
|
||||
("май", "Май"),
|
||||
("июн", "Июнь"),
|
||||
("июл", "Июль"),
|
||||
("авг", "Август"),
|
||||
("сен", "Сентябрь"),
|
||||
("окт", "Октябрь"),
|
||||
("ноя", "Ноябрь"),
|
||||
("дек", "Декабрь")]
|
||||
|
||||
expected = datetime(2015, 9, 10, 10, 20)
|
||||
res = parse('10 Сентябрь 2015 10:20', parserinfo=rus_parserinfo())
|
||||
assert res == expected
|
||||
|
||||
def testParseWithNulls(self):
|
||||
# This relies on the from __future__ import unicode_literals, because
|
||||
# explicitly specifying a unicode literal is a syntax error in Py 3.2
|
||||
# May want to switch to u'...' if we ever drop Python 3.2 support.
|
||||
pstring = '\x00\x00August 29, 1924'
|
||||
|
||||
assert parse(pstring) == datetime(1924, 8, 29)
|
||||
|
||||
def testDateCommandFormat(self):
|
||||
self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
|
||||
tzinfos=self.tzinfos),
|
||||
datetime(2003, 9, 25, 10, 36, 28,
|
||||
tzinfo=self.brsttz))
|
||||
|
||||
def testDateCommandFormatReversed(self):
|
||||
self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu",
|
||||
tzinfos=self.tzinfos),
|
||||
datetime(2003, 9, 25, 10, 36, 28,
|
||||
tzinfo=self.brsttz))
|
||||
|
||||
def testDateCommandFormatWithLong(self):
|
||||
if PY2:
|
||||
self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
|
||||
tzinfos={"BRST": long(-10800)}),
|
||||
datetime(2003, 9, 25, 10, 36, 28,
|
||||
tzinfo=self.brsttz))
|
||||
|
||||
def testISOFormatStrip2(self):
|
||||
self.assertEqual(parse("2003-09-25T10:49:41+03:00"),
|
||||
datetime(2003, 9, 25, 10, 49, 41,
|
||||
tzinfo=tzoffset(None, 10800)))
|
||||
|
||||
def testISOStrippedFormatStrip2(self):
|
||||
self.assertEqual(parse("20030925T104941+0300"),
|
||||
datetime(2003, 9, 25, 10, 49, 41,
|
||||
tzinfo=tzoffset(None, 10800)))
|
||||
|
||||
def testAMPMNoHour(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse("AM")
|
||||
|
||||
with pytest.raises(ParserError):
|
||||
parse("Jan 20, 2015 PM")
|
||||
|
||||
def testAMPMRange(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse("13:44 AM")
|
||||
|
||||
with pytest.raises(ParserError):
|
||||
parse("January 25, 1921 23:13 PM")
|
||||
|
||||
def testPertain(self):
|
||||
self.assertEqual(parse("Sep 03", default=self.default),
|
||||
datetime(2003, 9, 3))
|
||||
self.assertEqual(parse("Sep of 03", default=self.default),
|
||||
datetime(2003, 9, 25))
|
||||
|
||||
def testFuzzy(self):
|
||||
s = "Today is 25 of September of 2003, exactly " \
|
||||
"at 10:49:41 with timezone -03:00."
|
||||
self.assertEqual(parse(s, fuzzy=True),
|
||||
datetime(2003, 9, 25, 10, 49, 41,
|
||||
tzinfo=self.brsttz))
|
||||
|
||||
def testFuzzyWithTokens(self):
|
||||
s1 = "Today is 25 of September of 2003, exactly " \
|
||||
"at 10:49:41 with timezone -03:00."
|
||||
self.assertEqual(parse(s1, fuzzy_with_tokens=True),
|
||||
(datetime(2003, 9, 25, 10, 49, 41,
|
||||
tzinfo=self.brsttz),
|
||||
('Today is ', 'of ', ', exactly at ',
|
||||
' with timezone ', '.')))
|
||||
|
||||
s2 = "http://biz.yahoo.com/ipo/p/600221.html"
|
||||
self.assertEqual(parse(s2, fuzzy_with_tokens=True),
|
||||
(datetime(2060, 2, 21, 0, 0, 0),
|
||||
('http://biz.yahoo.com/ipo/p/', '.html')))
|
||||
|
||||
def testFuzzyAMPMProblem(self):
|
||||
# Sometimes fuzzy parsing results in AM/PM flag being set without
|
||||
# hours - if it's fuzzy it should ignore that.
|
||||
s1 = "I have a meeting on March 1, 1974."
|
||||
s2 = "On June 8th, 2020, I am going to be the first man on Mars"
|
||||
|
||||
# Also don't want any erroneous AM or PMs changing the parsed time
|
||||
s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003"
|
||||
s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset"
|
||||
|
||||
self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1))
|
||||
self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8))
|
||||
self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3))
|
||||
self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3))
|
||||
|
||||
def testFuzzyIgnoreAMPM(self):
|
||||
s1 = "Jan 29, 1945 14:45 AM I going to see you there?"
|
||||
with pytest.warns(UnknownTimezoneWarning):
|
||||
res = parse(s1, fuzzy=True)
|
||||
self.assertEqual(res, datetime(1945, 1, 29, 14, 45))
|
||||
|
||||
def testRandomFormat24(self):
|
||||
self.assertEqual(parse("0:00 PM, PST", default=self.default,
|
||||
ignoretz=True),
|
||||
datetime(2003, 9, 25, 12, 0))
|
||||
|
||||
def testRandomFormat26(self):
|
||||
with pytest.warns(UnknownTimezoneWarning):
|
||||
res = parse("5:50 A.M. on June 13, 1990")
|
||||
|
||||
self.assertEqual(res, datetime(1990, 6, 13, 5, 50))
|
||||
|
||||
def testUnspecifiedDayFallback(self):
|
||||
# Test that for an unspecified day, the fallback behavior is correct.
|
||||
self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)),
|
||||
datetime(2009, 4, 30))
|
||||
|
||||
def testUnspecifiedDayFallbackFebNoLeapYear(self):
|
||||
self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)),
|
||||
datetime(2007, 2, 28))
|
||||
|
||||
def testUnspecifiedDayFallbackFebLeapYear(self):
|
||||
self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)),
|
||||
datetime(2008, 2, 29))
|
||||
|
||||
def testErrorType01(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse('shouldfail')
|
||||
|
||||
def testCorrectErrorOnFuzzyWithTokens(self):
|
||||
assertRaisesRegex(self, ParserError, 'Unknown string format',
|
||||
parse, '04/04/32/423', fuzzy_with_tokens=True)
|
||||
assertRaisesRegex(self, ParserError, 'Unknown string format',
|
||||
parse, '04/04/04 +32423', fuzzy_with_tokens=True)
|
||||
assertRaisesRegex(self, ParserError, 'Unknown string format',
|
||||
parse, '04/04/0d4', fuzzy_with_tokens=True)
|
||||
|
||||
def testIncreasingCTime(self):
|
||||
# This test will check 200 different years, every month, every day,
|
||||
# every hour, every minute, every second, and every weekday, using
|
||||
# a delta of more or less 1 year, 1 month, 1 day, 1 minute and
|
||||
# 1 second.
|
||||
delta = timedelta(days=365+31+1, seconds=1+60+60*60)
|
||||
dt = datetime(1900, 1, 1, 0, 0, 0, 0)
|
||||
for i in range(200):
|
||||
assert parse(dt.ctime()) == dt
|
||||
dt += delta
|
||||
|
||||
def testIncreasingISOFormat(self):
|
||||
delta = timedelta(days=365+31+1, seconds=1+60+60*60)
|
||||
dt = datetime(1900, 1, 1, 0, 0, 0, 0)
|
||||
for i in range(200):
|
||||
assert parse(dt.isoformat()) == dt
|
||||
dt += delta
|
||||
|
||||
def testMicrosecondsPrecisionError(self):
|
||||
# Skip found out that sad precision problem. :-(
|
||||
dt1 = parse("00:11:25.01")
|
||||
dt2 = parse("00:12:10.01")
|
||||
assert dt1.microsecond == 10000
|
||||
assert dt2.microsecond == 10000
|
||||
|
||||
def testMicrosecondPrecisionErrorReturns(self):
|
||||
# One more precision issue, discovered by Eric Brown. This should
|
||||
# be the last one, as we're no longer using floating points.
|
||||
for ms in [100001, 100000, 99999, 99998,
|
||||
10001, 10000, 9999, 9998,
|
||||
1001, 1000, 999, 998,
|
||||
101, 100, 99, 98]:
|
||||
dt = datetime(2008, 2, 27, 21, 26, 1, ms)
|
||||
assert parse(dt.isoformat()) == dt
|
||||
|
||||
def testCustomParserInfo(self):
|
||||
# Custom parser info wasn't working, as Michael Elsdörfer discovered.
|
||||
from dateutil.parser import parserinfo, parser
|
||||
|
||||
class myparserinfo(parserinfo):
|
||||
MONTHS = parserinfo.MONTHS[:]
|
||||
MONTHS[0] = ("Foo", "Foo")
|
||||
myparser = parser(myparserinfo())
|
||||
dt = myparser.parse("01/Foo/2007")
|
||||
assert dt == datetime(2007, 1, 1)
|
||||
|
||||
def testCustomParserShortDaynames(self):
|
||||
# Horacio Hoyos discovered that day names shorter than 3 characters,
|
||||
# for example two letter German day name abbreviations, don't work:
|
||||
# https://github.com/dateutil/dateutil/issues/343
|
||||
from dateutil.parser import parserinfo, parser
|
||||
|
||||
class GermanParserInfo(parserinfo):
|
||||
WEEKDAYS = [("Mo", "Montag"),
|
||||
("Di", "Dienstag"),
|
||||
("Mi", "Mittwoch"),
|
||||
("Do", "Donnerstag"),
|
||||
("Fr", "Freitag"),
|
||||
("Sa", "Samstag"),
|
||||
("So", "Sonntag")]
|
||||
|
||||
myparser = parser(GermanParserInfo())
|
||||
dt = myparser.parse("Sa 21. Jan 2017")
|
||||
self.assertEqual(dt, datetime(2017, 1, 21))
|
||||
|
||||
def testNoYearFirstNoDayFirst(self):
|
||||
dtstr = '090107'
|
||||
|
||||
# Should be MMDDYY
|
||||
self.assertEqual(parse(dtstr),
|
||||
datetime(2007, 9, 1))
|
||||
|
||||
self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False),
|
||||
datetime(2007, 9, 1))
|
||||
|
||||
def testYearFirst(self):
|
||||
dtstr = '090107'
|
||||
|
||||
# Should be MMDDYY
|
||||
self.assertEqual(parse(dtstr, yearfirst=True),
|
||||
datetime(2009, 1, 7))
|
||||
|
||||
self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False),
|
||||
datetime(2009, 1, 7))
|
||||
|
||||
def testDayFirst(self):
|
||||
dtstr = '090107'
|
||||
|
||||
# Should be DDMMYY
|
||||
self.assertEqual(parse(dtstr, dayfirst=True),
|
||||
datetime(2007, 1, 9))
|
||||
|
||||
self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True),
|
||||
datetime(2007, 1, 9))
|
||||
|
||||
def testDayFirstYearFirst(self):
|
||||
dtstr = '090107'
|
||||
# Should be YYDDMM
|
||||
self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True),
|
||||
datetime(2009, 7, 1))
|
||||
|
||||
def testUnambiguousYearFirst(self):
|
||||
dtstr = '2015 09 25'
|
||||
self.assertEqual(parse(dtstr, yearfirst=True),
|
||||
datetime(2015, 9, 25))
|
||||
|
||||
def testUnambiguousDayFirst(self):
|
||||
dtstr = '2015 09 25'
|
||||
self.assertEqual(parse(dtstr, dayfirst=True),
|
||||
datetime(2015, 9, 25))
|
||||
|
||||
def testUnambiguousDayFirstYearFirst(self):
|
||||
dtstr = '2015 09 25'
|
||||
self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True),
|
||||
datetime(2015, 9, 25))
|
||||
|
||||
def test_mstridx(self):
|
||||
# See GH408
|
||||
dtstr = '2015-15-May'
|
||||
self.assertEqual(parse(dtstr),
|
||||
datetime(2015, 5, 15))
|
||||
|
||||
def test_idx_check(self):
|
||||
dtstr = '2017-07-17 06:15:'
|
||||
# Pre-PR, the trailing colon will cause an IndexError at 824-825
|
||||
# when checking `i < len_l` and then accessing `l[i+1]`
|
||||
res = parse(dtstr, fuzzy=True)
|
||||
assert res == datetime(2017, 7, 17, 6, 15)
|
||||
|
||||
def test_hmBY(self):
|
||||
# See GH#483
|
||||
dtstr = '02:17NOV2017'
|
||||
res = parse(dtstr, default=self.default)
|
||||
assert res == datetime(2017, 11, self.default.day, 2, 17)
|
||||
|
||||
def test_validate_hour(self):
|
||||
# See GH353
|
||||
invalid = "201A-01-01T23:58:39.239769+03:00"
|
||||
with pytest.raises(ParserError):
|
||||
parse(invalid)
|
||||
|
||||
def test_era_trailing_year(self):
|
||||
dstr = 'AD2001'
|
||||
res = parse(dstr)
|
||||
assert res.year == 2001, res
|
||||
|
||||
def test_includes_timestr(self):
|
||||
timestr = "2020-13-97T44:61:83"
|
||||
|
||||
try:
|
||||
parse(timestr)
|
||||
except ParserError as e:
|
||||
assert e.args[1] == timestr
|
||||
else:
|
||||
pytest.fail("Failed to raise ParserError")
|
||||
|
||||
|
||||
class TestOutOfBounds(object):
|
||||
|
||||
def test_no_year_zero(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse("0000 Jun 20")
|
||||
|
||||
def test_out_of_bound_day(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse("Feb 30, 2007")
|
||||
|
||||
def test_illegal_month_error(self):
|
||||
with pytest.raises(ParserError):
|
||||
parse("0-100")
|
||||
|
||||
def test_day_sanity(self, fuzzy):
|
||||
dstr = "2014-15-25"
|
||||
with pytest.raises(ParserError):
|
||||
parse(dstr, fuzzy=fuzzy)
|
||||
|
||||
def test_minute_sanity(self, fuzzy):
|
||||
dstr = "2014-02-28 22:64"
|
||||
with pytest.raises(ParserError):
|
||||
parse(dstr, fuzzy=fuzzy)
|
||||
|
||||
def test_hour_sanity(self, fuzzy):
|
||||
dstr = "2014-02-28 25:16 PM"
|
||||
with pytest.raises(ParserError):
|
||||
parse(dstr, fuzzy=fuzzy)
|
||||
|
||||
def test_second_sanity(self, fuzzy):
|
||||
dstr = "2014-02-28 22:14:64"
|
||||
with pytest.raises(ParserError):
|
||||
parse(dstr, fuzzy=fuzzy)
|
||||
|
||||
|
||||
class TestParseUnimplementedCases(object):
|
||||
@pytest.mark.xfail
|
||||
def test_somewhat_ambiguous_string(self):
|
||||
# Ref: github issue #487
|
||||
# The parser is choosing the wrong part for hour
|
||||
# causing datetime to raise an exception.
|
||||
dtstr = '1237 PM BRST Mon Oct 30 2017'
|
||||
res = parse(dtstr, tzinfo=self.tzinfos)
|
||||
assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_YmdH_M_S(self):
|
||||
# found in nasdaq's ftp data
|
||||
dstr = '1991041310:19:24'
|
||||
expected = datetime(1991, 4, 13, 10, 19, 24)
|
||||
res = parse(dstr)
|
||||
assert res == expected, (res, expected)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_first_century(self):
|
||||
dstr = '0031 Nov 03'
|
||||
expected = datetime(31, 11, 3)
|
||||
res = parse(dstr)
|
||||
assert res == expected, res
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_era_trailing_year_with_dots(self):
|
||||
dstr = 'A.D.2001'
|
||||
res = parse(dstr)
|
||||
assert res.year == 2001, res
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_ad_nospace(self):
|
||||
expected = datetime(6, 5, 19)
|
||||
for dstr in [' 6AD May 19', ' 06AD May 19',
|
||||
' 006AD May 19', ' 0006AD May 19']:
|
||||
res = parse(dstr)
|
||||
assert res == expected, (dstr, res)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_four_letter_day(self):
|
||||
dstr = 'Frid Dec 30, 2016'
|
||||
expected = datetime(2016, 12, 30)
|
||||
res = parse(dstr)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_non_date_number(self):
|
||||
dstr = '1,700'
|
||||
with pytest.raises(ParserError):
|
||||
parse(dstr)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_on_era(self):
|
||||
# This could be classified as an "eras" test, but the relevant part
|
||||
# about this is the ` on `
|
||||
dstr = '2:15 PM on January 2nd 1973 A.D.'
|
||||
expected = datetime(1973, 1, 2, 14, 15)
|
||||
res = parse(dstr)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_extraneous_year(self):
|
||||
# This was found in the wild at insidertrading.org
|
||||
dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
|
||||
res = parse(dstr, fuzzy_with_tokens=True)
|
||||
expected = datetime(2012, 11, 7)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_extraneous_year_tokens(self):
|
||||
# This was found in the wild at insidertrading.org
|
||||
# Unlike in the case above, identifying the first "2012" as the year
|
||||
# would not be a problem, but inferring that the latter 2012 is hhmm
|
||||
# is a problem.
|
||||
dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012"
|
||||
expected = datetime(2012, 11, 7)
|
||||
(res, tokens) = parse(dstr, fuzzy_with_tokens=True)
|
||||
assert res == expected
|
||||
assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_extraneous_year2(self):
|
||||
# This was found in the wild at insidertrading.org
|
||||
dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust "
|
||||
"u/d/t November 2, 1998 f/b/o Jennifer L Berylson")
|
||||
res = parse(dstr, fuzzy_with_tokens=True)
|
||||
expected = datetime(1998, 11, 2)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_extraneous_year3(self):
|
||||
# This was found in the wild at insidertrading.org
|
||||
dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994"
|
||||
res = parse(dstr, fuzzy_with_tokens=True)
|
||||
expected = datetime(1994, 12, 1)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_unambiguous_YYYYMM(self):
|
||||
# 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed
|
||||
# as instance of YYMMDD and parser could fallback to YYYYMM format.
|
||||
dstr = "201712"
|
||||
res = parse(dstr)
|
||||
expected = datetime(2017, 12, 1)
|
||||
assert res == expected
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_extraneous_numerical_content(self):
|
||||
# ref: https://github.com/dateutil/dateutil/issues/1029
|
||||
# parser interprets price and percentage as parts of the date
|
||||
dstr = "£14.99 (25% off, until April 20)"
|
||||
res = parse(dstr, fuzzy=True, default=datetime(2000, 1, 1))
|
||||
expected = datetime(2000, 4, 20)
|
||||
assert res == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_WIN, reason="Windows does not use TZ var")
|
||||
class TestTZVar(object):
|
||||
def test_parse_unambiguous_nonexistent_local(self):
|
||||
# When dates are specified "EST" even when they should be "EDT" in the
|
||||
# local time zone, we should still assign the local time zone
|
||||
with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
|
||||
dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal())
|
||||
dt = parse('2011-08-01T12:30 EST')
|
||||
|
||||
assert dt.tzname() == 'EDT'
|
||||
assert dt == dt_exp
|
||||
|
||||
def test_tzlocal_in_gmt(self):
|
||||
# GH #318
|
||||
with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'):
|
||||
# This is an imaginary datetime in tz.tzlocal() but should still
|
||||
# parse using the GMT-as-alias-for-UTC rule
|
||||
dt = parse('2004-05-01T12:00 GMT')
|
||||
dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.UTC)
|
||||
|
||||
assert dt == dt_exp
|
||||
|
||||
def test_tzlocal_parse_fold(self):
|
||||
# One manifestion of GH #318
|
||||
with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
|
||||
dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal())
|
||||
dt_exp = tz.enfold(dt_exp, fold=1)
|
||||
dt = parse('2011-11-06T01:30 EST')
|
||||
|
||||
# Because this is ambiguous, until `tz.tzlocal() is tz.tzlocal()`
|
||||
# we'll just check the attributes we care about rather than
|
||||
# dt == dt_exp
|
||||
assert dt.tzname() == dt_exp.tzname()
|
||||
assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None)
|
||||
assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
|
||||
assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC)
|
||||
|
||||
|
||||
def test_parse_tzinfos_fold():
|
||||
NYC = tz.gettz('America/New_York')
|
||||
tzinfos = {'EST': NYC, 'EDT': NYC}
|
||||
|
||||
dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1)
|
||||
dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos)
|
||||
|
||||
assert dt == dt_exp
|
||||
assert dt.tzinfo is dt_exp.tzinfo
|
||||
assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
|
||||
assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dtstr,dt', [
|
||||
('5.6h', datetime(2003, 9, 25, 5, 36)),
|
||||
('5.6m', datetime(2003, 9, 25, 0, 5, 36)),
|
||||
# '5.6s' never had a rounding problem, test added for completeness
|
||||
('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000))
|
||||
])
|
||||
def test_rounding_floatlike_strings(dtstr, dt):
|
||||
assert parse(dtstr, default=datetime(2003, 9, 25)) == dt
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', ['1: test', 'Nan'])
|
||||
def test_decimal_error(value):
|
||||
# GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception
|
||||
# when constructed with an invalid value
|
||||
with pytest.raises(ParserError):
|
||||
parse(value)
|
||||
|
||||
def test_parsererror_repr():
|
||||
# GH 991 — the __repr__ was not properly indented and so was never defined.
|
||||
# This tests the current behavior of the ParserError __repr__, but the
|
||||
# precise format is not guaranteed to be stable and may change even in
|
||||
# minor versions. This test exists to avoid regressions.
|
||||
s = repr(ParserError("Problem with string: %s", "2019-01-01"))
|
||||
|
||||
assert s == "ParserError('Problem with string: %s', '2019-01-01')"
|
706
lib/dateutil/test/test_relativedelta.py
Normal file
706
lib/dateutil/test/test_relativedelta.py
Normal file
|
@ -0,0 +1,706 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from ._common import NotAValue
|
||||
|
||||
import calendar
|
||||
from datetime import datetime, date, timedelta
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU
|
||||
|
||||
|
||||
class RelativeDeltaTest(unittest.TestCase):
|
||||
now = datetime(2003, 9, 17, 20, 54, 47, 282310)
|
||||
today = date(2003, 9, 17)
|
||||
|
||||
def testInheritance(self):
|
||||
# Ensure that relativedelta is inheritance-friendly.
|
||||
class rdChildClass(relativedelta):
|
||||
pass
|
||||
|
||||
ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1,
|
||||
hours=1, minutes=1, seconds=1, microseconds=1)
|
||||
|
||||
rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1,
|
||||
hours=1, minutes=1, seconds=1, microseconds=1)
|
||||
|
||||
self.assertEqual(type(ccRD + rd), type(ccRD),
|
||||
msg='Addition does not inherit type.')
|
||||
|
||||
self.assertEqual(type(ccRD - rd), type(ccRD),
|
||||
msg='Subtraction does not inherit type.')
|
||||
|
||||
self.assertEqual(type(-ccRD), type(ccRD),
|
||||
msg='Negation does not inherit type.')
|
||||
|
||||
self.assertEqual(type(ccRD * 5.0), type(ccRD),
|
||||
msg='Multiplication does not inherit type.')
|
||||
|
||||
self.assertEqual(type(ccRD / 5.0), type(ccRD),
|
||||
msg='Division does not inherit type.')
|
||||
|
||||
def testMonthEndMonthBeginning(self):
|
||||
self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59),
|
||||
datetime(2003, 3, 1, 0, 0, 0)),
|
||||
relativedelta(months=-1, seconds=-1))
|
||||
|
||||
self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
|
||||
datetime(2003, 1, 31, 23, 59, 59)),
|
||||
relativedelta(months=1, seconds=1))
|
||||
|
||||
def testMonthEndMonthBeginningLeapYear(self):
|
||||
self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59),
|
||||
datetime(2012, 3, 1, 0, 0, 0)),
|
||||
relativedelta(months=-1, seconds=-1))
|
||||
|
||||
self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0),
|
||||
datetime(2003, 1, 31, 23, 59, 59)),
|
||||
relativedelta(months=1, seconds=1))
|
||||
|
||||
def testNextMonth(self):
|
||||
self.assertEqual(self.now+relativedelta(months=+1),
|
||||
datetime(2003, 10, 17, 20, 54, 47, 282310))
|
||||
|
||||
def testNextMonthPlusOneWeek(self):
|
||||
self.assertEqual(self.now+relativedelta(months=+1, weeks=+1),
|
||||
datetime(2003, 10, 24, 20, 54, 47, 282310))
|
||||
|
||||
def testNextMonthPlusOneWeek10am(self):
|
||||
self.assertEqual(self.today +
|
||||
relativedelta(months=+1, weeks=+1, hour=10),
|
||||
datetime(2003, 10, 24, 10, 0))
|
||||
|
||||
def testNextMonthPlusOneWeek10amDiff(self):
|
||||
self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0),
|
||||
self.today),
|
||||
relativedelta(months=+1, days=+7, hours=+10))
|
||||
|
||||
def testOneMonthBeforeOneYear(self):
|
||||
self.assertEqual(self.now+relativedelta(years=+1, months=-1),
|
||||
datetime(2004, 8, 17, 20, 54, 47, 282310))
|
||||
|
||||
def testMonthsOfDiffNumOfDays(self):
|
||||
self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1),
|
||||
date(2003, 2, 27))
|
||||
self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1),
|
||||
date(2003, 2, 28))
|
||||
self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2),
|
||||
date(2003, 3, 31))
|
||||
|
||||
def testMonthsOfDiffNumOfDaysWithYears(self):
|
||||
self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1),
|
||||
date(2001, 2, 28))
|
||||
self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1),
|
||||
date(2001, 2, 28))
|
||||
|
||||
self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1),
|
||||
date(2000, 2, 28))
|
||||
self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
|
||||
date(2000, 3, 1))
|
||||
self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1),
|
||||
date(2000, 3, 1))
|
||||
|
||||
self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1),
|
||||
date(2000, 2, 28))
|
||||
self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1),
|
||||
date(2000, 3, 1))
|
||||
|
||||
def testNextFriday(self):
|
||||
self.assertEqual(self.today+relativedelta(weekday=FR),
|
||||
date(2003, 9, 19))
|
||||
|
||||
def testNextFridayInt(self):
|
||||
self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY),
|
||||
date(2003, 9, 19))
|
||||
|
||||
def testLastFridayInThisMonth(self):
|
||||
self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)),
|
||||
date(2003, 9, 26))
|
||||
|
||||
def testLastDayOfFebruary(self):
|
||||
self.assertEqual(date(2021, 2, 1) + relativedelta(day=31),
|
||||
date(2021, 2, 28))
|
||||
|
||||
def testLastDayOfFebruaryLeapYear(self):
|
||||
self.assertEqual(date(2020, 2, 1) + relativedelta(day=31),
|
||||
date(2020, 2, 29))
|
||||
|
||||
def testNextWednesdayIsToday(self):
|
||||
self.assertEqual(self.today+relativedelta(weekday=WE),
|
||||
date(2003, 9, 17))
|
||||
|
||||
def testNextWednesdayNotToday(self):
|
||||
self.assertEqual(self.today+relativedelta(days=+1, weekday=WE),
|
||||
date(2003, 9, 24))
|
||||
|
||||
def testAddMoreThan12Months(self):
|
||||
self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13),
|
||||
date(2005, 1, 1))
|
||||
|
||||
def testAddNegativeMonths(self):
|
||||
self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2),
|
||||
date(2002, 11, 1))
|
||||
|
||||
def test15thISOYearWeek(self):
|
||||
self.assertEqual(date(2003, 1, 1) +
|
||||
relativedelta(day=4, weeks=+14, weekday=MO(-1)),
|
||||
date(2003, 4, 7))
|
||||
|
||||
def testMillenniumAge(self):
|
||||
self.assertEqual(relativedelta(self.now, date(2001, 1, 1)),
|
||||
relativedelta(years=+2, months=+8, days=+16,
|
||||
hours=+20, minutes=+54, seconds=+47,
|
||||
microseconds=+282310))
|
||||
|
||||
def testJohnAge(self):
|
||||
self.assertEqual(relativedelta(self.now,
|
||||
datetime(1978, 4, 5, 12, 0)),
|
||||
relativedelta(years=+25, months=+5, days=+12,
|
||||
hours=+8, minutes=+54, seconds=+47,
|
||||
microseconds=+282310))
|
||||
|
||||
def testJohnAgeWithDate(self):
|
||||
self.assertEqual(relativedelta(self.today,
|
||||
datetime(1978, 4, 5, 12, 0)),
|
||||
relativedelta(years=+25, months=+5, days=+11,
|
||||
hours=+12))
|
||||
|
||||
def testYearDay(self):
|
||||
self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260),
|
||||
date(2003, 9, 17))
|
||||
self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260),
|
||||
date(2002, 9, 17))
|
||||
self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260),
|
||||
date(2000, 9, 16))
|
||||
self.assertEqual(self.today+relativedelta(yearday=261),
|
||||
date(2003, 9, 18))
|
||||
|
||||
def testYearDayBug(self):
|
||||
# Tests a problem reported by Adam Ryan.
|
||||
self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15),
|
||||
date(2010, 1, 15))
|
||||
|
||||
def testNonLeapYearDay(self):
|
||||
self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260),
|
||||
date(2003, 9, 17))
|
||||
self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260),
|
||||
date(2002, 9, 17))
|
||||
self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260),
|
||||
date(2000, 9, 17))
|
||||
self.assertEqual(self.today+relativedelta(yearday=261),
|
||||
date(2003, 9, 18))
|
||||
|
||||
def testAddition(self):
|
||||
self.assertEqual(relativedelta(days=10) +
|
||||
relativedelta(years=1, months=2, days=3, hours=4,
|
||||
minutes=5, microseconds=6),
|
||||
relativedelta(years=1, months=2, days=13, hours=4,
|
||||
minutes=5, microseconds=6))
|
||||
|
||||
def testAbsoluteAddition(self):
|
||||
self.assertEqual(relativedelta() + relativedelta(day=0, hour=0),
|
||||
relativedelta(day=0, hour=0))
|
||||
self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(),
|
||||
relativedelta(day=0, hour=0))
|
||||
|
||||
def testAdditionToDatetime(self):
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1),
|
||||
datetime(2000, 1, 2))
|
||||
|
||||
def testRightAdditionToDatetime(self):
|
||||
self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1),
|
||||
datetime(2000, 1, 2))
|
||||
|
||||
def testAdditionInvalidType(self):
|
||||
with self.assertRaises(TypeError):
|
||||
relativedelta(days=3) + 9
|
||||
|
||||
def testAdditionUnsupportedType(self):
|
||||
# For unsupported types that define their own comparators, etc.
|
||||
self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
|
||||
|
||||
def testAdditionFloatValue(self):
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)),
|
||||
datetime(2000, 1, 2))
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)),
|
||||
datetime(2000, 2, 1))
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)),
|
||||
datetime(2001, 1, 1))
|
||||
|
||||
def testAdditionFloatFractionals(self):
|
||||
self.assertEqual(datetime(2000, 1, 1, 0) +
|
||||
relativedelta(days=float(0.5)),
|
||||
datetime(2000, 1, 1, 12))
|
||||
self.assertEqual(datetime(2000, 1, 1, 0, 0) +
|
||||
relativedelta(hours=float(0.5)),
|
||||
datetime(2000, 1, 1, 0, 30))
|
||||
self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) +
|
||||
relativedelta(minutes=float(0.5)),
|
||||
datetime(2000, 1, 1, 0, 0, 30))
|
||||
self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
|
||||
relativedelta(seconds=float(0.5)),
|
||||
datetime(2000, 1, 1, 0, 0, 0, 500000))
|
||||
self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) +
|
||||
relativedelta(microseconds=float(500000.25)),
|
||||
datetime(2000, 1, 1, 0, 0, 0, 500000))
|
||||
|
||||
def testSubtraction(self):
|
||||
self.assertEqual(relativedelta(days=10) -
|
||||
relativedelta(years=1, months=2, days=3, hours=4,
|
||||
minutes=5, microseconds=6),
|
||||
relativedelta(years=-1, months=-2, days=7, hours=-4,
|
||||
minutes=-5, microseconds=-6))
|
||||
|
||||
def testRightSubtractionFromDatetime(self):
|
||||
self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1),
|
||||
datetime(2000, 1, 1))
|
||||
|
||||
def testSubractionWithDatetime(self):
|
||||
self.assertRaises(TypeError, lambda x, y: x - y,
|
||||
(relativedelta(days=1), datetime(2000, 1, 1)))
|
||||
|
||||
def testSubtractionInvalidType(self):
|
||||
with self.assertRaises(TypeError):
|
||||
relativedelta(hours=12) - 14
|
||||
|
||||
def testSubtractionUnsupportedType(self):
|
||||
self.assertIs(relativedelta(days=1) + NotAValue, NotAValue)
|
||||
|
||||
def testMultiplication(self):
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28,
|
||||
datetime(2000, 1, 29))
|
||||
self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1),
|
||||
datetime(2000, 1, 29))
|
||||
|
||||
def testMultiplicationUnsupportedType(self):
|
||||
self.assertIs(relativedelta(days=1) * NotAValue, NotAValue)
|
||||
|
||||
def testDivision(self):
|
||||
self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28,
|
||||
datetime(2000, 1, 2))
|
||||
|
||||
def testDivisionUnsupportedType(self):
|
||||
self.assertIs(relativedelta(days=1) / NotAValue, NotAValue)
|
||||
|
||||
def testBoolean(self):
|
||||
self.assertFalse(relativedelta(days=0))
|
||||
self.assertTrue(relativedelta(days=1))
|
||||
|
||||
def testAbsoluteValueNegative(self):
|
||||
rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3,
|
||||
minutes=-5, seconds=-2, microseconds=-12)
|
||||
rd_expected = relativedelta(years=1, months=5, days=2, hours=3,
|
||||
minutes=5, seconds=2, microseconds=12)
|
||||
self.assertEqual(abs(rd_base), rd_expected)
|
||||
|
||||
def testAbsoluteValuePositive(self):
|
||||
rd_base = relativedelta(years=1, months=5, days=2, hours=3,
|
||||
minutes=5, seconds=2, microseconds=12)
|
||||
rd_expected = rd_base
|
||||
|
||||
self.assertEqual(abs(rd_base), rd_expected)
|
||||
|
||||
def testComparison(self):
|
||||
d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
|
||||
minutes=1, seconds=1, microseconds=1)
|
||||
d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
|
||||
minutes=1, seconds=1, microseconds=1)
|
||||
d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1,
|
||||
minutes=1, seconds=1, microseconds=2)
|
||||
|
||||
self.assertEqual(d1, d2)
|
||||
self.assertNotEqual(d1, d3)
|
||||
|
||||
def testInequalityTypeMismatch(self):
|
||||
# Different type
|
||||
self.assertFalse(relativedelta(year=1) == 19)
|
||||
|
||||
def testInequalityUnsupportedType(self):
|
||||
self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue)
|
||||
|
||||
def testInequalityWeekdays(self):
|
||||
# Different weekdays
|
||||
no_wday = relativedelta(year=1997, month=4)
|
||||
wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1))
|
||||
wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2))
|
||||
wday_tu = relativedelta(year=1997, month=4, weekday=TU)
|
||||
|
||||
self.assertTrue(wday_mo_1 == wday_mo_1)
|
||||
|
||||
self.assertFalse(no_wday == wday_mo_1)
|
||||
self.assertFalse(wday_mo_1 == no_wday)
|
||||
|
||||
self.assertFalse(wday_mo_1 == wday_mo_2)
|
||||
self.assertFalse(wday_mo_2 == wday_mo_1)
|
||||
|
||||
self.assertFalse(wday_mo_1 == wday_tu)
|
||||
self.assertFalse(wday_tu == wday_mo_1)
|
||||
|
||||
def testMonthOverflow(self):
|
||||
self.assertEqual(relativedelta(months=273),
|
||||
relativedelta(years=22, months=9))
|
||||
|
||||
def testWeeks(self):
|
||||
# Test that the weeks property is working properly.
|
||||
rd = relativedelta(years=4, months=2, weeks=8, days=6)
|
||||
self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6))
|
||||
|
||||
rd.weeks = 3
|
||||
self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6))
|
||||
|
||||
def testRelativeDeltaRepr(self):
|
||||
self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)),
|
||||
'relativedelta(years=+1, months=-1, days=+15)')
|
||||
|
||||
self.assertEqual(repr(relativedelta(months=14, seconds=-25)),
|
||||
'relativedelta(years=+1, months=+2, seconds=-25)')
|
||||
|
||||
self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))),
|
||||
'relativedelta(month=3, weekday=SU(+3), hour=3)')
|
||||
|
||||
def testRelativeDeltaFractionalYear(self):
|
||||
with self.assertRaises(ValueError):
|
||||
relativedelta(years=1.5)
|
||||
|
||||
def testRelativeDeltaFractionalMonth(self):
|
||||
with self.assertRaises(ValueError):
|
||||
relativedelta(months=1.5)
|
||||
|
||||
def testRelativeDeltaInvalidDatetimeObject(self):
|
||||
with self.assertRaises(TypeError):
|
||||
relativedelta(dt1='2018-01-01', dt2='2018-01-02')
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02')
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2))
|
||||
|
||||
def testRelativeDeltaFractionalAbsolutes(self):
|
||||
# Fractional absolute values will soon be unsupported,
|
||||
# check for the deprecation warning.
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(year=2.86)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(month=1.29)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(day=0.44)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(hour=23.98)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(minute=45.21)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(second=13.2)
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
relativedelta(microsecond=157221.93)
|
||||
|
||||
def testRelativeDeltaFractionalRepr(self):
|
||||
rd = relativedelta(years=3, months=-2, days=1.25)
|
||||
|
||||
self.assertEqual(repr(rd),
|
||||
'relativedelta(years=+3, months=-2, days=+1.25)')
|
||||
|
||||
rd = relativedelta(hours=0.5, seconds=9.22)
|
||||
self.assertEqual(repr(rd),
|
||||
'relativedelta(hours=+0.5, seconds=+9.22)')
|
||||
|
||||
def testRelativeDeltaFractionalWeeks(self):
|
||||
# Equivalent to days=8, hours=18
|
||||
rd = relativedelta(weeks=1.25)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd,
|
||||
datetime(2009, 9, 11, 18))
|
||||
|
||||
def testRelativeDeltaFractionalDays(self):
|
||||
rd1 = relativedelta(days=1.48)
|
||||
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd1,
|
||||
datetime(2009, 9, 4, 11, 31, 12))
|
||||
|
||||
rd2 = relativedelta(days=1.5)
|
||||
self.assertEqual(d1 + rd2,
|
||||
datetime(2009, 9, 4, 12, 0, 0))
|
||||
|
||||
def testRelativeDeltaFractionalHours(self):
|
||||
rd = relativedelta(days=1, hours=12.5)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd,
|
||||
datetime(2009, 9, 4, 12, 30, 0))
|
||||
|
||||
def testRelativeDeltaFractionalMinutes(self):
|
||||
rd = relativedelta(hours=1, minutes=30.5)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd,
|
||||
datetime(2009, 9, 3, 1, 30, 30))
|
||||
|
||||
def testRelativeDeltaFractionalSeconds(self):
|
||||
rd = relativedelta(hours=5, minutes=30, seconds=30.5)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd,
|
||||
datetime(2009, 9, 3, 5, 30, 30, 500000))
|
||||
|
||||
def testRelativeDeltaFractionalPositiveOverflow(self):
|
||||
# Equivalent to (days=1, hours=14)
|
||||
rd1 = relativedelta(days=1.5, hours=2)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd1,
|
||||
datetime(2009, 9, 4, 14, 0, 0))
|
||||
|
||||
# Equivalent to (days=1, hours=14, minutes=45)
|
||||
rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
|
||||
d1 = datetime(2009, 9, 3, 0, 0)
|
||||
self.assertEqual(d1 + rd2,
|
||||
datetime(2009, 9, 4, 14, 45))
|
||||
|
||||
# Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1)
|
||||
rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31)
|
||||
self.assertEqual(d1 + rd3,
|
||||
datetime(2009, 9, 5, 2, 0, 1))
|
||||
|
||||
def testRelativeDeltaFractionalNegativeDays(self):
|
||||
# Equivalent to (days=-1, hours=-1)
|
||||
rd1 = relativedelta(days=-1.5, hours=11)
|
||||
d1 = datetime(2009, 9, 3, 12, 0)
|
||||
self.assertEqual(d1 + rd1,
|
||||
datetime(2009, 9, 2, 11, 0, 0))
|
||||
|
||||
# Equivalent to (days=-1, hours=-9)
|
||||
rd2 = relativedelta(days=-1.25, hours=-3)
|
||||
self.assertEqual(d1 + rd2,
|
||||
datetime(2009, 9, 2, 3))
|
||||
|
||||
def testRelativeDeltaNormalizeFractionalDays(self):
|
||||
# Equivalent to (days=2, hours=18)
|
||||
rd1 = relativedelta(days=2.75)
|
||||
|
||||
self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18))
|
||||
|
||||
# Equivalent to (days=1, hours=11, minutes=31, seconds=12)
|
||||
rd2 = relativedelta(days=1.48)
|
||||
|
||||
self.assertEqual(rd2.normalized(),
|
||||
relativedelta(days=1, hours=11, minutes=31, seconds=12))
|
||||
|
||||
def testRelativeDeltaNormalizeFractionalDays2(self):
|
||||
# Equivalent to (hours=1, minutes=30)
|
||||
rd1 = relativedelta(hours=1.5)
|
||||
|
||||
self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30))
|
||||
|
||||
# Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100)
|
||||
rd2 = relativedelta(hours=3.28472225)
|
||||
|
||||
self.assertEqual(rd2.normalized(),
|
||||
relativedelta(hours=3, minutes=17, seconds=5, microseconds=100))
|
||||
|
||||
def testRelativeDeltaNormalizeFractionalMinutes(self):
|
||||
# Equivalent to (minutes=15, seconds=36)
|
||||
rd1 = relativedelta(minutes=15.6)
|
||||
|
||||
self.assertEqual(rd1.normalized(),
|
||||
relativedelta(minutes=15, seconds=36))
|
||||
|
||||
# Equivalent to (minutes=25, seconds=20, microseconds=25000)
|
||||
rd2 = relativedelta(minutes=25.33375)
|
||||
|
||||
self.assertEqual(rd2.normalized(),
|
||||
relativedelta(minutes=25, seconds=20, microseconds=25000))
|
||||
|
||||
def testRelativeDeltaNormalizeFractionalSeconds(self):
|
||||
# Equivalent to (seconds=45, microseconds=25000)
|
||||
rd1 = relativedelta(seconds=45.025)
|
||||
self.assertEqual(rd1.normalized(),
|
||||
relativedelta(seconds=45, microseconds=25000))
|
||||
|
||||
def testRelativeDeltaFractionalPositiveOverflow2(self):
|
||||
# Equivalent to (days=1, hours=14)
|
||||
rd1 = relativedelta(days=1.5, hours=2)
|
||||
self.assertEqual(rd1.normalized(),
|
||||
relativedelta(days=1, hours=14))
|
||||
|
||||
# Equivalent to (days=1, hours=14, minutes=45)
|
||||
rd2 = relativedelta(days=1.5, hours=2.5, minutes=15)
|
||||
self.assertEqual(rd2.normalized(),
|
||||
relativedelta(days=1, hours=14, minutes=45))
|
||||
|
||||
# Carry back up - equivalent to:
|
||||
# (days=2, hours=2, minutes=0, seconds=2, microseconds=3)
|
||||
rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045,
|
||||
seconds=31.473, microseconds=500003)
|
||||
self.assertEqual(rd3.normalized(),
|
||||
relativedelta(days=2, hours=2, minutes=0,
|
||||
seconds=2, microseconds=3))
|
||||
|
||||
def testRelativeDeltaFractionalNegativeOverflow(self):
|
||||
# Equivalent to (days=-1)
|
||||
rd1 = relativedelta(days=-0.5, hours=-12)
|
||||
self.assertEqual(rd1.normalized(),
|
||||
relativedelta(days=-1))
|
||||
|
||||
# Equivalent to (days=-1)
|
||||
rd2 = relativedelta(days=-1.5, hours=12)
|
||||
self.assertEqual(rd2.normalized(),
|
||||
relativedelta(days=-1))
|
||||
|
||||
# Equivalent to (days=-1, hours=-14, minutes=-45)
|
||||
rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15)
|
||||
self.assertEqual(rd3.normalized(),
|
||||
relativedelta(days=-1, hours=-14, minutes=-45))
|
||||
|
||||
# Equivalent to (days=-1, hours=-14, minutes=+15)
|
||||
rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45)
|
||||
self.assertEqual(rd4.normalized(),
|
||||
relativedelta(days=-1, hours=-14, minutes=+15))
|
||||
|
||||
# Carry back up - equivalent to:
|
||||
# (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3)
|
||||
rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045,
|
||||
seconds=-31.473, microseconds=-500003)
|
||||
self.assertEqual(rd3.normalized(),
|
||||
relativedelta(days=-2, hours=-2, minutes=0,
|
||||
seconds=-2, microseconds=-3))
|
||||
|
||||
def testInvalidYearDay(self):
|
||||
with self.assertRaises(ValueError):
|
||||
relativedelta(yearday=367)
|
||||
|
||||
def testAddTimedeltaToUnpopulatedRelativedelta(self):
|
||||
td = timedelta(
|
||||
days=1,
|
||||
seconds=1,
|
||||
microseconds=1,
|
||||
milliseconds=1,
|
||||
minutes=1,
|
||||
hours=1,
|
||||
weeks=1
|
||||
)
|
||||
|
||||
expected = relativedelta(
|
||||
weeks=1,
|
||||
days=1,
|
||||
hours=1,
|
||||
minutes=1,
|
||||
seconds=1,
|
||||
microseconds=1001
|
||||
)
|
||||
|
||||
self.assertEqual(expected, relativedelta() + td)
|
||||
|
||||
def testAddTimedeltaToPopulatedRelativeDelta(self):
|
||||
td = timedelta(
|
||||
days=1,
|
||||
seconds=1,
|
||||
microseconds=1,
|
||||
milliseconds=1,
|
||||
minutes=1,
|
||||
hours=1,
|
||||
weeks=1
|
||||
)
|
||||
|
||||
rd = relativedelta(
|
||||
year=1,
|
||||
month=1,
|
||||
day=1,
|
||||
hour=1,
|
||||
minute=1,
|
||||
second=1,
|
||||
microsecond=1,
|
||||
years=1,
|
||||
months=1,
|
||||
days=1,
|
||||
weeks=1,
|
||||
hours=1,
|
||||
minutes=1,
|
||||
seconds=1,
|
||||
microseconds=1
|
||||
)
|
||||
|
||||
expected = relativedelta(
|
||||
year=1,
|
||||
month=1,
|
||||
day=1,
|
||||
hour=1,
|
||||
minute=1,
|
||||
second=1,
|
||||
microsecond=1,
|
||||
years=1,
|
||||
months=1,
|
||||
weeks=2,
|
||||
days=2,
|
||||
hours=2,
|
||||
minutes=2,
|
||||
seconds=2,
|
||||
microseconds=1002,
|
||||
)
|
||||
|
||||
self.assertEqual(expected, rd + td)
|
||||
|
||||
def testHashable(self):
|
||||
try:
|
||||
{relativedelta(minute=1): 'test'}
|
||||
except:
|
||||
self.fail("relativedelta() failed to hash!")
|
||||
|
||||
|
||||
class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase):
|
||||
"""Test the weeks property getter"""
|
||||
|
||||
def test_one_day(self):
|
||||
rd = relativedelta(days=1)
|
||||
self.assertEqual(rd.days, 1)
|
||||
self.assertEqual(rd.weeks, 0)
|
||||
|
||||
def test_minus_one_day(self):
|
||||
rd = relativedelta(days=-1)
|
||||
self.assertEqual(rd.days, -1)
|
||||
self.assertEqual(rd.weeks, 0)
|
||||
|
||||
def test_height_days(self):
|
||||
rd = relativedelta(days=8)
|
||||
self.assertEqual(rd.days, 8)
|
||||
self.assertEqual(rd.weeks, 1)
|
||||
|
||||
def test_minus_height_days(self):
|
||||
rd = relativedelta(days=-8)
|
||||
self.assertEqual(rd.days, -8)
|
||||
self.assertEqual(rd.weeks, -1)
|
||||
|
||||
|
||||
class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase):
|
||||
"""Test the weeks setter which makes a "smart" update of the days attribute"""
|
||||
|
||||
def test_one_day_set_one_week(self):
|
||||
rd = relativedelta(days=1)
|
||||
rd.weeks = 1 # add 7 days
|
||||
self.assertEqual(rd.days, 8)
|
||||
self.assertEqual(rd.weeks, 1)
|
||||
|
||||
def test_minus_one_day_set_one_week(self):
|
||||
rd = relativedelta(days=-1)
|
||||
rd.weeks = 1 # add 7 days
|
||||
self.assertEqual(rd.days, 6)
|
||||
self.assertEqual(rd.weeks, 0)
|
||||
|
||||
def test_height_days_set_minus_one_week(self):
|
||||
rd = relativedelta(days=8)
|
||||
rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day
|
||||
self.assertEqual(rd.days, -6)
|
||||
self.assertEqual(rd.weeks, 0)
|
||||
|
||||
def test_minus_height_days_set_minus_one_week(self):
|
||||
rd = relativedelta(days=-8)
|
||||
rd.weeks = -1 # does not change anything
|
||||
self.assertEqual(rd.days, -8)
|
||||
self.assertEqual(rd.weeks, -1)
|
||||
|
||||
|
||||
# vim:ts=4:sw=4:et
|
4914
lib/dateutil/test/test_rrule.py
Normal file
4914
lib/dateutil/test/test_rrule.py
Normal file
File diff suppressed because it is too large
Load diff
2811
lib/dateutil/test/test_tz.py
Normal file
2811
lib/dateutil/test/test_tz.py
Normal file
File diff suppressed because it is too large
Load diff
52
lib/dateutil/test/test_utils.py
Normal file
52
lib/dateutil/test/test_utils.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from dateutil import tz
|
||||
from dateutil import utils
|
||||
from dateutil.tz import UTC
|
||||
from dateutil.utils import within_delta
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
NYC = tz.gettz("America/New_York")
|
||||
|
||||
|
||||
@freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003))
|
||||
def test_utils_today():
|
||||
assert utils.today() == datetime(2014, 12, 15, 0, 0, 0)
|
||||
|
||||
|
||||
@freeze_time(datetime(2014, 12, 15, 12), tz_offset=5)
|
||||
def test_utils_today_tz_info():
|
||||
assert utils.today(NYC) == datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC)
|
||||
|
||||
|
||||
@freeze_time(datetime(2014, 12, 15, 23), tz_offset=5)
|
||||
def test_utils_today_tz_info_different_day():
|
||||
assert utils.today(UTC) == datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def test_utils_default_tz_info_naive():
|
||||
dt = datetime(2014, 9, 14, 9, 30)
|
||||
assert utils.default_tzinfo(dt, NYC).tzinfo is NYC
|
||||
|
||||
|
||||
def test_utils_default_tz_info_aware():
|
||||
dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC)
|
||||
assert utils.default_tzinfo(dt, NYC).tzinfo is UTC
|
||||
|
||||
|
||||
def test_utils_within_delta():
|
||||
d1 = datetime(2016, 1, 1, 12, 14, 1, 9)
|
||||
d2 = d1.replace(microsecond=15)
|
||||
|
||||
assert within_delta(d1, d2, timedelta(seconds=1))
|
||||
assert not within_delta(d1, d2, timedelta(microseconds=1))
|
||||
|
||||
|
||||
def test_utils_within_delta_with_negative_delta():
|
||||
d1 = datetime(2016, 1, 1)
|
||||
d2 = datetime(2015, 12, 31)
|
||||
|
||||
assert within_delta(d2, d1, timedelta(days=-1))
|
|
@ -1,986 +0,0 @@
|
|||
# -*- 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
|
||||
timezone.
|
||||
"""
|
||||
import datetime
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
from six import string_types, PY3
|
||||
|
||||
try:
|
||||
from dateutil.tzwin import tzwin, tzwinlocal
|
||||
except ImportError:
|
||||
tzwin = tzwinlocal = None
|
||||
|
||||
relativedelta = None
|
||||
parser = None
|
||||
rrule = None
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
|
||||
|
||||
|
||||
def tzname_in_python2(myfunc):
|
||||
"""Change unicode output into bytestrings in Python 2
|
||||
|
||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||
to unicode strings
|
||||
"""
|
||||
def inner_func(*args, **kwargs):
|
||||
if PY3:
|
||||
return myfunc(*args, **kwargs)
|
||||
else:
|
||||
return myfunc(*args, **kwargs).encode()
|
||||
return inner_func
|
||||
|
||||
ZERO = datetime.timedelta(0)
|
||||
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
|
||||
|
||||
|
||||
class tzutc(datetime.tzinfo):
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return ZERO
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, tzutc) or
|
||||
(isinstance(other, tzoffset) and other._offset == ZERO))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s()" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzoffset(datetime.tzinfo):
|
||||
|
||||
def __init__(self, name, offset):
|
||||
self._name = name
|
||||
self._offset = datetime.timedelta(seconds=offset)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._offset
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return self._name
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, tzoffset) and
|
||||
self._offset == other._offset)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, %s)" % (self.__class__.__name__,
|
||||
repr(self._name),
|
||||
self._offset.days*86400+self._offset.seconds)
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzlocal(datetime.tzinfo):
|
||||
|
||||
_std_offset = datetime.timedelta(seconds=-time.timezone)
|
||||
if time.daylight:
|
||||
_dst_offset = datetime.timedelta(seconds=-time.altzone)
|
||||
else:
|
||||
_dst_offset = _std_offset
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset-self._std_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return time.tzname[self._isdst(dt)]
|
||||
|
||||
def _isdst(self, dt):
|
||||
# We can't use mktime here. It is unstable when deciding if
|
||||
# the hour near to a change is DST or not.
|
||||
#
|
||||
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
|
||||
# dt.minute, dt.second, dt.weekday(), 0, -1))
|
||||
# return time.localtime(timestamp).tm_isdst
|
||||
#
|
||||
# The code above yields the following result:
|
||||
#
|
||||
# >>> import tz, datetime
|
||||
# >>> t = tz.tzlocal()
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
|
||||
# 'BRST'
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRST'
|
||||
# >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
||||
# 'BRDT'
|
||||
#
|
||||
# Here is a more stable implementation:
|
||||
#
|
||||
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ dt.hour * 3600
|
||||
+ dt.minute * 60
|
||||
+ dt.second)
|
||||
return time.localtime(timestamp+time.timezone).tm_isdst
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzlocal):
|
||||
return False
|
||||
return (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset)
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s()" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class _ttinfo(object):
|
||||
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
|
||||
|
||||
def __init__(self):
|
||||
for attr in self.__slots__:
|
||||
setattr(self, attr, None)
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, _ttinfo):
|
||||
return False
|
||||
return (self.offset == other.offset and
|
||||
self.delta == other.delta and
|
||||
self.isdst == other.isdst and
|
||||
self.abbr == other.abbr and
|
||||
self.isstd == other.isstd and
|
||||
self.isgmt == other.isgmt)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __getstate__(self):
|
||||
state = {}
|
||||
for name in self.__slots__:
|
||||
state[name] = getattr(self, name, None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for name in self.__slots__:
|
||||
if name in state:
|
||||
setattr(self, name, state[name])
|
||||
|
||||
|
||||
class tzfile(datetime.tzinfo):
|
||||
|
||||
# http://www.twinsun.com/tz/tz-link.htm
|
||||
# ftp://ftp.iana.org/tz/tz*.tar.gz
|
||||
|
||||
def __init__(self, fileobj, filename=None):
|
||||
file_opened_here = False
|
||||
if isinstance(fileobj, string_types):
|
||||
self._filename = fileobj
|
||||
fileobj = open(fileobj, 'rb')
|
||||
file_opened_here = True
|
||||
elif filename is not None:
|
||||
self._filename = filename
|
||||
elif hasattr(fileobj, "name"):
|
||||
self._filename = fileobj.name
|
||||
else:
|
||||
self._filename = repr(fileobj)
|
||||
|
||||
# From tzfile(5):
|
||||
#
|
||||
# The time zone information files used by tzset(3)
|
||||
# begin with the magic characters "TZif" to identify
|
||||
# them as time zone information files, followed by
|
||||
# sixteen bytes reserved for future use, followed by
|
||||
# six four-byte values of type long, written in a
|
||||
# ``standard'' byte order (the high-order byte
|
||||
# of the value is written first).
|
||||
try:
|
||||
if fileobj.read(4).decode() != "TZif":
|
||||
raise ValueError("magic not found")
|
||||
|
||||
fileobj.read(16)
|
||||
|
||||
(
|
||||
# The number of UTC/local indicators stored in the file.
|
||||
ttisgmtcnt,
|
||||
|
||||
# The number of standard/wall indicators stored in the file.
|
||||
ttisstdcnt,
|
||||
|
||||
# The number of leap seconds for which data is
|
||||
# stored in the file.
|
||||
leapcnt,
|
||||
|
||||
# The number of "transition times" for which data
|
||||
# is stored in the file.
|
||||
timecnt,
|
||||
|
||||
# The number of "local time types" for which data
|
||||
# is stored in the file (must not be zero).
|
||||
typecnt,
|
||||
|
||||
# The number of characters of "time zone
|
||||
# abbreviation strings" stored in the file.
|
||||
charcnt,
|
||||
|
||||
) = struct.unpack(">6l", fileobj.read(24))
|
||||
|
||||
# The above header is followed by tzh_timecnt four-byte
|
||||
# values of type long, sorted in ascending order.
|
||||
# These values are written in ``standard'' byte order.
|
||||
# Each is used as a transition time (as returned by
|
||||
# time(2)) at which the rules for computing local time
|
||||
# change.
|
||||
|
||||
if timecnt:
|
||||
self._trans_list = struct.unpack(">%dl" % timecnt,
|
||||
fileobj.read(timecnt*4))
|
||||
else:
|
||||
self._trans_list = []
|
||||
|
||||
# Next come tzh_timecnt one-byte values of type unsigned
|
||||
# char; each one tells which of the different types of
|
||||
# ``local time'' types described in the file is associated
|
||||
# with the same-indexed transition time. These values
|
||||
# serve as indices into an array of ttinfo structures that
|
||||
# appears next in the file.
|
||||
|
||||
if timecnt:
|
||||
self._trans_idx = struct.unpack(">%dB" % timecnt,
|
||||
fileobj.read(timecnt))
|
||||
else:
|
||||
self._trans_idx = []
|
||||
|
||||
# Each ttinfo structure is written as a four-byte value
|
||||
# for tt_gmtoff of type long, in a standard byte
|
||||
# order, followed by a one-byte value for tt_isdst
|
||||
# and a one-byte value for tt_abbrind. In each
|
||||
# structure, tt_gmtoff gives the number of
|
||||
# seconds to be added to UTC, tt_isdst tells whether
|
||||
# tm_isdst should be set by localtime(3), and
|
||||
# tt_abbrind serves as an index into the array of
|
||||
# time zone abbreviation characters that follow the
|
||||
# ttinfo structure(s) in the file.
|
||||
|
||||
ttinfo = []
|
||||
|
||||
for i in range(typecnt):
|
||||
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
|
||||
|
||||
abbr = fileobj.read(charcnt).decode()
|
||||
|
||||
# Then there are tzh_leapcnt pairs of four-byte
|
||||
# values, written in standard byte order; the
|
||||
# first value of each pair gives the time (as
|
||||
# returned by time(2)) at which a leap second
|
||||
# occurs; the second gives the total number of
|
||||
# leap seconds to be applied after the given time.
|
||||
# The pairs of values are sorted in ascending order
|
||||
# by time.
|
||||
|
||||
# Not used, for now
|
||||
# if leapcnt:
|
||||
# leap = struct.unpack(">%dl" % (leapcnt*2),
|
||||
# fileobj.read(leapcnt*8))
|
||||
|
||||
# Then there are tzh_ttisstdcnt standard/wall
|
||||
# indicators, each stored as a one-byte value;
|
||||
# they tell whether the transition times associated
|
||||
# with local time types were specified as standard
|
||||
# time or wall clock time, and are used when
|
||||
# a time zone file is used in handling POSIX-style
|
||||
# time zone environment variables.
|
||||
|
||||
if ttisstdcnt:
|
||||
isstd = struct.unpack(">%db" % ttisstdcnt,
|
||||
fileobj.read(ttisstdcnt))
|
||||
|
||||
# Finally, there are tzh_ttisgmtcnt UTC/local
|
||||
# indicators, each stored as a one-byte value;
|
||||
# they tell whether the transition times associated
|
||||
# with local time types were specified as UTC or
|
||||
# local time, and are used when a time zone file
|
||||
# is used in handling POSIX-style time zone envi-
|
||||
# ronment variables.
|
||||
|
||||
if ttisgmtcnt:
|
||||
isgmt = struct.unpack(">%db" % ttisgmtcnt,
|
||||
fileobj.read(ttisgmtcnt))
|
||||
|
||||
# ** Everything has been read **
|
||||
finally:
|
||||
if file_opened_here:
|
||||
fileobj.close()
|
||||
|
||||
# Build ttinfo list
|
||||
self._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 = (gmtoff+30)//60*60
|
||||
tti = _ttinfo()
|
||||
tti.offset = gmtoff
|
||||
tti.delta = datetime.timedelta(seconds=gmtoff)
|
||||
tti.isdst = isdst
|
||||
tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
|
||||
tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
|
||||
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
|
||||
self._ttinfo_list.append(tti)
|
||||
|
||||
# Replace ttinfo indexes for ttinfo objects.
|
||||
trans_idx = []
|
||||
for idx in self._trans_idx:
|
||||
trans_idx.append(self._ttinfo_list[idx])
|
||||
self._trans_idx = tuple(trans_idx)
|
||||
|
||||
# Set standard, dst, and before ttinfos. before will be
|
||||
# used when a given time is before any transitions,
|
||||
# and will be set to the first non-dst ttinfo, or to
|
||||
# the first dst, if all of them are dst.
|
||||
self._ttinfo_std = None
|
||||
self._ttinfo_dst = None
|
||||
self._ttinfo_before = None
|
||||
if self._ttinfo_list:
|
||||
if not self._trans_list:
|
||||
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
|
||||
else:
|
||||
for i in range(timecnt-1, -1, -1):
|
||||
tti = self._trans_idx[i]
|
||||
if not self._ttinfo_std and not tti.isdst:
|
||||
self._ttinfo_std = tti
|
||||
elif not self._ttinfo_dst and tti.isdst:
|
||||
self._ttinfo_dst = tti
|
||||
if self._ttinfo_std and self._ttinfo_dst:
|
||||
break
|
||||
else:
|
||||
if self._ttinfo_dst and not self._ttinfo_std:
|
||||
self._ttinfo_std = self._ttinfo_dst
|
||||
|
||||
for tti in self._ttinfo_list:
|
||||
if not tti.isdst:
|
||||
self._ttinfo_before = tti
|
||||
break
|
||||
else:
|
||||
self._ttinfo_before = self._ttinfo_list[0]
|
||||
|
||||
# Now fix transition times to become relative to wall time.
|
||||
#
|
||||
# I'm not sure about this. In my tests, the tz source file
|
||||
# is setup to wall time, and in the binary file isstd and
|
||||
# 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 = 0
|
||||
self._trans_list = list(self._trans_list)
|
||||
for i in range(len(self._trans_list)):
|
||||
tti = self._trans_idx[i]
|
||||
if not tti.isdst:
|
||||
# This is std time.
|
||||
self._trans_list[i] += tti.offset
|
||||
laststdoffset = tti.offset
|
||||
else:
|
||||
# This is dst time. Convert to std.
|
||||
self._trans_list[i] += laststdoffset
|
||||
self._trans_list = tuple(self._trans_list)
|
||||
|
||||
def _find_ttinfo(self, dt, laststd=0):
|
||||
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ dt.hour * 3600
|
||||
+ dt.minute * 60
|
||||
+ dt.second)
|
||||
idx = 0
|
||||
for trans in self._trans_list:
|
||||
if timestamp < trans:
|
||||
break
|
||||
idx += 1
|
||||
else:
|
||||
return self._ttinfo_std
|
||||
if idx == 0:
|
||||
return self._ttinfo_before
|
||||
if laststd:
|
||||
while idx > 0:
|
||||
tti = self._trans_idx[idx-1]
|
||||
if not tti.isdst:
|
||||
return tti
|
||||
idx -= 1
|
||||
else:
|
||||
return self._ttinfo_std
|
||||
else:
|
||||
return self._trans_idx[idx-1]
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if not self._ttinfo_std:
|
||||
return ZERO
|
||||
return self._find_ttinfo(dt).delta
|
||||
|
||||
def dst(self, dt):
|
||||
if not self._ttinfo_dst:
|
||||
return ZERO
|
||||
tti = self._find_ttinfo(dt)
|
||||
if not tti.isdst:
|
||||
return ZERO
|
||||
|
||||
# The documentation says that utcoffset()-dst() must
|
||||
# be constant for every dt.
|
||||
return tti.delta-self._find_ttinfo(dt, laststd=1).delta
|
||||
|
||||
# An alternative for that would be:
|
||||
#
|
||||
# return self._ttinfo_dst.offset-self._ttinfo_std.offset
|
||||
#
|
||||
# However, this class stores historical changes in the
|
||||
# dst offset, so I belive that this wouldn't be the right
|
||||
# way to implement this.
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if not self._ttinfo_std:
|
||||
return None
|
||||
return self._find_ttinfo(dt).abbr
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzfile):
|
||||
return False
|
||||
return (self._trans_list == other._trans_list and
|
||||
self._trans_idx == other._trans_idx and
|
||||
self._ttinfo_list == other._ttinfo_list)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
|
||||
|
||||
def __reduce__(self):
|
||||
if not os.path.isfile(self._filename):
|
||||
raise ValueError("Unpickable %s class" % self.__class__.__name__)
|
||||
return (self.__class__, (self._filename,))
|
||||
|
||||
|
||||
class tzrange(datetime.tzinfo):
|
||||
def __init__(self, stdabbr, stdoffset=None,
|
||||
dstabbr=None, dstoffset=None,
|
||||
start=None, end=None):
|
||||
global relativedelta
|
||||
if not relativedelta:
|
||||
from dateutil import relativedelta
|
||||
self._std_abbr = stdabbr
|
||||
self._dst_abbr = dstabbr
|
||||
if stdoffset is not None:
|
||||
self._std_offset = datetime.timedelta(seconds=stdoffset)
|
||||
else:
|
||||
self._std_offset = ZERO
|
||||
if dstoffset is not None:
|
||||
self._dst_offset = datetime.timedelta(seconds=dstoffset)
|
||||
elif dstabbr and stdoffset is not None:
|
||||
self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
|
||||
else:
|
||||
self._dst_offset = ZERO
|
||||
if dstabbr and start is None:
|
||||
self._start_delta = relativedelta.relativedelta(
|
||||
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
|
||||
else:
|
||||
self._start_delta = start
|
||||
if dstabbr and end is None:
|
||||
self._end_delta = relativedelta.relativedelta(
|
||||
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
|
||||
else:
|
||||
self._end_delta = end
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_offset-self._std_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_abbr
|
||||
else:
|
||||
return self._std_abbr
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self._start_delta:
|
||||
return False
|
||||
year = datetime.datetime(dt.year, 1, 1)
|
||||
start = year+self._start_delta
|
||||
end = year+self._end_delta
|
||||
dt = dt.replace(tzinfo=None)
|
||||
if start < end:
|
||||
return dt >= start and dt < end
|
||||
else:
|
||||
return dt >= start or dt < end
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, tzrange):
|
||||
return False
|
||||
return (self._std_abbr == other._std_abbr and
|
||||
self._dst_abbr == other._dst_abbr and
|
||||
self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset and
|
||||
self._start_delta == other._start_delta and
|
||||
self._end_delta == other._end_delta)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(...)" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzstr(tzrange):
|
||||
|
||||
def __init__(self, s):
|
||||
global parser
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
self._s = s
|
||||
|
||||
res = parser._parsetz(s)
|
||||
if res is None:
|
||||
raise ValueError("unknown string format")
|
||||
|
||||
# Here we break the compatibility with the TZ variable handling.
|
||||
# GMT-3 actually *means* the timezone -3.
|
||||
if res.stdabbr in ("GMT", "UTC"):
|
||||
res.stdoffset *= -1
|
||||
|
||||
# We must initialize it first, since _delta() needs
|
||||
# _std_offset and _dst_offset set. Use False in start/end
|
||||
# to avoid building it two times.
|
||||
tzrange.__init__(self, res.stdabbr, res.stdoffset,
|
||||
res.dstabbr, res.dstoffset,
|
||||
start=False, end=False)
|
||||
|
||||
if not res.dstabbr:
|
||||
self._start_delta = None
|
||||
self._end_delta = None
|
||||
else:
|
||||
self._start_delta = self._delta(res.start)
|
||||
if self._start_delta:
|
||||
self._end_delta = self._delta(res.end, isend=1)
|
||||
|
||||
def _delta(self, x, isend=0):
|
||||
kwargs = {}
|
||||
if x.month is not None:
|
||||
kwargs["month"] = x.month
|
||||
if x.weekday is not None:
|
||||
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
|
||||
if x.week > 0:
|
||||
kwargs["day"] = 1
|
||||
else:
|
||||
kwargs["day"] = 31
|
||||
elif x.day:
|
||||
kwargs["day"] = x.day
|
||||
elif x.yday is not None:
|
||||
kwargs["yearday"] = x.yday
|
||||
elif x.jyday is not None:
|
||||
kwargs["nlyearday"] = x.jyday
|
||||
if not kwargs:
|
||||
# Default is to start on first sunday of april, and end
|
||||
# on last sunday of october.
|
||||
if not isend:
|
||||
kwargs["month"] = 4
|
||||
kwargs["day"] = 1
|
||||
kwargs["weekday"] = relativedelta.SU(+1)
|
||||
else:
|
||||
kwargs["month"] = 10
|
||||
kwargs["day"] = 31
|
||||
kwargs["weekday"] = relativedelta.SU(-1)
|
||||
if x.time is not None:
|
||||
kwargs["seconds"] = x.time
|
||||
else:
|
||||
# Default is 2AM.
|
||||
kwargs["seconds"] = 7200
|
||||
if isend:
|
||||
# Convert to standard time, to follow the documented way
|
||||
# of working with the extra hour. See the documentation
|
||||
# of the tzinfo class.
|
||||
delta = self._dst_offset-self._std_offset
|
||||
kwargs["seconds"] -= delta.seconds+delta.days*86400
|
||||
return relativedelta.relativedelta(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
||||
|
||||
|
||||
class _tzicalvtzcomp(object):
|
||||
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
|
||||
tzname=None, rrule=None):
|
||||
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
|
||||
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
|
||||
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
|
||||
self.isdst = isdst
|
||||
self.tzname = tzname
|
||||
self.rrule = rrule
|
||||
|
||||
|
||||
class _tzicalvtz(datetime.tzinfo):
|
||||
def __init__(self, tzid, comps=[]):
|
||||
self._tzid = tzid
|
||||
self._comps = comps
|
||||
self._cachedate = []
|
||||
self._cachecomp = []
|
||||
|
||||
def _find_comp(self, dt):
|
||||
if len(self._comps) == 1:
|
||||
return self._comps[0]
|
||||
dt = dt.replace(tzinfo=None)
|
||||
try:
|
||||
return self._cachecomp[self._cachedate.index(dt)]
|
||||
except ValueError:
|
||||
pass
|
||||
lastcomp = None
|
||||
lastcompdt = None
|
||||
for comp in self._comps:
|
||||
if not comp.isdst:
|
||||
# Handle the extra hour in DST -> STD
|
||||
compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
|
||||
else:
|
||||
compdt = comp.rrule.before(dt, inc=True)
|
||||
if compdt and (not lastcompdt or lastcompdt < compdt):
|
||||
lastcompdt = compdt
|
||||
lastcomp = comp
|
||||
if not lastcomp:
|
||||
# RFC says nothing about what to do when a given
|
||||
# time is before the first onset date. We'll look for the
|
||||
# first standard component, or the first component, if
|
||||
# none is found.
|
||||
for comp in self._comps:
|
||||
if not comp.isdst:
|
||||
lastcomp = comp
|
||||
break
|
||||
else:
|
||||
lastcomp = comp[0]
|
||||
self._cachedate.insert(0, dt)
|
||||
self._cachecomp.insert(0, lastcomp)
|
||||
if len(self._cachedate) > 10:
|
||||
self._cachedate.pop()
|
||||
self._cachecomp.pop()
|
||||
return lastcomp
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._find_comp(dt).tzoffsetto
|
||||
|
||||
def dst(self, dt):
|
||||
comp = self._find_comp(dt)
|
||||
if comp.isdst:
|
||||
return comp.tzoffsetdiff
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
return self._find_comp(dt).tzname
|
||||
|
||||
def __repr__(self):
|
||||
return "<tzicalvtz %s>" % repr(self._tzid)
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
|
||||
class tzical(object):
|
||||
def __init__(self, fileobj):
|
||||
global rrule
|
||||
if not rrule:
|
||||
from dateutil import rrule
|
||||
|
||||
if isinstance(fileobj, string_types):
|
||||
self._s = fileobj
|
||||
# ical should be encoded in UTF-8 with CRLF
|
||||
fileobj = open(fileobj, 'r')
|
||||
elif hasattr(fileobj, "name"):
|
||||
self._s = fileobj.name
|
||||
else:
|
||||
self._s = repr(fileobj)
|
||||
|
||||
self._vtz = {}
|
||||
|
||||
self._parse_rfc(fileobj.read())
|
||||
|
||||
def keys(self):
|
||||
return list(self._vtz.keys())
|
||||
|
||||
def get(self, tzid=None):
|
||||
if tzid is None:
|
||||
keys = list(self._vtz.keys())
|
||||
if len(keys) == 0:
|
||||
raise ValueError("no timezones defined")
|
||||
elif len(keys) > 1:
|
||||
raise ValueError("more than one timezone available")
|
||||
tzid = keys[0]
|
||||
return self._vtz.get(tzid)
|
||||
|
||||
def _parse_offset(self, s):
|
||||
s = s.strip()
|
||||
if not s:
|
||||
raise ValueError("empty offset")
|
||||
if s[0] in ('+', '-'):
|
||||
signal = (-1, +1)[s[0] == '+']
|
||||
s = s[1:]
|
||||
else:
|
||||
signal = +1
|
||||
if len(s) == 4:
|
||||
return (int(s[:2])*3600+int(s[2:])*60)*signal
|
||||
elif len(s) == 6:
|
||||
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
|
||||
else:
|
||||
raise ValueError("invalid offset: "+s)
|
||||
|
||||
def _parse_rfc(self, s):
|
||||
lines = s.splitlines()
|
||||
if not lines:
|
||||
raise ValueError("empty string")
|
||||
|
||||
# Unfold
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].rstrip()
|
||||
if not line:
|
||||
del lines[i]
|
||||
elif i > 0 and line[0] == " ":
|
||||
lines[i-1] += line[1:]
|
||||
del lines[i]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
tzid = None
|
||||
comps = []
|
||||
invtz = False
|
||||
comptype = None
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
name, value = line.split(':', 1)
|
||||
parms = name.split(';')
|
||||
if not parms:
|
||||
raise ValueError("empty property name")
|
||||
name = parms[0].upper()
|
||||
parms = parms[1:]
|
||||
if invtz:
|
||||
if name == "BEGIN":
|
||||
if value in ("STANDARD", "DAYLIGHT"):
|
||||
# Process component
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unknown component: "+value)
|
||||
comptype = value
|
||||
founddtstart = False
|
||||
tzoffsetfrom = None
|
||||
tzoffsetto = None
|
||||
rrulelines = []
|
||||
tzname = None
|
||||
elif name == "END":
|
||||
if value == "VTIMEZONE":
|
||||
if comptype:
|
||||
raise ValueError("component not closed: "+comptype)
|
||||
if not tzid:
|
||||
raise ValueError("mandatory TZID not found")
|
||||
if not comps:
|
||||
raise ValueError(
|
||||
"at least one component is needed")
|
||||
# Process vtimezone
|
||||
self._vtz[tzid] = _tzicalvtz(tzid, comps)
|
||||
invtz = False
|
||||
elif value == comptype:
|
||||
if not founddtstart:
|
||||
raise ValueError("mandatory DTSTART not found")
|
||||
if tzoffsetfrom is None:
|
||||
raise ValueError(
|
||||
"mandatory TZOFFSETFROM not found")
|
||||
if tzoffsetto is None:
|
||||
raise ValueError(
|
||||
"mandatory TZOFFSETFROM not found")
|
||||
# Process component
|
||||
rr = None
|
||||
if rrulelines:
|
||||
rr = rrule.rrulestr("\n".join(rrulelines),
|
||||
compatible=True,
|
||||
ignoretz=True,
|
||||
cache=True)
|
||||
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
|
||||
(comptype == "DAYLIGHT"),
|
||||
tzname, rr)
|
||||
comps.append(comp)
|
||||
comptype = None
|
||||
else:
|
||||
raise ValueError("invalid component end: "+value)
|
||||
elif comptype:
|
||||
if name == "DTSTART":
|
||||
rrulelines.append(line)
|
||||
founddtstart = True
|
||||
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
|
||||
rrulelines.append(line)
|
||||
elif name == "TZOFFSETFROM":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported %s parm: %s " % (name, parms[0]))
|
||||
tzoffsetfrom = self._parse_offset(value)
|
||||
elif name == "TZOFFSETTO":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZOFFSETTO parm: "+parms[0])
|
||||
tzoffsetto = self._parse_offset(value)
|
||||
elif name == "TZNAME":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZNAME parm: "+parms[0])
|
||||
tzname = value
|
||||
elif name == "COMMENT":
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unsupported property: "+name)
|
||||
else:
|
||||
if name == "TZID":
|
||||
if parms:
|
||||
raise ValueError(
|
||||
"unsupported TZID parm: "+parms[0])
|
||||
tzid = value
|
||||
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
|
||||
pass
|
||||
else:
|
||||
raise ValueError("unsupported property: "+name)
|
||||
elif name == "BEGIN" and value == "VTIMEZONE":
|
||||
tzid = None
|
||||
comps = []
|
||||
invtz = True
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
||||
|
||||
if sys.platform != "win32":
|
||||
TZFILES = ["/etc/localtime", "localtime"]
|
||||
TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
|
||||
else:
|
||||
TZFILES = []
|
||||
TZPATHS = []
|
||||
|
||||
|
||||
def gettz(name=None):
|
||||
tz = None
|
||||
if not name:
|
||||
try:
|
||||
name = os.environ["TZ"]
|
||||
except KeyError:
|
||||
pass
|
||||
if name is None or name == ":":
|
||||
for filepath in TZFILES:
|
||||
if not os.path.isabs(filepath):
|
||||
filename = filepath
|
||||
for path in TZPATHS:
|
||||
filepath = os.path.join(path, filename)
|
||||
if os.path.isfile(filepath):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if os.path.isfile(filepath):
|
||||
try:
|
||||
tz = tzfile(filepath)
|
||||
break
|
||||
except (IOError, OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
tz = tzlocal()
|
||||
else:
|
||||
if name.startswith(":"):
|
||||
name = name[:-1]
|
||||
if os.path.isabs(name):
|
||||
if os.path.isfile(name):
|
||||
tz = tzfile(name)
|
||||
else:
|
||||
tz = None
|
||||
else:
|
||||
for path in TZPATHS:
|
||||
filepath = os.path.join(path, name)
|
||||
if not os.path.isfile(filepath):
|
||||
filepath = filepath.replace(' ', '_')
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
try:
|
||||
tz = tzfile(filepath)
|
||||
break
|
||||
except (IOError, OSError, ValueError):
|
||||
pass
|
||||
else:
|
||||
tz = None
|
||||
if tzwin is not None:
|
||||
try:
|
||||
tz = tzwin(name)
|
||||
except WindowsError:
|
||||
tz = None
|
||||
if not tz:
|
||||
from dateutil.zoneinfo import gettz
|
||||
tz = gettz(name)
|
||||
if not tz:
|
||||
for c in name:
|
||||
# name must have at least one offset to be a tzstr
|
||||
if c in "0123456789":
|
||||
try:
|
||||
tz = tzstr(name)
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
else:
|
||||
if name in ("GMT", "UTC"):
|
||||
tz = tzutc()
|
||||
elif name in time.tzname:
|
||||
tz = tzlocal()
|
||||
return tz
|
||||
|
||||
# vim:ts=4:sw=4:et
|
12
lib/dateutil/tz/__init__.py
Normal file
12
lib/dateutil/tz/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .tz import *
|
||||
from .tz import __doc__
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||
|
||||
|
||||
class DeprecatedTzFormatWarning(Warning):
|
||||
"""Warning raised when time zones are parsed from deprecated formats."""
|
419
lib/dateutil/tz/_common.py
Normal file
419
lib/dateutil/tz/_common.py
Normal file
|
@ -0,0 +1,419 @@
|
|||
from six import PY2
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
|
||||
ZERO = timedelta(0)
|
||||
|
||||
__all__ = ['tzname_in_python2', 'enfold']
|
||||
|
||||
|
||||
def tzname_in_python2(namefunc):
|
||||
"""Change unicode output into bytestrings in Python 2
|
||||
|
||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||
to unicode strings
|
||||
"""
|
||||
if PY2:
|
||||
@wraps(namefunc)
|
||||
def adjust_encoding(*args, **kwargs):
|
||||
name = namefunc(*args, **kwargs)
|
||||
if name is not None:
|
||||
name = name.encode()
|
||||
|
||||
return name
|
||||
|
||||
return adjust_encoding
|
||||
else:
|
||||
return namefunc
|
||||
|
||||
|
||||
# The following is adapted from Alexander Belopolsky's tz library
|
||||
# https://github.com/abalkin/tz
|
||||
if hasattr(datetime, 'fold'):
|
||||
# This is the pre-python 3.6 fold situation
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
return dt.replace(fold=fold)
|
||||
|
||||
else:
|
||||
class _DatetimeWithFold(datetime):
|
||||
"""
|
||||
This is a class designed to provide a PEP 495-compliant interface for
|
||||
Python versions before 3.6. It is used only for dates in a fold, so
|
||||
the ``fold`` attribute is fixed at ``1``.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def replace(self, *args, **kwargs):
|
||||
"""
|
||||
Return a datetime with the same attributes, except for those
|
||||
attributes given new values by whichever keyword arguments are
|
||||
specified. Note that tzinfo=None can be specified to create a naive
|
||||
datetime from an aware datetime with no conversion of date and time
|
||||
data.
|
||||
|
||||
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||
"""
|
||||
argnames = (
|
||||
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||
'microsecond', 'tzinfo'
|
||||
)
|
||||
|
||||
for arg, argname in zip(args, argnames):
|
||||
if argname in kwargs:
|
||||
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||
|
||||
kwargs[argname] = arg
|
||||
|
||||
for argname in argnames:
|
||||
if argname not in kwargs:
|
||||
kwargs[argname] = getattr(self, argname)
|
||||
|
||||
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||
|
||||
return dt_class(**kwargs)
|
||||
|
||||
@property
|
||||
def fold(self):
|
||||
return 1
|
||||
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if getattr(dt, 'fold', 0) == fold:
|
||||
return dt
|
||||
|
||||
args = dt.timetuple()[:6]
|
||||
args += (dt.microsecond, dt.tzinfo)
|
||||
|
||||
if fold:
|
||||
return _DatetimeWithFold(*args)
|
||||
else:
|
||||
return datetime(*args)
|
||||
|
||||
|
||||
def _validate_fromutc_inputs(f):
|
||||
"""
|
||||
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||
object and that ``self`` is attached as its ``tzinfo``.
|
||||
"""
|
||||
@wraps(f)
|
||||
def fromutc(self, dt):
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
return f(self, dt)
|
||||
|
||||
return fromutc
|
||||
|
||||
|
||||
class _tzinfo(tzinfo):
|
||||
"""
|
||||
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||
"""
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
|
||||
dt = dt.replace(tzinfo=self)
|
||||
|
||||
wall_0 = enfold(dt, fold=0)
|
||||
wall_1 = enfold(dt, fold=1)
|
||||
|
||||
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||
|
||||
return same_dt and not same_offset
|
||||
|
||||
def _fold_status(self, dt_utc, dt_wall):
|
||||
"""
|
||||
Determine the fold status of a "wall" datetime, given a representation
|
||||
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||
datetimes, and that this offset is the actual number of hours separating
|
||||
``dt_utc`` and ``dt_wall``.
|
||||
|
||||
:param dt_utc:
|
||||
Representation of the datetime as UTC
|
||||
|
||||
:param dt_wall:
|
||||
Representation of the datetime as "wall time". This parameter must
|
||||
either have a `fold` attribute or have a fold-naive
|
||||
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||
fail.
|
||||
"""
|
||||
if self.is_ambiguous(dt_wall):
|
||||
delta_wall = dt_wall - dt_utc
|
||||
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||
else:
|
||||
_fold = 0
|
||||
|
||||
return _fold
|
||||
|
||||
def _fold(self, dt):
|
||||
return getattr(dt, 'fold', 0)
|
||||
|
||||
def _fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
|
||||
# Re-implement the algorithm from Python's datetime.py
|
||||
dtoff = dt.utcoffset()
|
||||
if dtoff is None:
|
||||
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||
"result")
|
||||
|
||||
# The original datetime.py code assumes that `dst()` defaults to
|
||||
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||
dtdst = dt.dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc() requires a non-None dst() result")
|
||||
delta = dtoff - dtdst
|
||||
|
||||
dt += delta
|
||||
# Set fold=1 so we can default to being in the fold for
|
||||
# ambiguous dates.
|
||||
dtdst = enfold(dt, fold=1).dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||
"results; cannot convert")
|
||||
return dt + dtdst
|
||||
|
||||
@_validate_fromutc_inputs
|
||||
def fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
dt_wall = self._fromutc(dt)
|
||||
|
||||
# Calculate the fold status given the two datetimes.
|
||||
_fold = self._fold_status(dt, dt_wall)
|
||||
|
||||
# Set the default fold value for ambiguous dates
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
|
||||
class tzrangebase(_tzinfo):
|
||||
"""
|
||||
This is an abstract base class for time zones represented by an annual
|
||||
transition into and out of DST. Child classes should implement the following
|
||||
methods:
|
||||
|
||||
* ``__init__(self, *args, **kwargs)``
|
||||
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||
datetimes representing the DST on and off transitions in standard
|
||||
time.
|
||||
|
||||
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||
following attributes:
|
||||
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||
representing the respective UTC offsets.
|
||||
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||
abbreviations in DST and STD, respectively.
|
||||
* ``_hasdst``: Whether or not the zone has DST.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||
|
||||
def utcoffset(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_base_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_abbr
|
||||
else:
|
||||
return self._std_abbr
|
||||
|
||||
def fromutc(self, dt):
|
||||
""" Given a datetime in UTC, return local time """
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
# Get transitions - if there are none, fixed offset
|
||||
transitions = self.transitions(dt.year)
|
||||
if transitions is None:
|
||||
return dt + self.utcoffset(dt)
|
||||
|
||||
# Get the transition times in UTC
|
||||
dston, dstoff = transitions
|
||||
|
||||
dston -= self._std_offset
|
||||
dstoff -= self._std_offset
|
||||
|
||||
utc_transitions = (dston, dstoff)
|
||||
dt_utc = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||
|
||||
if isdst:
|
||||
dt_wall = dt + self._dst_offset
|
||||
else:
|
||||
dt_wall = dt + self._std_offset
|
||||
|
||||
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if not self.hasdst:
|
||||
return False
|
||||
|
||||
start, end = self.transitions(dt.year)
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
return (end <= dt < end + self._dst_base_offset)
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self.hasdst:
|
||||
return False
|
||||
elif dt is None:
|
||||
return None
|
||||
|
||||
transitions = self.transitions(dt.year)
|
||||
|
||||
if transitions is None:
|
||||
return False
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt, transitions)
|
||||
|
||||
# Handle ambiguous dates
|
||||
if not isdst and self.is_ambiguous(dt):
|
||||
return not self._fold(dt)
|
||||
else:
|
||||
return isdst
|
||||
|
||||
def _naive_isdst(self, dt, transitions):
|
||||
dston, dstoff = transitions
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
if dston < dstoff:
|
||||
isdst = dston <= dt < dstoff
|
||||
else:
|
||||
isdst = not dstoff <= dt < dston
|
||||
|
||||
return isdst
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_offset - self._std_offset
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(...)" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
80
lib/dateutil/tz/_factories.py
Normal file
80
lib/dateutil/tz/_factories.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from datetime import timedelta
|
||||
import weakref
|
||||
from collections import OrderedDict
|
||||
|
||||
from six.moves import _thread
|
||||
|
||||
|
||||
class _TzSingleton(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instance = None
|
||||
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||
|
||||
def __call__(cls):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||
return cls.__instance
|
||||
|
||||
|
||||
class _TzFactory(type):
|
||||
def instance(cls, *args, **kwargs):
|
||||
"""Alternate constructor that returns a fresh instance"""
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class _TzOffsetFactory(_TzFactory):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instances = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls._cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, name, offset):
|
||||
if isinstance(offset, timedelta):
|
||||
key = (name, offset.total_seconds())
|
||||
else:
|
||||
key = (name, offset)
|
||||
|
||||
instance = cls.__instances.get(key, None)
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(name, offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls._cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
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 = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls.__cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, s, posix_offset=False):
|
||||
key = (s, posix_offset)
|
||||
instance = cls.__instances.get(key, None)
|
||||
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(s, posix_offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls.__cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||
cls.__strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
1849
lib/dateutil/tz/tz.py
Normal file
1849
lib/dateutil/tz/tz.py
Normal file
File diff suppressed because it is too large
Load diff
370
lib/dateutil/tz/win.py
Normal file
370
lib/dateutil/tz/win.py
Normal file
|
@ -0,0 +1,370 @@
|
|||
# -*- 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
|
||||
|
||||
from six.moves import winreg
|
||||
from six import text_type
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
except ValueError:
|
||||
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||
raise ImportError("Running tzwin on non-Windows system")
|
||||
|
||||
from ._common import tzrangebase
|
||||
|
||||
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||
|
||||
ONEWEEK = datetime.timedelta(7)
|
||||
|
||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||
|
||||
|
||||
def _settzkeyname():
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
try:
|
||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||
TZKEYNAME = TZKEYNAMENT
|
||||
except WindowsError:
|
||||
TZKEYNAME = TZKEYNAME9X
|
||||
handle.Close()
|
||||
return TZKEYNAME
|
||||
|
||||
|
||||
TZKEYNAME = _settzkeyname()
|
||||
|
||||
|
||||
class tzres(object):
|
||||
"""
|
||||
Class for accessing ``tzres.dll``, which contains timezone name related
|
||||
resources.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||
|
||||
def __init__(self, tzres_loc='tzres.dll'):
|
||||
# Load the user32 DLL so we can load strings from tzres
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
|
||||
# Specify the LoadStringW function
|
||||
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||
wintypes.UINT,
|
||||
wintypes.LPWSTR,
|
||||
ctypes.c_int)
|
||||
|
||||
self.LoadStringW = user32.LoadStringW
|
||||
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||
self.tzres_loc = tzres_loc
|
||||
|
||||
def load_name(self, offset):
|
||||
"""
|
||||
Load a timezone name from a DLL offset (integer).
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.load_name(112))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param offset:
|
||||
A positive integer value referring to a string from the tzres dll.
|
||||
|
||||
.. note::
|
||||
|
||||
Offsets found in the registry are generally of the form
|
||||
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
||||
|
||||
"""
|
||||
resource = self.p_wchar()
|
||||
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||
return resource[:nchar]
|
||||
|
||||
def name_from_string(self, tzname_str):
|
||||
"""
|
||||
Parse strings as returned from the Windows registry into the time zone
|
||||
name as defined in the registry.
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||
'Dateline Daylight Time'
|
||||
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param tzname_str:
|
||||
A timezone name string as returned from a Windows registry key.
|
||||
|
||||
:return:
|
||||
Returns the localized timezone string from tzres.dll if the string
|
||||
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||
"""
|
||||
if not tzname_str.startswith('@'):
|
||||
return tzname_str
|
||||
|
||||
name_splt = tzname_str.split(',-')
|
||||
try:
|
||||
offset = int(name_splt[1])
|
||||
except:
|
||||
raise ValueError("Malformed timezone string.")
|
||||
|
||||
return self.load_name(offset)
|
||||
|
||||
|
||||
class tzwinbase(tzrangebase):
|
||||
"""tzinfo class based on win32's timezones available in the registry."""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||
|
||||
def __eq__(self, other):
|
||||
# Compare on all relevant dimensions, including name.
|
||||
if not isinstance(other, tzwinbase):
|
||||
return NotImplemented
|
||||
|
||||
return (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset and
|
||||
self._stddayofweek == other._stddayofweek and
|
||||
self._dstdayofweek == other._dstdayofweek and
|
||||
self._stdweeknumber == other._stdweeknumber and
|
||||
self._dstweeknumber == other._dstweeknumber and
|
||||
self._stdhour == other._stdhour and
|
||||
self._dsthour == other._dsthour and
|
||||
self._stdminute == other._stdminute and
|
||||
self._dstminute == other._dstminute and
|
||||
self._std_abbr == other._std_abbr and
|
||||
self._dst_abbr == other._dst_abbr)
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
"""Return a list of all time zones known to the system."""
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||
result = [winreg.EnumKey(tzkey, i)
|
||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||
return result
|
||||
|
||||
def display(self):
|
||||
"""
|
||||
Return the display name of the time zone.
|
||||
"""
|
||||
return self._display
|
||||
|
||||
def transitions(self, year):
|
||||
"""
|
||||
For a given year, get the DST on and off transition times, expressed
|
||||
always on the standard time side. For zones with no transitions, this
|
||||
function returns ``None``.
|
||||
|
||||
:param year:
|
||||
The year whose transitions you would like to query.
|
||||
|
||||
:return:
|
||||
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||
``None`` for fixed offset zones.
|
||||
"""
|
||||
|
||||
if not self.hasdst:
|
||||
return None
|
||||
|
||||
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||
self._dsthour, self._dstminute,
|
||||
self._dstweeknumber)
|
||||
|
||||
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||
self._stdhour, self._stdminute,
|
||||
self._stdweeknumber)
|
||||
|
||||
# Ambiguous dates default to the STD side
|
||||
dstoff -= self._dst_base_offset
|
||||
|
||||
return dston, dstoff
|
||||
|
||||
def _get_hasdst(self):
|
||||
return self._dstmonth != 0
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_base_offset_
|
||||
|
||||
|
||||
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
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
keydict = valuestodict(tzkey)
|
||||
|
||||
self._std_abbr = keydict["Std"]
|
||||
self._dst_abbr = keydict["Dlt"]
|
||||
|
||||
self._display = keydict["Display"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[4:9]
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[12:17]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwin(%s)" % repr(self._name)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self._name,))
|
||||
|
||||
|
||||
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:
|
||||
keydict = valuestodict(tzlocalkey)
|
||||
|
||||
self._std_abbr = keydict["StandardName"]
|
||||
self._dst_abbr = keydict["DaylightName"]
|
||||
|
||||
try:
|
||||
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||
sn=self._std_abbr)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
_keydict = valuestodict(tzkey)
|
||||
self._display = _keydict["Display"]
|
||||
except OSError:
|
||||
self._display = None
|
||||
|
||||
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# For reasons unclear, in this particular key, the day of week has been
|
||||
# moved to the END of the SYSTEMTIME structure.
|
||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||
|
||||
(self._stdmonth,
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[1:5]
|
||||
|
||||
self._stddayofweek = tup[7]
|
||||
|
||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[1:5]
|
||||
|
||||
self._dstdayofweek = tup[7]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwinlocal()"
|
||||
|
||||
def __str__(self):
|
||||
# str will return the standard name, not the daylight name.
|
||||
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, ())
|
||||
|
||||
|
||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||
first = datetime.datetime(year, month, 1, hour, minute)
|
||||
|
||||
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||
# Because 7 % 7 = 0
|
||||
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||
if (wd.month != month):
|
||||
wd -= ONEWEEK
|
||||
|
||||
return wd
|
||||
|
||||
|
||||
def valuestodict(key):
|
||||
"""Convert a registry key's values to a dictionary."""
|
||||
dout = {}
|
||||
size = winreg.QueryInfoKey(key)[1]
|
||||
tz_res = None
|
||||
|
||||
for i in range(size):
|
||||
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||
# that to a proper signed integer
|
||||
if value & (1 << 31):
|
||||
value = value - (1 << 32)
|
||||
elif dtype == winreg.REG_SZ:
|
||||
# If it's a reference to the tzres DLL, load the actual string
|
||||
if value.startswith('@tzres'):
|
||||
tz_res = tz_res or tzres()
|
||||
value = tz_res.name_from_string(value)
|
||||
|
||||
value = value.rstrip('\x00') # Remove trailing nulls
|
||||
|
||||
dout[key_name] = value
|
||||
|
||||
return dout
|
|
@ -1,184 +1,2 @@
|
|||
# This code was originally contributed by Jeffrey Harris.
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
from six.moves import winreg
|
||||
|
||||
__all__ = ["tzwin", "tzwinlocal"]
|
||||
|
||||
ONEWEEK = datetime.timedelta(7)
|
||||
|
||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||
|
||||
|
||||
def _settzkeyname():
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
try:
|
||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||
TZKEYNAME = TZKEYNAMENT
|
||||
except WindowsError:
|
||||
TZKEYNAME = TZKEYNAME9X
|
||||
handle.Close()
|
||||
return TZKEYNAME
|
||||
|
||||
TZKEYNAME = _settzkeyname()
|
||||
|
||||
|
||||
class tzwinbase(datetime.tzinfo):
|
||||
"""tzinfo class based on win32's timezones available in the registry."""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return datetime.timedelta(minutes=self._dstoffset)
|
||||
else:
|
||||
return datetime.timedelta(minutes=self._stdoffset)
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
minutes = self._dstoffset - self._stdoffset
|
||||
return datetime.timedelta(minutes=minutes)
|
||||
else:
|
||||
return datetime.timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dstname
|
||||
else:
|
||||
return self._stdname
|
||||
|
||||
def list():
|
||||
"""Return a list of all time zones known to the system."""
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
tzkey = winreg.OpenKey(handle, TZKEYNAME)
|
||||
result = [winreg.EnumKey(tzkey, i)
|
||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||
tzkey.Close()
|
||||
handle.Close()
|
||||
return result
|
||||
list = staticmethod(list)
|
||||
|
||||
def display(self):
|
||||
return self._display
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self._dstmonth:
|
||||
# dstmonth == 0 signals the zone has no daylight saving time
|
||||
return False
|
||||
dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek,
|
||||
self._dsthour, self._dstminute,
|
||||
self._dstweeknumber)
|
||||
dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek,
|
||||
self._stdhour, self._stdminute,
|
||||
self._stdweeknumber)
|
||||
if dston < dstoff:
|
||||
return dston <= dt.replace(tzinfo=None) < dstoff
|
||||
else:
|
||||
return not dstoff <= dt.replace(tzinfo=None) < dston
|
||||
|
||||
|
||||
class tzwin(tzwinbase):
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
# multiple contexts only possible in 2.7 and 3.1, we still support 2.6
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle,
|
||||
"%s\%s" % (TZKEYNAME, name)) as tzkey:
|
||||
keydict = valuestodict(tzkey)
|
||||
|
||||
self._stdname = keydict["Std"].encode("iso-8859-1")
|
||||
self._dstname = keydict["Dlt"].encode("iso-8859-1")
|
||||
|
||||
self._display = keydict["Display"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||
self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||
self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1
|
||||
|
||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[4:9]
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[12:17]
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwin(%s)" % repr(self._name)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self._name,))
|
||||
|
||||
|
||||
class tzwinlocal(tzwinbase):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
|
||||
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||
keydict = valuestodict(tzlocalkey)
|
||||
|
||||
self._stdname = keydict["StandardName"].encode("iso-8859-1")
|
||||
self._dstname = keydict["DaylightName"].encode("iso-8859-1")
|
||||
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
handle, "%s\%s" % (TZKEYNAME, self._stdname)) as tzkey:
|
||||
_keydict = valuestodict(tzkey)
|
||||
self._display = _keydict["Display"]
|
||||
except OSError:
|
||||
self._display = None
|
||||
|
||||
self._stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||
self._dstoffset = self._stdoffset-keydict["DaylightBias"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[1:6]
|
||||
|
||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[1:6]
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, ())
|
||||
|
||||
|
||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||
"""dayofweek == 0 means Sunday, whichweek 5 means last instance"""
|
||||
first = datetime.datetime(year, month, 1, hour, minute)
|
||||
weekdayone = first.replace(day=((dayofweek-first.isoweekday()) % 7+1))
|
||||
for n in range(whichweek):
|
||||
dt = weekdayone+(whichweek-n)*ONEWEEK
|
||||
if dt.month == month:
|
||||
return dt
|
||||
|
||||
|
||||
def valuestodict(key):
|
||||
"""Convert a registry key's values to a dictionary."""
|
||||
dict = {}
|
||||
size = winreg.QueryInfoKey(key)[1]
|
||||
for i in range(size):
|
||||
data = winreg.EnumValue(key, i)
|
||||
dict[data[0]] = data[1]
|
||||
return dict
|
||||
# tzwin has moved to dateutil.tz.win
|
||||
from .tz.win import *
|
||||
|
|
71
lib/dateutil/utils.py
Normal file
71
lib/dateutil/utils.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# -*- 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
|
||||
|
||||
|
||||
def today(tzinfo=None):
|
||||
"""
|
||||
Returns a :py:class:`datetime` representing the current day at midnight
|
||||
|
||||
:param tzinfo:
|
||||
The time zone to attach (also used to determine the current day).
|
||||
|
||||
:return:
|
||||
A :py:class:`datetime.datetime` object representing the current day
|
||||
at midnight.
|
||||
"""
|
||||
|
||||
dt = datetime.now(tzinfo)
|
||||
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||
|
||||
|
||||
def default_tzinfo(dt, tzinfo):
|
||||
"""
|
||||
Sets the ``tzinfo`` parameter on naive datetimes only
|
||||
|
||||
This is useful for example when you are provided a datetime that may have
|
||||
either an implicit or explicit time zone, such as when parsing a time zone
|
||||
string.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from dateutil.tz import tzoffset
|
||||
>>> from dateutil.parser import parse
|
||||
>>> from dateutil.utils import default_tzinfo
|
||||
>>> dflt_tz = tzoffset("EST", -18000)
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||
2014-01-01 12:30:00+00:00
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||
2014-01-01 12:30:00-05:00
|
||||
|
||||
:param dt:
|
||||
The datetime on which to replace the time zone
|
||||
|
||||
:param tzinfo:
|
||||
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||
``dt`` if (and only if) it is naive.
|
||||
|
||||
:return:
|
||||
Returns an aware :py:class:`datetime.datetime`.
|
||||
"""
|
||||
if dt.tzinfo is not None:
|
||||
return dt
|
||||
else:
|
||||
return dt.replace(tzinfo=tzinfo)
|
||||
|
||||
|
||||
def within_delta(dt1, dt2, delta):
|
||||
"""
|
||||
Useful for comparing two datetimes that may have a negligible difference
|
||||
to be considered equal.
|
||||
"""
|
||||
delta = abs(delta)
|
||||
difference = dt1 - dt2
|
||||
return -delta <= difference <= delta
|
167
lib/dateutil/zoneinfo/__init__.py
Normal file
167
lib/dateutil/zoneinfo/__init__.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
import json
|
||||
|
||||
from tarfile import TarFile
|
||||
from pkgutil import get_data
|
||||
from io import BytesIO
|
||||
|
||||
from dateutil.tz import tzfile as _tzfile
|
||||
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||
|
||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||
METADATA_FN = 'METADATA'
|
||||
|
||||
|
||||
class tzfile(_tzfile):
|
||||
def __reduce__(self):
|
||||
return (gettz, (self._filename,))
|
||||
|
||||
|
||||
def getzoneinfofile_stream():
|
||||
try:
|
||||
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||
except IOError as e: # TODO switch to FileNotFoundError?
|
||||
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||
return None
|
||||
|
||||
|
||||
class ZoneInfoFile(object):
|
||||
def __init__(self, zonefile_stream=None):
|
||||
if zonefile_stream is not None:
|
||||
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||
for zf in tf.getmembers()
|
||||
if zf.isfile() and zf.name != METADATA_FN}
|
||||
# deal with links: They'll point to their parent object. Less
|
||||
# waste of memory
|
||||
links = {zl.name: self.zones[zl.linkname]
|
||||
for zl in tf.getmembers() if
|
||||
zl.islnk() or zl.issym()}
|
||||
self.zones.update(links)
|
||||
try:
|
||||
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||
metadata_str = metadata_json.read().decode('UTF-8')
|
||||
self.metadata = json.loads(metadata_str)
|
||||
except KeyError:
|
||||
# no metadata in tar file
|
||||
self.metadata = None
|
||||
else:
|
||||
self.zones = {}
|
||||
self.metadata = None
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""
|
||||
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||
for retrieving zones from the zone dictionary.
|
||||
|
||||
:param name:
|
||||
The name of the zone to retrieve. (Generally IANA zone names)
|
||||
|
||||
:param default:
|
||||
The value to return in the event of a missing key.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
|
||||
"""
|
||||
return self.zones.get(name, default)
|
||||
|
||||
|
||||
# The current API has gettz as a module function, although in fact it taps into
|
||||
# a stateful class. So as a workaround for now, without changing the API, we
|
||||
# will create a new "global" class instance the first time a user requests a
|
||||
# timezone. Ugly, but adheres to the api.
|
||||
#
|
||||
# TODO: Remove after deprecation period.
|
||||
_CLASS_ZONE_INSTANCE = []
|
||||
|
||||
|
||||
def get_zonefile_instance(new_instance=False):
|
||||
"""
|
||||
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||
instance using the data provided by the ``dateutil`` package. By default, it
|
||||
caches a single instance of the ZoneInfoFile object and returns that.
|
||||
|
||||
:param new_instance:
|
||||
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||
used as the cached instance for the next call. Otherwise, new instances
|
||||
are created only as necessary.
|
||||
|
||||
:return:
|
||||
Returns a :class:`ZoneInfoFile` object.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
if new_instance:
|
||||
zif = None
|
||||
else:
|
||||
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||
|
||||
if zif is None:
|
||||
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||
|
||||
get_zonefile_instance._cached_instance = zif
|
||||
|
||||
return zif
|
||||
|
||||
|
||||
def gettz(name):
|
||||
"""
|
||||
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||
with dateutil.
|
||||
|
||||
:param name:
|
||||
An IANA-style time zone name, as found in the zoneinfo file.
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||
|
||||
.. warning::
|
||||
It is generally inadvisable to use this function, and it is only
|
||||
provided for API compatibility with earlier versions. This is *not*
|
||||
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||
date compared to the system zoneinfo).
|
||||
|
||||
.. deprecated:: 2.6
|
||||
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||
|
||||
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||
dateutil-provided zoneinfo.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||
|
||||
|
||||
def gettz_db_metadata():
|
||||
""" Get the zonefile metadata
|
||||
|
||||
See `zonefile_metadata`_
|
||||
|
||||
:returns:
|
||||
A dictionary with the database metadata
|
||||
|
||||
.. deprecated:: 2.6
|
||||
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||
"versions, to use the dateutil-provided zoneinfo files, "
|
||||
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].metadata
|
BIN
lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
Normal file
BIN
lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
Normal file
Binary file not shown.
75
lib/dateutil/zoneinfo/rebuild.py
Normal file
75
lib/dateutil/zoneinfo/rebuild.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from subprocess import check_call, check_output
|
||||
from tarfile import TarFile
|
||||
|
||||
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||
|
||||
|
||||
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||
|
||||
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||
|
||||
"""
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||
moduledir = os.path.dirname(__file__)
|
||||
try:
|
||||
with TarFile.open(filename) as tf:
|
||||
for name in zonegroups:
|
||||
tf.extract(name, tmpdir)
|
||||
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||
|
||||
_run_zic(zonedir, filepaths)
|
||||
|
||||
# write metadata file
|
||||
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||
target = os.path.join(moduledir, ZONEFILENAME)
|
||||
with TarFile.open(target, "w:%s" % format) as tf:
|
||||
for entry in os.listdir(zonedir):
|
||||
entrypath = os.path.join(zonedir, entry)
|
||||
tf.add(entrypath, entry)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def _run_zic(zonedir, filepaths):
|
||||
"""Calls the ``zic`` compiler in a compatible way to get a "fat" binary.
|
||||
|
||||
Recent versions of ``zic`` default to ``-b slim``, while older versions
|
||||
don't even have the ``-b`` option (but default to "fat" binaries). The
|
||||
current version of dateutil does not support Version 2+ TZif files, which
|
||||
causes problems when used in conjunction with "slim" binaries, so this
|
||||
function is used to ensure that we always get a "fat" binary.
|
||||
"""
|
||||
|
||||
try:
|
||||
help_text = check_output(["zic", "--help"])
|
||||
except OSError as e:
|
||||
_print_on_nosuchfile(e)
|
||||
raise
|
||||
|
||||
if b"-b " in help_text:
|
||||
bloat_args = ["-b", "fat"]
|
||||
else:
|
||||
bloat_args = []
|
||||
|
||||
check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths)
|
||||
|
||||
|
||||
def _print_on_nosuchfile(e):
|
||||
"""Print helpful troubleshooting message
|
||||
|
||||
e is an exception raised by subprocess.check_call()
|
||||
|
||||
"""
|
||||
if e.errno == 2:
|
||||
logging.error(
|
||||
"Could not find zic. Perhaps you need to install "
|
||||
"libc-bin or some other package that provides it, "
|
||||
"or it's not in your PATH?")
|
Loading…
Add table
Add a link
Reference in a new issue