-
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = 'unknown'
|
||||
|
||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||
'utils', 'zoneinfo']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a generic Easter computing method for any given year, using
|
||||
Western, Orthodox or Julian algorithms.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
|
||||
|
||||
def easter(year, method=EASTER_WESTERN):
|
||||
"""
|
||||
This method was ported from the work done by GM Arts,
|
||||
on top of the algorithm by Claus Tondering, which was
|
||||
based in part on the algorithm of Ouding (1940), as
|
||||
quoted in "Explanatory Supplement to the Astronomical
|
||||
Almanac", P. Kenneth Seidelmann, editor.
|
||||
|
||||
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
|
||||
|
||||
These methods are represented by the constants:
|
||||
|
||||
* ``EASTER_JULIAN = 1``
|
||||
* ``EASTER_ORTHODOX = 2``
|
||||
* ``EASTER_WESTERN = 3``
|
||||
|
||||
The default method is method 3.
|
||||
|
||||
More about the algorithm may be found at:
|
||||
|
||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||
|
||||
and
|
||||
|
||||
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||
|
||||
"""
|
||||
|
||||
if not (1 <= method <= 3):
|
||||
raise ValueError("invalid method")
|
||||
|
||||
# g - Golden year - 1
|
||||
# c - Century
|
||||
# h - (23 - Epact) mod 30
|
||||
# i - Number of days from March 21 to Paschal Full Moon
|
||||
# j - Weekday for PFM (0=Sunday, etc)
|
||||
# p - Number of days from March 21 to Sunday on or before PFM
|
||||
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||
# e - Extra days to add for method 2 (converting Julian
|
||||
# date to Gregorian date)
|
||||
|
||||
y = year
|
||||
g = y % 19
|
||||
e = 0
|
||||
if method < 3:
|
||||
# Old method
|
||||
i = (19*g + 15) % 30
|
||||
j = (y + y//4 + i) % 7
|
||||
if method == 2:
|
||||
# Extra dates to convert Julian to Gregorian date
|
||||
e = 10
|
||||
if y > 1600:
|
||||
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||
else:
|
||||
# New method
|
||||
c = y//100
|
||||
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||
|
||||
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||
p = i - j + e
|
||||
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||
m = 3 + (p + 26)//30
|
||||
return datetime.date(int(y), int(m), int(d))
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,599 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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 relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is designed to be applied to an existing datetime and
|
||||
can replace specific components of that datetime, or represents an interval
|
||||
of time.
|
||||
|
||||
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||
in his
|
||||
`mx.DateTime <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.
|
||||
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc) available in the
|
||||
relativedelta module. These instances may receive a parameter N,
|
||||
specifying the Nth weekday, which could be positive or negative
|
||||
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
||||
+1. You can also use an integer, where 0=MO. This argument is always
|
||||
relative e.g. if the calculated date is already Monday, using MO(1)
|
||||
or MO(-1) won't change the day. To effectively make it absolute, use
|
||||
it in combination with the day argument (e.g. day=1, MO(1) for first
|
||||
Monday of the month).
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
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).
|
||||
|
||||
The order of attributes considered when this relativedelta is
|
||||
added to a datetime is:
|
||||
|
||||
1. Year
|
||||
2. Month
|
||||
3. Day
|
||||
4. Hours
|
||||
5. Minutes
|
||||
6. Seconds
|
||||
7. Microseconds
|
||||
|
||||
Finally, weekday is applied, using the rule described above.
|
||||
|
||||
For example
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from dateutil.relativedelta import relativedelta, MO
|
||||
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||
>>> 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.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dt1=None, dt2=None,
|
||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||
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) !=
|
||||
isinstance(dt2, datetime.datetime)):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
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
|
||||
self.leapdays = 0
|
||||
self.hours = 0
|
||||
self.minutes = 0
|
||||
self.seconds = 0
|
||||
self.microseconds = 0
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.weekday = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
# 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:
|
||||
compare = operator.gt
|
||||
increment = 1
|
||||
else:
|
||||
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.microseconds = delta.microseconds
|
||||
else:
|
||||
# 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
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
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:
|
||||
self.weekday = weekday
|
||||
|
||||
yday = 0
|
||||
if nlyearday:
|
||||
yday = nlyearday
|
||||
elif yearday:
|
||||
yday = yearday
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||
243, 273, 304, 334, 366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
if idx == 0:
|
||||
self.day = yday
|
||||
else:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
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 = _sign(self.seconds)
|
||||
div, mod = divmod(self.seconds * s, 60)
|
||||
self.seconds = mod * s
|
||||
self.minutes += div * s
|
||||
if abs(self.minutes) > 59:
|
||||
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 = _sign(self.hours)
|
||||
div, mod = divmod(self.hours * s, 24)
|
||||
self.hours = mod * s
|
||||
self.days += div * s
|
||||
if abs(self.months) > 11:
|
||||
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):
|
||||
self._has_time = 1
|
||||
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 = _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 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 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):
|
||||
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
|
||||
month = self.month or other.month
|
||||
if self.months:
|
||||
assert 1 <= abs(self.months) <= 12
|
||||
month += self.months
|
||||
if month > 12:
|
||||
year += 1
|
||||
month -= 12
|
||||
elif month < 1:
|
||||
year -= 1
|
||||
month += 12
|
||||
day = min(calendar.monthrange(year, month)[1],
|
||||
self.day or other.day)
|
||||
repl = {"year": year, "month": month, "day": day}
|
||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
repl[attr] = value
|
||||
days = self.days
|
||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||
days += self.leapdays
|
||||
ret = (other.replace(**repl)
|
||||
+ datetime.timedelta(days=days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds,
|
||||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth) - 1) * 7
|
||||
if nth > 0:
|
||||
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||
else:
|
||||
jumpdays += (ret.weekday() - weekday) % 7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
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 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 self.__class__(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
minutes=-self.minutes,
|
||||
seconds=-self.seconds,
|
||||
microseconds=-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 __bool__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
not self.hours and
|
||||
not self.minutes and
|
||||
not self.seconds and
|
||||
not self.microseconds and
|
||||
not self.leapdays and
|
||||
self.year is None and
|
||||
self.month is None and
|
||||
self.day is None and
|
||||
self.weekday is None and
|
||||
self.hour is None and
|
||||
self.minute is None and
|
||||
self.second is None and
|
||||
self.microsecond is None)
|
||||
# Compatibility with Python 2.x
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __mul__(self, other):
|
||||
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,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return NotImplemented
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
if self.weekday.weekday != other.weekday.weekday:
|
||||
return False
|
||||
n1, n2 = self.weekday.n, other.weekday.n
|
||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||
return False
|
||||
return (self.years == other.years and
|
||||
self.months == other.months and
|
||||
self.days == other.days and
|
||||
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
|
||||
self.day == other.day and
|
||||
self.hour == other.hour and
|
||||
self.minute == other.minute and
|
||||
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):
|
||||
try:
|
||||
reciprocal = 1 / float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__mul__(reciprocal)
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if 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("{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
|
||||
1737
Kodi/Lenovo/addons/script.module.dateutil/lib/dateutil/rrule.py
Normal file
1737
Kodi/Lenovo/addons/script.module.dateutil/lib/dateutil/rrule.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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')"
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
@@ -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."""
|
||||
@@ -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__
|
||||
@@ -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
Kodi/Lenovo/addons/script.module.dateutil/lib/dateutil/tz/tz.py
Normal file
1849
Kodi/Lenovo/addons/script.module.dateutil/lib/dateutil/tz/tz.py
Normal file
File diff suppressed because it is too large
Load Diff
370
Kodi/Lenovo/addons/script.module.dateutil/lib/dateutil/tz/win.py
Normal file
370
Kodi/Lenovo/addons/script.module.dateutil/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
|
||||
@@ -0,0 +1,2 @@
|
||||
# tzwin has moved to dateutil.tz.win
|
||||
from .tz.win import *
|
||||
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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?")
|
||||
Reference in New Issue
Block a user