From dc66dc11ba5b0a938b746dfcd60aa5c02d2dbd2d Mon Sep 17 00:00:00 2001 From: ueffel Date: Mon, 17 Sep 2018 20:03:20 +0200 Subject: [PATCH] initial commit --- .gitignore | 2 + README.md | 25 + build_package.cmd | 54 + clock.ico | Bin 0 -> 55658 bytes lib/dateutil/__init__.py | 8 + lib/dateutil/_common.py | 43 + lib/dateutil/_version.py | 4 + lib/dateutil/easter.py | 89 + lib/dateutil/parser/__init__.py | 60 + lib/dateutil/parser/_parser.py | 1578 ++++++ lib/dateutil/parser/isoparser.py | 406 ++ lib/dateutil/relativedelta.py | 590 ++ lib/dateutil/rrule.py | 1672 ++++++ lib/dateutil/test/__init__.py | 0 lib/dateutil/test/_common.py | 275 + .../test/property/test_isoparse_prop.py | 27 + .../test/property/test_parser_prop.py | 22 + lib/dateutil/test/test_easter.py | 95 + lib/dateutil/test/test_import_star.py | 33 + lib/dateutil/test/test_imports.py | 166 + lib/dateutil/test/test_internals.py | 95 + lib/dateutil/test/test_isoparser.py | 482 ++ lib/dateutil/test/test_parser.py | 1114 ++++ lib/dateutil/test/test_relativedelta.py | 678 +++ lib/dateutil/test/test_rrule.py | 4842 +++++++++++++++++ lib/dateutil/test/test_tz.py | 2603 +++++++++ lib/dateutil/test/test_utils.py | 53 + lib/dateutil/tz/__init__.py | 17 + lib/dateutil/tz/_common.py | 415 ++ lib/dateutil/tz/_factories.py | 49 + lib/dateutil/tz/tz.py | 1785 ++++++ lib/dateutil/tz/win.py | 331 ++ lib/dateutil/tzwin.py | 2 + lib/dateutil/utils.py | 71 + lib/dateutil/zoneinfo/__init__.py | 167 + .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 139130 bytes lib/dateutil/zoneinfo/rebuild.py | 53 + lib/six.py | 891 +++ time.ini | 24 + time.py | 211 + 40 files changed, 19032 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build_package.cmd create mode 100644 clock.ico create mode 100644 lib/dateutil/__init__.py create mode 100644 lib/dateutil/_common.py create mode 100644 lib/dateutil/_version.py create mode 100644 lib/dateutil/easter.py create mode 100644 lib/dateutil/parser/__init__.py create mode 100644 lib/dateutil/parser/_parser.py create mode 100644 lib/dateutil/parser/isoparser.py create mode 100644 lib/dateutil/relativedelta.py create mode 100644 lib/dateutil/rrule.py create mode 100644 lib/dateutil/test/__init__.py create mode 100644 lib/dateutil/test/_common.py create mode 100644 lib/dateutil/test/property/test_isoparse_prop.py create mode 100644 lib/dateutil/test/property/test_parser_prop.py create mode 100644 lib/dateutil/test/test_easter.py create mode 100644 lib/dateutil/test/test_import_star.py create mode 100644 lib/dateutil/test/test_imports.py create mode 100644 lib/dateutil/test/test_internals.py create mode 100644 lib/dateutil/test/test_isoparser.py create mode 100644 lib/dateutil/test/test_parser.py create mode 100644 lib/dateutil/test/test_relativedelta.py create mode 100644 lib/dateutil/test/test_rrule.py create mode 100644 lib/dateutil/test/test_tz.py create mode 100644 lib/dateutil/test/test_utils.py create mode 100644 lib/dateutil/tz/__init__.py create mode 100644 lib/dateutil/tz/_common.py create mode 100644 lib/dateutil/tz/_factories.py create mode 100644 lib/dateutil/tz/tz.py create mode 100644 lib/dateutil/tz/win.py create mode 100644 lib/dateutil/tzwin.py create mode 100644 lib/dateutil/utils.py create mode 100644 lib/dateutil/zoneinfo/__init__.py create mode 100644 lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 lib/dateutil/zoneinfo/rebuild.py create mode 100644 lib/six.py create mode 100644 time.ini create mode 100644 time.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd28209 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.bak diff --git a/README.md b/README.md new file mode 100644 index 0000000..dad529d --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Keypirinha Time +======================= + +This is a package that extends the fast keystroke launcher keypirinha (http://keypirinha.com/) with +a command to convert and format datetime strings. + +## Usage + +Use the `Time:` item. (configurable) +If no other input is entered the displayed datetime is the current time. +Any input is interpreted as date string (or at least tried to) + +Executing any of the suggestions copies the selected item to the clipboard. + +## Installation + +### With [PackageControl](https://github.com/ueffel/Keypirinha-PackageControl) + +Install Package "Keypirinha-Time" + +### Manually + +* Download the `Time.keypirinha-package` from the [Release Notes](https://github.com/ueffel/Keypirinha-Time/releases/latest). +* Copy the file into `%APPDATA%\Keypirinha\InstalledPackages` (installed mode) or + `\portable\Profile\InstalledPackages` (portable mode) diff --git a/build_package.cmd b/build_package.cmd new file mode 100644 index 0000000..736e0e3 --- /dev/null +++ b/build_package.cmd @@ -0,0 +1,54 @@ +@echo off +set PACKAGE_NAME=Time + +set SEVENZIP= +where 7z > nul 2>&1 +if not errorlevel 1 ( + set SEVENZIP=7z + goto done_sevenzip +) + +where 7za > nul 2>&1 +if not errorlevel 1 ( + set SEVENZIP=7za + goto done_sevenzip +) + +if exist "c:\Program Files (x86)\7-Zip\7z.exe" ( + set "SEVENZIP=c:\Program Files (x86)\7-Zip\7z.exe" + goto done_sevenzip +) + +if exist "c:\Program Files (x86)\7-Zip\7za.exe" ( + set "SEVENZIP=c:\Program Files (x86)\7-Zip\7za.exe" + goto done_sevenzip +) + +if exist "c:\Program Files\7-Zip\7z.exe" ( + set "SEVENZIP=c:\Program Files\7-Zip\7z.exe" + goto done_sevenzip +) + +if exist "c:\Program Files\7-Zip\7za.exe" ( + set "SEVENZIP=c:\Program Files\7-Zip\7za.exe" + goto done_sevenzip +) + +if NOT DEFINED SEVENZIP ( + echo 7zip not found + exit /b 1 +) +:done_sevenzip + +:pack +if exist %PACKAGE_NAME%.keypirinha-package ( + del %PACKAGE_NAME%.keypirinha-package +) +echo Using "%SEVENZIP%" to pack +"%SEVENZIP%" a -mx9 ^ + -tzip "%PACKAGE_NAME%.keypirinha-package" ^ + -x!%~nx0 ^ + -xr!.git ^ + -x@.gitignore ^ + -x!.gitignore ^ + * diff --git a/clock.ico b/clock.ico new file mode 100644 index 0000000000000000000000000000000000000000..60ea2e5ac01e1caf35f7fed7e9a22dabfdc9ec80 GIT binary patch literal 55658 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�D3p+X}1NFLU;+`+cHdng0=lO3& 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%0A?y#^wb)oSFK3&rHq*sL-)kFmUQ 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 A00000 literal 0 HcmV?d00001 diff --git a/lib/dateutil/__init__.py b/lib/dateutil/__init__.py new file mode 100644 index 0000000..0defb82 --- /dev/null +++ b/lib/dateutil/__init__.py @@ -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'] diff --git a/lib/dateutil/_common.py b/lib/dateutil/_common.py new file mode 100644 index 0000000..4eb2659 --- /dev/null +++ b/lib/dateutil/_common.py @@ -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 diff --git a/lib/dateutil/_version.py b/lib/dateutil/_version.py new file mode 100644 index 0000000..713fe0d --- /dev/null +++ b/lib/dateutil/_version.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '2.7.3' diff --git a/lib/dateutil/easter.py b/lib/dateutil/easter.py new file mode 100644 index 0000000..53b7c78 --- /dev/null +++ b/lib/dateutil/easter.py @@ -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 `_ + + and + + `The Calendar FAQ: Easter `_ + + """ + + 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)) diff --git a/lib/dateutil/parser/__init__.py b/lib/dateutil/parser/__init__.py new file mode 100644 index 0000000..216762c --- /dev/null +++ b/lib/dateutil/parser/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + '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) diff --git a/lib/dateutil/parser/_parser.py b/lib/dateutil/parser/_parser.py new file mode 100644 index 0000000..9d2bb79 --- /dev/null +++ b/lib/dateutil/parser/_parser.py @@ -0,0 +1,1578 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +import six +from six import binary_type, integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if six.PY2: + # In Python 2, we can't duck type properly because unicode has + # a 'decode' function, and we'd be double-decoding + if isinstance(instream, (binary_type, bytearray)): + instream = instream.decode() + else: + if getattr(instream, 'decode', None) is not None: + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leapyear + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precendence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ValueError("Unknown string format:", timestr) + + if len(res) == 0: + raise ValueError("String does not contain a date:", timestr) + + ret = self._build_naive(res, default) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceeding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + all(x in string.ascii_uppercase for x in token)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *dont* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.tzutc()) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ValueError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo""" +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/parser/isoparser.py b/lib/dateutil/parser/isoparser.py new file mode 100644 index 0000000..cd27f93 --- /dev/null +++ b/lib/dateutil/parser/isoparser.py @@ -0,0 +1,406 @@ +# -*- 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.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. + + .. 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') + + 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: {}'.format(datestr)) + 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 + """ + return time(*self._parse_isotime(timestr)) + + @_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 + _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') + _DATE_SEP = b'-' + _TIME_SEP = b':' + _MICRO_SEP = b'.' + + 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(timestr) < 2: + raise ValueError('ISO time too short') + + has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Z': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + if (has_sep and pos < len_str and + timestr[pos:pos + 1] == self._TIME_SEP): + pos += 1 + + if comp == 3: + # Microsecond + if timestr[pos:pos + 1] != self._MICRO_SEP: + continue + + pos += 1 + us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], + 1)[0] + + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(us_str) + + 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') + components[0] = 0 + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z': + return tz.tzutc() + + 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.tzutc() + 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 diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py new file mode 100644 index 0000000..1e0d616 --- /dev/null +++ b/lib/dateutil/relativedelta.py @@ -0,0 +1,590 @@ +# -*- 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 based on the specification of the excellent + work done by M.-A. Lemburg in his + `mx.DateTime `_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + 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 aritmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc). These + instances may receive a parameter N, specifying the Nth + weekday, which could be positive or negative (like MO(+1) + or MO(-2). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. Notice that + if the calculated date is already Monday, for example, + using MO(1) or MO(-1) won't change the day. + + 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 + + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + datetime(2018, 4, 2, 14, 37, 0) + + 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 diff --git a/lib/dateutil/rrule.py b/lib/dateutil/rrule.py new file mode 100644 index 0000000..8e9c2af --- /dev/null +++ b/lib/dateutil/rrule.py @@ -0,0 +1,1672 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import itertools +import datetime +import calendar +import re +import sys + +try: + from math import gcd +except ImportError: + from fractions import gcd + +from six import advance_iterator, integer_types +from six.moves import _thread, range +import heapq + +from ._common import weekday as weekdaybase +from .tz import tzutc, tzlocal + +# For warning about deprecation of until and count +from warnings import warn + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penality. + def count(self): + """ Returns the number of recurrences in this set. It will have go + trough the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + How many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the ``until`` keyword together + with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + :param until: + If given, this must be a datetime instance, that will specify the + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the ``until`` keyword together + with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + +class _rrulestr(object): + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported EXDATE parm: "+parm) + exdatevals.append(value) + elif name == "DTSTART": + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but + # may be found only once. + value_found = False + TZID = None + valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"} + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = TZID_NAMES[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, ' + + 'mapping, or None, ' + + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + if parm not in valid_values: + raise ValueError("unsupported DTSTART parm: "+parm) + else: + if value_found: + msg = ("Duplicate value parameter found in " + + "DTSTART: " + parm) + raise ValueError(msg) + value_found = True + if not parser: + from dateutil import parser + dtstart = parser.parse(value, ignoretz=ignoretz, + tzinfos=tzinfos) + if TZID is not None: + if dtstart.tzinfo is None: + dtstart = dtstart.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART specifies multiple timezones') + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + for datestr in value.split(','): + rset.exdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/test/__init__.py b/lib/dateutil/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/dateutil/test/_common.py b/lib/dateutil/test/_common.py new file mode 100644 index 0000000..264dfbd --- /dev/null +++ b/lib/dateutil/test/_common.py @@ -0,0 +1,275 @@ +from __future__ import unicode_literals +import os +import time +import subprocess +import warnings +import tempfile +import pickle + + +class WarningTestMixin(object): + # Based on https://stackoverflow.com/a/12935176/467366 + class _AssertWarnsContext(warnings.catch_warnings): + def __init__(self, expected_warnings, parent, **kwargs): + super(WarningTestMixin._AssertWarnsContext, self).__init__(**kwargs) + + self.parent = parent + try: + self.expected_warnings = list(expected_warnings) + except TypeError: + self.expected_warnings = [expected_warnings] + + self._warning_log = [] + + def __enter__(self, *args, **kwargs): + rv = super(WarningTestMixin._AssertWarnsContext, self).__enter__(*args, **kwargs) + + if self._showwarning is not self._module.showwarning: + super_showwarning = self._module.showwarning + else: + super_showwarning = None + + def showwarning(*args, **kwargs): + if super_showwarning is not None: + super_showwarning(*args, **kwargs) + + self._warning_log.append(warnings.WarningMessage(*args, **kwargs)) + + self._module.showwarning = showwarning + return rv + + def __exit__(self, *args, **kwargs): + super(WarningTestMixin._AssertWarnsContext, self).__exit__(self, *args, **kwargs) + + self.parent.assertTrue(any(issubclass(item.category, warning) + for warning in self.expected_warnings + for item in self._warning_log)) + + def assertWarns(self, warning, callable=None, *args, **kwargs): + warnings.simplefilter('always') + context = self.__class__._AssertWarnsContext(warning, self) + if callable is None: + return context + else: + with context: + callable(*args, **kwargs) + + +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(): + raise ValueError(self.tz_change_disallowed_message()) + + 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() diff --git a/lib/dateutil/test/property/test_isoparse_prop.py b/lib/dateutil/test/property/test_isoparse_prop.py new file mode 100644 index 0000000..c6a4b82 --- /dev/null +++ b/lib/dateutil/test/property/test_isoparse_prop.py @@ -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.tzutc()] + + [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 diff --git a/lib/dateutil/test/property/test_parser_prop.py b/lib/dateutil/test/property/test_parser_prop.py new file mode 100644 index 0000000..fdfd171 --- /dev/null +++ b/lib/dateutil/test/property/test_parser_prop.py @@ -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 diff --git a/lib/dateutil/test/test_easter.py b/lib/dateutil/test/test_easter.py new file mode 100644 index 0000000..eeb094e --- /dev/null +++ b/lib/dateutil/test/test_easter.py @@ -0,0 +1,95 @@ +from dateutil.easter import easter +from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN + +from datetime import date +import unittest + +# 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) +] + + +class EasterTest(unittest.TestCase): + def testEasterWestern(self): + for easter_date in western_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_WESTERN)) + + def testEasterOrthodox(self): + for easter_date in orthodox_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_ORTHODOX)) + + def testEasterJulian(self): + for easter_date in julian_easter_dates: + self.assertEqual(easter_date, + easter(easter_date.year, EASTER_JULIAN)) + + def testEasterBadMethod(self): + # Invalid methods raise ValueError + with self.assertRaises(ValueError): + easter(1975, 4) diff --git a/lib/dateutil/test/test_import_star.py b/lib/dateutil/test/test_import_star.py new file mode 100644 index 0000000..8e66f38 --- /dev/null +++ b/lib/dateutil/test/test_import_star.py @@ -0,0 +1,33 @@ +"""Test for the "import *" functionality. + +As imort * can be only done at module level, it has been added in a separate file +""" +import unittest + +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') + +class ImportStarTest(unittest.TestCase): + """ Test that `from dateutil import *` adds the modules in __all__ locally""" + + def testImportedModules(self): + import dateutil.easter + import dateutil.parser + import dateutil.relativedelta + import dateutil.rrule + import dateutil.tz + import dateutil.utils + import dateutil.zoneinfo + + self.assertEquals(dateutil.easter, new_locals.pop("easter")) + self.assertEquals(dateutil.parser, new_locals.pop("parser")) + self.assertEquals(dateutil.relativedelta, new_locals.pop("relativedelta")) + self.assertEquals(dateutil.rrule, new_locals.pop("rrule")) + self.assertEquals(dateutil.tz, new_locals.pop("tz")) + self.assertEquals(dateutil.utils, new_locals.pop("utils")) + self.assertEquals(dateutil.zoneinfo, new_locals.pop("zoneinfo")) + + self.assertFalse(new_locals) diff --git a/lib/dateutil/test/test_imports.py b/lib/dateutil/test/test_imports.py new file mode 100644 index 0000000..2a19b62 --- /dev/null +++ b/lib/dateutil/test/test_imports.py @@ -0,0 +1,166 @@ +import sys +import unittest + +class ImportVersionTest(unittest.TestCase): + """ Test that dateutil.__version__ can be imported""" + + def testImportVersionStr(self): + from dateutil import __version__ + + def testImportRoot(self): + import dateutil + + self.assertTrue(hasattr(dateutil, '__version__')) + + +class ImportEasterTest(unittest.TestCase): + """ Test that dateutil.easter-related imports work properly """ + + def testEasterDirect(self): + import dateutil.easter + + def testEasterFrom(self): + from dateutil import easter + + def testEasterStar(self): + from dateutil.easter import easter + + +class ImportParserTest(unittest.TestCase): + """ Test that dateutil.parser-related imports work properly """ + def testParserDirect(self): + import dateutil.parser + + def testParserFrom(self): + from dateutil import parser + + def testParserAll(self): + # 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): + self.assertIsNot(var, None) + + +class ImportRelativeDeltaTest(unittest.TestCase): + """ Test that dateutil.relativedelta-related imports work properly """ + def testRelativeDeltaDirect(self): + import dateutil.relativedelta + + def testRelativeDeltaFrom(self): + from dateutil import relativedelta + + def testRelativeDeltaAll(self): + 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): + self.assertIsNot(var, None) + + # In the public interface but not in all + from dateutil.relativedelta import weekday + self.assertIsNot(weekday, None) + + +class ImportRRuleTest(unittest.TestCase): + """ Test that dateutil.rrule related imports work properly """ + def testRRuleDirect(self): + import dateutil.rrule + + def testRRuleFrom(self): + from dateutil import rrule + + def testRRuleAll(self): + 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: + self.assertIsNot(var, None) + + # In the public interface but not in all + from dateutil.rrule import weekday + self.assertIsNot(weekday, None) + + +class ImportTZTest(unittest.TestCase): + """ Test that dateutil.tz related imports work properly """ + def testTzDirect(self): + import dateutil.tz + + def testTzFrom(self): + from dateutil import tz + + def testTzAll(self): + 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: + self.assertIsNot(lvars[var], None) + +@unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows") +class ImportTZWinTest(unittest.TestCase): + """ Test that dateutil.tzwin related imports work properly """ + def testTzwinDirect(self): + import dateutil.tzwin + + def testTzwinFrom(self): + from dateutil import tzwin + + def testTzwinStar(self): + from dateutil.tzwin import tzwin + from dateutil.tzwin import tzwinlocal + + tzwin_all = [tzwin, tzwinlocal] + + for var in tzwin_all: + self.assertIsNot(var, None) + + +class ImportZoneInfoTest(unittest.TestCase): + def testZoneinfoDirect(self): + import dateutil.zoneinfo + + def testZoneinfoFrom(self): + from dateutil import zoneinfo + + def testZoneinfoStar(self): + 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: + self.assertIsNot(var, None) diff --git a/lib/dateutil/test/test_internals.py b/lib/dateutil/test/test_internals.py new file mode 100644 index 0000000..a64c514 --- /dev/null +++ b/lib/dateutil/test/test_internals.py @@ -0,0 +1,95 @@ +# -*- 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 unittest +import sys + +import pytest + +from dateutil.parser._parser import _ymd +from dateutil import tz + +IS_PY32 = sys.version_info[0:2] == (3, 2) + + +class TestYMD(unittest.TestCase): + + # @pytest.mark.smoke + def test_could_be_day(self): + 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 leapyear + 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 diff --git a/lib/dateutil/test/test_isoparser.py b/lib/dateutil/test/test_isoparser.py new file mode 100644 index 0000000..28c1bf7 --- /dev/null +++ b/lib/dateutil/test/test_isoparser.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta, date, time +import itertools as it + +from dateutil.tz import tz +from dateutil.parser import isoparser, isoparse + +import pytest +import six + +UTC = tz.tzutc() + +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((tz.tzutc(), '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'): + raise ValueError('Time format has no microseconds!') + + if microsecond_precision != 6: + dtstr = dtstr[:-(6 - microsecond_precision)] + elif microsecond_precision > 6: + 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 + '.%f' for x in HMS_FMTS)) +@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) + +@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-11T24', + '2014-04-11T00:00', + '2014-04-11T24:00', + '2014-04-11T00:00:00', + '2014-04-11T24:00:00', + '2014-04-11T00:00:00.000', + '2014-04-11T24:00:00.000', + '2014-04-11T00:00:00.000000', + '2014-04-11T24: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, + tz.tzutc())), + (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 + ('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', [ + ('C', 'T'), + ('T', 'C') +]) +def test_iso_raises_sep(sep_act, valid_sep): + isostr = '2012-04-25' + sep_act + '01:25:00' + + +@pytest.mark.xfail() +@pytest.mark.parametrize('isostr,exception', [ + ('20120425T01:2000', ValueError), # Inconsistent time separators +]) +def test_iso_raises_failing(isostr, exception): + # These are test cases where the current implementation is too lenient + # and need to be fixed + with pytest.raises(exception): + 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(six.PY3, 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 == tz.tzutc() + 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 six.PY3: + # 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, six.binary_type) 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 +]) +def test_isodate_raises(isostr, exception): + with pytest.raises(exception): + isoparser().parse_isodate(isostr) + + +### +# 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(time_val, six.text_type) and as_bytes: + tstr = tstr.encode('ascii') + elif isinstance(time_val, six.binary_type) and not as_bytes: + tstr = tstr.decode('ascii') + + iparser = isoparser() + + assert iparser.parse_isotime(tstr) == time_val + +@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 + ('14:30:15.3684000309', ValueError), # Too much us precision + ('25', ValueError), # Invalid hours + ('25:15', ValueError), # Invalid hours + ('14:60', ValueError), # Invalid minutes + ('14:59:61', ValueError), # Invalid seconds + ('14:30:15.3446830500', 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 +]) +def test_isotime_raises(isostr, exception): + iparser = isoparser() + with pytest.raises(exception): + iparser.parse_isotime(isostr) + + +@pytest.mark.xfail() +@pytest.mark.parametrize('isostr,exception', [ + ('14:3015', ValueError), # Inconsistent separator use + ('201202', ValueError) # Invalid ISO format +]) +def test_isotime_raises_xfail(isostr, exception): + iparser = isoparser() + with pytest.raises(exception): + iparser.parse_isotime(isostr) diff --git a/lib/dateutil/test/test_parser.py b/lib/dateutil/test/test_parser.py new file mode 100644 index 0000000..f8c2072 --- /dev/null +++ b/lib/dateutil/test/test_parser.py @@ -0,0 +1,1114 @@ +# -*- 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 UnknownTimezoneWarning + +from ._common import TZEnvContext + +from six import assertRaisesRegex, PY3 +from six.moves import StringIO + +import pytest + +# Platform info +IS_WIN = sys.platform.startswith('win') + +try: + datetime.now().strftime('%-d') + PLATFORM_HAS_DASH_D = True +except ValueError: + PLATFORM_HAS_DASH_D = False + + +class TestFormat(unittest.TestCase): + + 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) + self.assertEqual(res, actual) + + +class ParserTest(unittest.TestCase): + + def setUp(self): + self.tzinfos = {"BRST": -10800} + self.brsttz = tzoffset("BRST", -10800) + self.default = datetime(2003, 9, 25) + + # Parser should be able to handle bytestring and unicode + self.uni_str = '2014-05-01 08:00:00' + self.str_str = self.uni_str.encode() + + def testEmptyString(self): + with self.assertRaises(ValueError): + parse('') + + def testNone(self): + with self.assertRaises(TypeError): + parse(None) + + def testInvalidType(self): + with self.assertRaises(TypeError): + parse(13) + + def testDuckTyping(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')) + + self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + + def testParseStream(self): + dstr = StringIO('2014 January 19') + + self.assertEqual(parse(dstr), datetime(2014, 1, 19)) + + def testParseStr(self): + self.assertEqual(parse(self.str_str), + parse(self.uni_str)) + + def testParseBytes(self): + self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19)) + + def testParseBytearray(self): + # GH #417 + self.assertEqual(parse(bytearray(b'2014 January 19')), + datetime(2014, 1, 19)) + + def testParserParseStr(self): + from dateutil.parser import parser + + self.assertEqual(parser().parse(self.str_str), + parser().parse(self.uni_str)) + + def testParseUnicodeWords(self): + + class rus_parserinfo(parserinfo): + MONTHS = [("янв", "Январь"), + ("фев", "Февраль"), + ("мар", "Март"), + ("апр", "Апрель"), + ("май", "Май"), + ("июн", "Июнь"), + ("июл", "Июль"), + ("авг", "Август"), + ("сен", "Сентябрь"), + ("окт", "Октябрь"), + ("ноя", "Ноябрь"), + ("дек", "Декабрь")] + + self.assertEqual(parse('10 Сентябрь 2015 10:20', + parserinfo=rus_parserinfo()), + datetime(2015, 9, 10, 10, 20)) + + 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' + + self.assertEqual(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 testDateCommandFormatUnicode(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 not PY3: + 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 testDateCommandFormatIgnoreTz(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", + ignoretz=True), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip1(self): + self.assertEqual(parse("Thu Sep 25 10:36:28 2003"), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip2(self): + self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip3(self): + self.assertEqual(parse("Thu Sep 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip4(self): + self.assertEqual(parse("Thu 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip5(self): + self.assertEqual(parse("Sep 10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip6(self): + self.assertEqual(parse("10:36:28", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testDateCommandFormatStrip7(self): + self.assertEqual(parse("10:36", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testDateCommandFormatStrip8(self): + self.assertEqual(parse("Thu Sep 25 2003"), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip10(self): + self.assertEqual(parse("Sep 2003", default=self.default), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip11(self): + self.assertEqual(parse("Sep", default=self.default), + datetime(2003, 9, 25)) + + def testDateCommandFormatStrip12(self): + self.assertEqual(parse("2003", default=self.default), + datetime(2003, 9, 25)) + + def testDateRCommandFormat(self): + self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOFormat(self): + self.assertEqual(parse("2003-09-25T10:49:41.5-03:00"), + datetime(2003, 9, 25, 10, 49, 41, 500000, + tzinfo=self.brsttz)) + + def testISOFormatStrip1(self): + self.assertEqual(parse("2003-09-25T10:49:41-03:00"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOFormatStrip2(self): + self.assertEqual(parse("2003-09-25T10:49:41"), + datetime(2003, 9, 25, 10, 49, 41)) + + def testISOFormatStrip3(self): + self.assertEqual(parse("2003-09-25T10:49"), + datetime(2003, 9, 25, 10, 49)) + + def testISOFormatStrip4(self): + self.assertEqual(parse("2003-09-25T10"), + datetime(2003, 9, 25, 10)) + + def testISOFormatStrip5(self): + self.assertEqual(parse("2003-09-25"), + datetime(2003, 9, 25)) + + def testISOStrippedFormat(self): + self.assertEqual(parse("20030925T104941.5-0300"), + datetime(2003, 9, 25, 10, 49, 41, 500000, + tzinfo=self.brsttz)) + + def testISOStrippedFormatStrip1(self): + self.assertEqual(parse("20030925T104941-0300"), + datetime(2003, 9, 25, 10, 49, 41, + tzinfo=self.brsttz)) + + def testISOStrippedFormatStrip2(self): + self.assertEqual(parse("20030925T104941"), + datetime(2003, 9, 25, 10, 49, 41)) + + def testISOStrippedFormatStrip3(self): + self.assertEqual(parse("20030925T1049"), + datetime(2003, 9, 25, 10, 49, 0)) + + def testISOStrippedFormatStrip4(self): + self.assertEqual(parse("20030925T10"), + datetime(2003, 9, 25, 10)) + + def testISOStrippedFormatStrip5(self): + self.assertEqual(parse("20030925"), + datetime(2003, 9, 25)) + + def testPythonLoggerFormat(self): + self.assertEqual(parse("2003-09-25 10:49:41,502"), + datetime(2003, 9, 25, 10, 49, 41, 502000)) + + def testNoSeparator1(self): + self.assertEqual(parse("199709020908"), + datetime(1997, 9, 2, 9, 8)) + + def testNoSeparator2(self): + self.assertEqual(parse("19970902090807"), + datetime(1997, 9, 2, 9, 8, 7)) + + def testDateWithDash1(self): + self.assertEqual(parse("2003-09-25"), + datetime(2003, 9, 25)) + + def testDateWithDash6(self): + self.assertEqual(parse("09-25-2003"), + datetime(2003, 9, 25)) + + def testDateWithDash7(self): + self.assertEqual(parse("25-09-2003"), + datetime(2003, 9, 25)) + + def testDateWithDash8(self): + self.assertEqual(parse("10-09-2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithDash9(self): + self.assertEqual(parse("10-09-2003"), + datetime(2003, 10, 9)) + + def testDateWithDash10(self): + self.assertEqual(parse("10-09-03"), + datetime(2003, 10, 9)) + + def testDateWithDash11(self): + self.assertEqual(parse("10-09-03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithDot1(self): + self.assertEqual(parse("2003.09.25"), + datetime(2003, 9, 25)) + + def testDateWithDot6(self): + self.assertEqual(parse("09.25.2003"), + datetime(2003, 9, 25)) + + def testDateWithDot7(self): + self.assertEqual(parse("25.09.2003"), + datetime(2003, 9, 25)) + + def testDateWithDot8(self): + self.assertEqual(parse("10.09.2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithDot9(self): + self.assertEqual(parse("10.09.2003"), + datetime(2003, 10, 9)) + + def testDateWithDot10(self): + self.assertEqual(parse("10.09.03"), + datetime(2003, 10, 9)) + + def testDateWithDot11(self): + self.assertEqual(parse("10.09.03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSlash1(self): + self.assertEqual(parse("2003/09/25"), + datetime(2003, 9, 25)) + + def testDateWithSlash6(self): + self.assertEqual(parse("09/25/2003"), + datetime(2003, 9, 25)) + + def testDateWithSlash7(self): + self.assertEqual(parse("25/09/2003"), + datetime(2003, 9, 25)) + + def testDateWithSlash8(self): + self.assertEqual(parse("10/09/2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithSlash9(self): + self.assertEqual(parse("10/09/2003"), + datetime(2003, 10, 9)) + + def testDateWithSlash10(self): + self.assertEqual(parse("10/09/03"), + datetime(2003, 10, 9)) + + def testDateWithSlash11(self): + self.assertEqual(parse("10/09/03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSpace1(self): + self.assertEqual(parse("2003 09 25"), + datetime(2003, 9, 25)) + + def testDateWithSpace6(self): + self.assertEqual(parse("09 25 2003"), + datetime(2003, 9, 25)) + + def testDateWithSpace7(self): + self.assertEqual(parse("25 09 2003"), + datetime(2003, 9, 25)) + + def testDateWithSpace8(self): + self.assertEqual(parse("10 09 2003", dayfirst=True), + datetime(2003, 9, 10)) + + def testDateWithSpace9(self): + self.assertEqual(parse("10 09 2003"), + datetime(2003, 10, 9)) + + def testDateWithSpace10(self): + self.assertEqual(parse("10 09 03"), + datetime(2003, 10, 9)) + + def testDateWithSpace11(self): + self.assertEqual(parse("10 09 03", yearfirst=True), + datetime(2010, 9, 3)) + + def testDateWithSpace12(self): + self.assertEqual(parse("25 09 03"), + datetime(2003, 9, 25)) + + def testStrangelyOrderedDate1(self): + self.assertEqual(parse("03 25 Sep"), + datetime(2003, 9, 25)) + + def testStrangelyOrderedDate3(self): + self.assertEqual(parse("25 03 Sep"), + datetime(2025, 9, 3)) + + def testHourWithLetters(self): + self.assertEqual(parse("10h36m28.5s", default=self.default), + datetime(2003, 9, 25, 10, 36, 28, 500000)) + + def testHourWithLettersStrip1(self): + self.assertEqual(parse("10h36m28s", default=self.default), + datetime(2003, 9, 25, 10, 36, 28)) + + def testHourWithLettersStrip2(self): + self.assertEqual(parse("10h36m", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testHourWithLettersStrip3(self): + self.assertEqual(parse("10h", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourWithLettersStrip4(self): + self.assertEqual(parse("10 h 36", default=self.default), + datetime(2003, 9, 25, 10, 36)) + + def testHourWithLetterStrip5(self): + self.assertEqual(parse("10 h 36.5", default=self.default), + datetime(2003, 9, 25, 10, 36, 30)) + + def testMinuteWithLettersSpaces1(self): + self.assertEqual(parse("36 m 5", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces2(self): + self.assertEqual(parse("36 m 5 s", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces3(self): + self.assertEqual(parse("36 m 05", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testMinuteWithLettersSpaces4(self): + self.assertEqual(parse("36 m 05 s", default=self.default), + datetime(2003, 9, 25, 0, 36, 5)) + + def testAMPMNoHour(self): + with self.assertRaises(ValueError): + parse("AM") + + with self.assertRaises(ValueError): + parse("Jan 20, 2015 PM") + + def testHourAmPm1(self): + self.assertEqual(parse("10h am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm2(self): + self.assertEqual(parse("10h pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm3(self): + self.assertEqual(parse("10am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm4(self): + self.assertEqual(parse("10pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm5(self): + self.assertEqual(parse("10:00 am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm6(self): + self.assertEqual(parse("10:00 pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm7(self): + self.assertEqual(parse("10:00am", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm8(self): + self.assertEqual(parse("10:00pm", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm9(self): + self.assertEqual(parse("10:00a.m", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm10(self): + self.assertEqual(parse("10:00p.m", default=self.default), + datetime(2003, 9, 25, 22)) + + def testHourAmPm11(self): + self.assertEqual(parse("10:00a.m.", default=self.default), + datetime(2003, 9, 25, 10)) + + def testHourAmPm12(self): + self.assertEqual(parse("10:00p.m.", default=self.default), + datetime(2003, 9, 25, 22)) + + def testAMPMRange(self): + with self.assertRaises(ValueError): + parse("13:44 AM") + + with self.assertRaises(ValueError): + 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 testWeekdayAlone(self): + self.assertEqual(parse("Wed", default=self.default), + datetime(2003, 10, 1)) + + def testLongWeekday(self): + self.assertEqual(parse("Wednesday", default=self.default), + datetime(2003, 10, 1)) + + def testLongMonth(self): + self.assertEqual(parse("October", default=self.default), + datetime(2003, 10, 25)) + + def testZeroYear(self): + self.assertEqual(parse("31-Dec-00", default=self.default), + datetime(2000, 12, 31)) + + 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 testExtraSpace(self): + self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat1(self): + self.assertEqual(parse("Wed, July 10, '96"), + datetime(1996, 7, 10, 0, 0)) + + def testRandomFormat2(self): + self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT", + ignoretz=True), + datetime(1996, 7, 10, 15, 8, 56)) + + def testRandomFormat3(self): + self.assertEqual(parse("1996.July.10 AD 12:08 PM"), + datetime(1996, 7, 10, 12, 8)) + + def testRandomFormat4(self): + self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", + ignoretz=True), + datetime(1952, 4, 12, 15, 30, 42)) + + def testRandomFormat5(self): + self.assertEqual(parse("November 5, 1994, 8:15:30 am EST", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat6(self): + self.assertEqual(parse("1994-11-05T08:15:30-05:00", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat7(self): + self.assertEqual(parse("1994-11-05T08:15:30Z", + ignoretz=True), + datetime(1994, 11, 5, 8, 15, 30)) + + def testRandomFormat8(self): + self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4)) + + def testRandomFormat9(self): + self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4)) + + def testRandomFormat10(self): + self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4)) + + def testRandomFormat11(self): + self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4)) + + def testRandomFormat12(self): + self.assertEqual(parse("19760704"), datetime(1976, 7, 4)) + + def testRandomFormat13(self): + self.assertEqual(parse("0:01:02", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat14(self): + self.assertEqual(parse("12h 01m02s am", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat15(self): + self.assertEqual(parse("0:01:02 on July 4, 1976"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat16(self): + self.assertEqual(parse("0:01:02 on July 4, 1976"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat17(self): + self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat18(self): + self.assertEqual(parse("July 4, 1976 12:01:02 am"), + datetime(1976, 7, 4, 0, 1, 2)) + + def testRandomFormat19(self): + self.assertEqual(parse("Mon Jan 2 04:24:27 1995"), + datetime(1995, 1, 2, 4, 24, 27)) + + def testRandomFormat20(self): + self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True), + datetime(1995, 4, 4, 0, 22, 12)) + + def testRandomFormat21(self): + self.assertEqual(parse("04.04.95 00:22"), + datetime(1995, 4, 4, 0, 22)) + + def testRandomFormat22(self): + self.assertEqual(parse("Jan 1 1999 11:23:34.578"), + datetime(1999, 1, 1, 11, 23, 34, 578000)) + + def testRandomFormat23(self): + self.assertEqual(parse("950404 122212"), + datetime(1995, 4, 4, 12, 22, 12)) + + def testRandomFormat24(self): + self.assertEqual(parse("0:00 PM, PST", default=self.default, + ignoretz=True), + datetime(2003, 9, 25, 12, 0)) + + def testRandomFormat25(self): + self.assertEqual(parse("12:08 PM", default=self.default), + datetime(2003, 9, 25, 12, 8)) + + 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 testRandomFormat27(self): + self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) + + def testRandomFormat28(self): + self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5)) + + def testRandomFormat29(self): + self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1)) + + def testRandomFormat30(self): + self.assertEqual(parse("01h02m03", default=self.default), + datetime(2003, 9, 25, 1, 2, 3)) + + def testRandomFormat31(self): + self.assertEqual(parse("01h02", default=self.default), + datetime(2003, 9, 25, 1, 2)) + + def testRandomFormat32(self): + self.assertEqual(parse("01h02s", default=self.default), + datetime(2003, 9, 25, 1, 0, 2)) + + def testRandomFormat33(self): + self.assertEqual(parse("01m02", default=self.default), + datetime(2003, 9, 25, 0, 1, 2)) + + def testRandomFormat34(self): + self.assertEqual(parse("01m02h", default=self.default), + datetime(2003, 9, 25, 2, 1)) + + def testRandomFormat35(self): + self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default), + datetime(2004, 4, 10, 11, 30)) + + def test_99_ad(self): + self.assertEqual(parse('0099-01-01T00:00:00'), + datetime(99, 1, 1, 0, 0)) + + def test_31_ad(self): + self.assertEqual(parse('0031-01-01T00:00:00'), + datetime(31, 1, 1, 0, 0)) + + def testInvalidDay(self): + with self.assertRaises(ValueError): + parse("Feb 30, 2007") + + 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 testTzinfoDictionaryCouldReturnNone(self): + self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos={"BRST": None}), + datetime(2017, 2, 3, 12, 40)) + + def testTzinfosCallableCouldReturnNone(self): + self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos=lambda *args: None), + datetime(2017, 2, 3, 12, 40)) + + def testErrorType01(self): + self.assertRaises(ValueError, + parse, 'shouldfail') + + def testCorrectErrorOnFuzzyWithTokens(self): + assertRaisesRegex(self, ValueError, 'Unknown string format', + parse, '04/04/32/423', fuzzy_with_tokens=True) + assertRaisesRegex(self, ValueError, 'Unknown string format', + parse, '04/04/04 +32423', fuzzy_with_tokens=True) + assertRaisesRegex(self, ValueError, '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): + self.assertEqual(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): + self.assertEqual(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") + self.assertEqual(dt1.microsecond, 10000) + self.assertEqual(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) + self.assertEqual(parse(dt.isoformat()), dt) + + def testHighPrecisionSeconds(self): + self.assertEqual(parse("20080227T21:26:01.123456789"), + datetime(2008, 2, 27, 21, 26, 1, 123456)) + + 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") + self.assertEqual(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) + self.assertEqual(res, datetime(2017, 7, 17, 6, 15)) + + def test_dBY(self): + # See GH360 + dtstr = '13NOV2017' + res = parse(dtstr) + self.assertEqual(res, datetime(2017, 11, 13)) + + def test_hmBY(self): + # See GH#483 + dtstr = '02:17NOV2017' + res = parse(dtstr, default=self.default) + self.assertEqual(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 self.assertRaises(ValueError): + parse(invalid) + + def test_era_trailing_year(self): + dstr = 'AD2001' + res = parse(dstr) + assert res.year == 2001, res + + def test_pre_12_year_same_month(self): + # See GH PR #293 + dtstr = '0003-03-04' + assert parse(dtstr) == datetime(3, 3, 4) + + +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(ValueError): + 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 infering 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.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_parse_unambiguous_nonexistent_local(): + # 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 + + +@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_tzlocal_in_gmt(): + # 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.tzutc()) + + assert dt == dt_exp + + +@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') +def test_tzlocal_parse_fold(): + # 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, kuntil `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.tzutc()) == dt_exp.astimezone(tz.tzutc()) + + +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.tzutc()) == dt_exp.astimezone(tz.tzutc()) + + +@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-ValueError exception when + # constructed with an invalid value + with pytest.raises(ValueError): + parse(value) + + +def test_BYd_corner_case(): + # GH#687 + res = parse('December.0031.30') + assert res == datetime(31, 12, 30) diff --git a/lib/dateutil/test/test_relativedelta.py b/lib/dateutil/test/test_relativedelta.py new file mode 100644 index 0000000..70cb543 --- /dev/null +++ b/lib/dateutil/test/test_relativedelta.py @@ -0,0 +1,678 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import WarningTestMixin, NotAValue + +import calendar +from datetime import datetime, date, timedelta +import unittest + +from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU + + +class RelativeDeltaTest(WarningTestMixin, 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 testNextWednesdayIsToday(self): + self.assertEqual(self.today+relativedelta(weekday=WE), + date(2003, 9, 17)) + + def testNextWenesdayNotToday(self): + self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), + date(2003, 9, 24)) + + 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 testRelativeDeltaFractionalAbsolutes(self): + # Fractional absolute values will soon be unsupported, + # check for the deprecation warning. + with self.assertWarns(DeprecationWarning): + relativedelta(year=2.86) + + with self.assertWarns(DeprecationWarning): + relativedelta(month=1.29) + + with self.assertWarns(DeprecationWarning): + relativedelta(day=0.44) + + with self.assertWarns(DeprecationWarning): + relativedelta(hour=23.98) + + with self.assertWarns(DeprecationWarning): + relativedelta(minute=45.21) + + with self.assertWarns(DeprecationWarning): + relativedelta(second=13.2) + + with self.assertWarns(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)) + + # Equvalent 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 diff --git a/lib/dateutil/test/test_rrule.py b/lib/dateutil/test/test_rrule.py new file mode 100644 index 0000000..cd08ce2 --- /dev/null +++ b/lib/dateutil/test/test_rrule.py @@ -0,0 +1,4842 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import WarningTestMixin + +from datetime import datetime, date +import unittest +from six import PY3 + +from dateutil import tz +from dateutil.rrule import ( + rrule, rruleset, rrulestr, + YEARLY, MONTHLY, WEEKLY, DAILY, + HOURLY, MINUTELY, SECONDLY, + MO, TU, WE, TH, FR, SA, SU +) + +from freezegun import freeze_time + +import pytest + + +@pytest.mark.rrule +class RRuleTest(WarningTestMixin, unittest.TestCase): + def _rrulestr_reverse_test(self, rule): + """ + Call with an `rrule` and it will test that `str(rrule)` generates a + string which generates the same `rrule` as the input when passed to + `rrulestr()` + """ + rr_str = str(rule) + rrulestr_rrule = rrulestr(rr_str) + + self.assertEqual(list(rule), list(rrulestr_rrule)) + + def testStrAppendRRULEToken(self): + # `_rrulestr_reverse_test` does not check if the "RRULE:" prefix + # property is appended properly, so give it a dedicated test + self.assertEqual(str(rrule(YEARLY, + count=5, + dtstart=datetime(1997, 9, 2, 9, 0))), + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=5") + + rr_str = ( + 'DTSTART:19970105T083000\nRRULE:FREQ=YEARLY;INTERVAL=2' + ) + self.assertEqual(str(rrulestr(rr_str)), rr_str) + + def testYearly(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testYearlyInterval(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0), + datetime(2001, 9, 2, 9, 0)]) + + def testYearlyIntervalLarge(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + interval=100, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(2097, 9, 2, 9, 0), + datetime(2197, 9, 2, 9, 0)]) + + def testYearlyByMonth(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 2, 9, 0), + datetime(1998, 3, 2, 9, 0), + datetime(1999, 1, 2, 9, 0)]) + + def testYearlyByMonthDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testYearlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testYearlyByWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testYearlyByNWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testYearlyByNWeekDayLarge(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 11, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 12, 17, 9, 0)]) + + def testYearlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testYearlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 29, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testYearlyByMonthAndNWeekDayLarge(self): + # This is interesting because the TH(-3) ends up before + # the TU(3). + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 15, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 3, 12, 9, 0)]) + + def testYearlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testYearlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testYearlyByYearDay(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testYearlyByYearDayNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testYearlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testYearlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testYearlyByWeekNo(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testYearlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testYearlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testYearlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testYearlyByEaster(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testYearlyByEasterPos(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testYearlyByEasterNeg(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testYearlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testYearlyByHour(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1998, 9, 2, 6, 0), + datetime(1998, 9, 2, 18, 0)]) + + def testYearlyByMinute(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1998, 9, 2, 9, 6)]) + + def testYearlyBySecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1998, 9, 2, 9, 0, 6)]) + + def testYearlyByHourAndMinute(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1998, 9, 2, 6, 6)]) + + def testYearlyByHourAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1998, 9, 2, 6, 0, 6)]) + + def testYearlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testYearlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testYearlyBySetPos(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonthday=15, + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 11, 15, 18, 0), + datetime(1998, 2, 15, 6, 0), + datetime(1998, 11, 15, 18, 0)]) + + def testMonthly(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 10, 2, 9, 0), + datetime(1997, 11, 2, 9, 0)]) + + def testMonthlyInterval(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 11, 2, 9, 0), + datetime(1998, 1, 2, 9, 0)]) + + def testMonthlyIntervalLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + interval=18, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1999, 3, 2, 9, 0), + datetime(2000, 9, 2, 9, 0)]) + + def testMonthlyByMonth(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 2, 9, 0), + datetime(1998, 3, 2, 9, 0), + datetime(1999, 1, 2, 9, 0)]) + + def testMonthlyByMonthDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testMonthlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testMonthlyByWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + # Third Monday of the month + self.assertEqual(rrule(MONTHLY, + byweekday=(MO(+3)), + dtstart=datetime(1997, 9, 1)).between(datetime(1997, 9, 1), + datetime(1997, 12, 1)), + [datetime(1997, 9, 15, 0, 0), + datetime(1997, 10, 20, 0, 0), + datetime(1997, 11, 17, 0, 0)]) + + def testMonthlyByNWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 25, 9, 0), + datetime(1997, 10, 7, 9, 0)]) + + def testMonthlyByNWeekDayLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 10, 16, 9, 0)]) + + def testMonthlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testMonthlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 29, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testMonthlyByMonthAndNWeekDayLarge(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 15, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 3, 12, 9, 0)]) + + def testMonthlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testMonthlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testMonthlyByYearDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testMonthlyByYearDayNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testMonthlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testMonthlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 4, 10, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testMonthlyByWeekNo(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testMonthlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testMonthlyByEaster(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testMonthlyByEasterPos(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testMonthlyByEasterNeg(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testMonthlyByHour(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 10, 2, 6, 0), + datetime(1997, 10, 2, 18, 0)]) + + def testMonthlyByMinute(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 10, 2, 9, 6)]) + + def testMonthlyBySecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 10, 2, 9, 0, 6)]) + + def testMonthlyByHourAndMinute(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 10, 2, 6, 6)]) + + def testMonthlyByHourAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 10, 2, 6, 0, 6)]) + + def testMonthlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testMonthlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testMonthlyBySetPos(self): + self.assertEqual(list(rrule(MONTHLY, + count=3, + bymonthday=(13, 17), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 13, 18, 0), + datetime(1997, 9, 17, 6, 0), + datetime(1997, 10, 13, 18, 0)]) + + def testWeekly(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testWeeklyInterval(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 30, 9, 0)]) + + def testWeeklyIntervalLarge(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 1, 20, 9, 0), + datetime(1998, 6, 9, 9, 0)]) + + def testWeeklyByMonth(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 13, 9, 0), + datetime(1998, 1, 20, 9, 0)]) + + def testWeeklyByMonthDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testWeeklyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testWeeklyByWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testWeeklyByNWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testWeeklyByMonthAndWeekDay(self): + # This test is interesting, because it crosses the year + # boundary in a weekly period to find day '1' as a + # valid recurrence. + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testWeeklyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testWeeklyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testWeeklyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testWeeklyByYearDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testWeeklyByYearDayNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testWeeklyByMonthAndYearDay(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testWeeklyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testWeeklyByWeekNo(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testWeeklyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testWeeklyByEaster(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testWeeklyByEasterPos(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testWeeklyByEasterNeg(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testWeeklyByHour(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 9, 6, 0), + datetime(1997, 9, 9, 18, 0)]) + + def testWeeklyByMinute(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 9, 9, 6)]) + + def testWeeklyBySecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 9, 9, 0, 6)]) + + def testWeeklyByHourAndMinute(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 9, 6, 6)]) + + def testWeeklyByHourAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 9, 6, 0, 6)]) + + def testWeeklyByMinuteAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testWeeklyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testWeeklyBySetPos(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 4, 6, 0), + datetime(1997, 9, 9, 18, 0)]) + + def testDaily(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testDailyInterval(self): + self.assertEqual(list(rrule(DAILY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 6, 9, 0)]) + + def testDailyIntervalLarge(self): + self.assertEqual(list(rrule(DAILY, + count=3, + interval=92, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 12, 3, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testDailyByMonth(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 2, 9, 0), + datetime(1998, 1, 3, 9, 0)]) + + def testDailyByMonthDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 10, 1, 9, 0), + datetime(1997, 10, 3, 9, 0)]) + + def testDailyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(1998, 1, 7, 9, 0), + datetime(1998, 3, 5, 9, 0)]) + + def testDailyByWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testDailyByNWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testDailyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testDailyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 1, 8, 9, 0)]) + + def testDailyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 2, 3, 9, 0), + datetime(1998, 3, 3, 9, 0)]) + + def testDailyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 3, 3, 9, 0), + datetime(2001, 3, 1, 9, 0)]) + + def testDailyByYearDay(self): + self.assertEqual(list(rrule(DAILY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testDailyByYearDayNeg(self): + self.assertEqual(list(rrule(DAILY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 9, 0), + datetime(1998, 1, 1, 9, 0), + datetime(1998, 4, 10, 9, 0), + datetime(1998, 7, 19, 9, 0)]) + + def testDailyByMonthAndYearDay(self): + self.assertEqual(list(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testDailyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 9, 0), + datetime(1998, 7, 19, 9, 0), + datetime(1999, 1, 1, 9, 0), + datetime(1999, 7, 19, 9, 0)]) + + def testDailyByWeekNo(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 9, 0), + datetime(1998, 5, 12, 9, 0), + datetime(1998, 5, 13, 9, 0)]) + + def testDailyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 9, 0), + datetime(1999, 1, 4, 9, 0), + datetime(2000, 1, 3, 9, 0)]) + + def testDailyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1998, 12, 27, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testDailyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 9, 0), + datetime(1999, 1, 3, 9, 0), + datetime(2000, 1, 2, 9, 0)]) + + def testDailyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 9, 0), + datetime(2004, 12, 27, 9, 0), + datetime(2009, 12, 28, 9, 0)]) + + def testDailyByEaster(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 9, 0), + datetime(1999, 4, 4, 9, 0), + datetime(2000, 4, 23, 9, 0)]) + + def testDailyByEasterPos(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 9, 0), + datetime(1999, 4, 5, 9, 0), + datetime(2000, 4, 24, 9, 0)]) + + def testDailyByEasterNeg(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 9, 0), + datetime(1999, 4, 3, 9, 0), + datetime(2000, 4, 22, 9, 0)]) + + def testDailyByHour(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 3, 6, 0), + datetime(1997, 9, 3, 18, 0)]) + + def testDailyByMinute(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 3, 9, 6)]) + + def testDailyBySecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 3, 9, 0, 6)]) + + def testDailyByHourAndMinute(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testDailyByHourAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 3, 6, 0, 6)]) + + def testDailyByMinuteAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testDailyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testDailyBySetPos(self): + self.assertEqual(list(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 15), + datetime(1997, 9, 3, 6, 45), + datetime(1997, 9, 3, 18, 15)]) + + def testHourly(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyInterval(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 11, 0), + datetime(1997, 9, 2, 13, 0)]) + + def testHourlyIntervalLarge(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + interval=769, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 10, 4, 10, 0), + datetime(1997, 11, 5, 11, 0)]) + + def testHourlyByMonth(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 3, 1, 0), + datetime(1997, 9, 3, 2, 0)]) + + def testHourlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0), + datetime(1998, 1, 5, 1, 0), + datetime(1998, 1, 5, 2, 0)]) + + def testHourlyByWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyByNWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 10, 0), + datetime(1997, 9, 2, 11, 0)]) + + def testHourlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 1, 0), + datetime(1998, 1, 1, 2, 0)]) + + def testHourlyByYearDay(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 1, 0), + datetime(1997, 12, 31, 2, 0), + datetime(1997, 12, 31, 3, 0)]) + + def testHourlyByYearDayNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 1, 0), + datetime(1997, 12, 31, 2, 0), + datetime(1997, 12, 31, 3, 0)]) + + def testHourlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 1, 0), + datetime(1998, 4, 10, 2, 0), + datetime(1998, 4, 10, 3, 0)]) + + def testHourlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 1, 0), + datetime(1998, 4, 10, 2, 0), + datetime(1998, 4, 10, 3, 0)]) + + def testHourlyByWeekNo(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0), + datetime(1998, 5, 11, 1, 0), + datetime(1998, 5, 11, 2, 0)]) + + def testHourlyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0), + datetime(1997, 12, 29, 1, 0), + datetime(1997, 12, 29, 2, 0)]) + + def testHourlyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 1, 0), + datetime(1997, 12, 28, 2, 0)]) + + def testHourlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 1, 0), + datetime(1997, 12, 28, 2, 0)]) + + def testHourlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0), + datetime(1998, 12, 28, 1, 0), + datetime(1998, 12, 28, 2, 0)]) + + def testHourlyByEaster(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0), + datetime(1998, 4, 12, 1, 0), + datetime(1998, 4, 12, 2, 0)]) + + def testHourlyByEasterPos(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0), + datetime(1998, 4, 13, 1, 0), + datetime(1998, 4, 13, 2, 0)]) + + def testHourlyByEasterNeg(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0), + datetime(1998, 4, 11, 1, 0), + datetime(1998, 4, 11, 2, 0)]) + + def testHourlyByHour(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 3, 6, 0), + datetime(1997, 9, 3, 18, 0)]) + + def testHourlyByMinute(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 2, 10, 6)]) + + def testHourlyBySecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 10, 0, 6)]) + + def testHourlyByHourAndMinute(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testHourlyByHourAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 3, 6, 0, 6)]) + + def testHourlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testHourlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testHourlyBySetPos(self): + self.assertEqual(list(rrule(HOURLY, + count=3, + byminute=(15, 45), + bysecond=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 15, 45), + datetime(1997, 9, 2, 9, 45, 15), + datetime(1997, 9, 2, 10, 15, 45)]) + + def testMinutely(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyInterval(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 2), + datetime(1997, 9, 2, 9, 4)]) + + def testMinutelyIntervalLarge(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + interval=1501, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 10, 1), + datetime(1997, 9, 4, 11, 2)]) + + def testMinutelyByMonth(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 3, 0, 1), + datetime(1997, 9, 3, 0, 2)]) + + def testMinutelyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0), + datetime(1998, 1, 5, 0, 1), + datetime(1998, 1, 5, 0, 2)]) + + def testMinutelyByWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyByNWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 2, 9, 1), + datetime(1997, 9, 2, 9, 2)]) + + def testMinutelyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0), + datetime(1998, 1, 1, 0, 1), + datetime(1998, 1, 1, 0, 2)]) + + def testMinutelyByYearDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 0, 1), + datetime(1997, 12, 31, 0, 2), + datetime(1997, 12, 31, 0, 3)]) + + def testMinutelyByYearDayNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0), + datetime(1997, 12, 31, 0, 1), + datetime(1997, 12, 31, 0, 2), + datetime(1997, 12, 31, 0, 3)]) + + def testMinutelyByMonthAndYearDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 0, 1), + datetime(1998, 4, 10, 0, 2), + datetime(1998, 4, 10, 0, 3)]) + + def testMinutelyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0), + datetime(1998, 4, 10, 0, 1), + datetime(1998, 4, 10, 0, 2), + datetime(1998, 4, 10, 0, 3)]) + + def testMinutelyByWeekNo(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0), + datetime(1998, 5, 11, 0, 1), + datetime(1998, 5, 11, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0), + datetime(1997, 12, 29, 0, 1), + datetime(1997, 12, 29, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 0, 1), + datetime(1997, 12, 28, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0), + datetime(1997, 12, 28, 0, 1), + datetime(1997, 12, 28, 0, 2)]) + + def testMinutelyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0), + datetime(1998, 12, 28, 0, 1), + datetime(1998, 12, 28, 0, 2)]) + + def testMinutelyByEaster(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0), + datetime(1998, 4, 12, 0, 1), + datetime(1998, 4, 12, 0, 2)]) + + def testMinutelyByEasterPos(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0), + datetime(1998, 4, 13, 0, 1), + datetime(1998, 4, 13, 0, 2)]) + + def testMinutelyByEasterNeg(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0), + datetime(1998, 4, 11, 0, 1), + datetime(1998, 4, 11, 0, 2)]) + + def testMinutelyByHour(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0), + datetime(1997, 9, 2, 18, 1), + datetime(1997, 9, 2, 18, 2)]) + + def testMinutelyByMinute(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6), + datetime(1997, 9, 2, 9, 18), + datetime(1997, 9, 2, 10, 6)]) + + def testMinutelyBySecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 9, 1, 6)]) + + def testMinutelyByHourAndMinute(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6), + datetime(1997, 9, 2, 18, 18), + datetime(1997, 9, 3, 6, 6)]) + + def testMinutelyByHourAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 2, 18, 1, 6)]) + + def testMinutelyByMinuteAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testMinutelyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testMinutelyBySetPos(self): + self.assertEqual(list(rrule(MINUTELY, + count=3, + bysecond=(15, 30, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 15), + datetime(1997, 9, 2, 9, 0, 45), + datetime(1997, 9, 2, 9, 1, 15)]) + + def testSecondly(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyInterval(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 2), + datetime(1997, 9, 2, 9, 0, 4)]) + + def testSecondlyIntervalLarge(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + interval=90061, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 3, 10, 1, 1), + datetime(1997, 9, 4, 11, 2, 2)]) + + def testSecondlyByMonth(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 3, 0, 0, 0), + datetime(1997, 9, 3, 0, 0, 1), + datetime(1997, 9, 3, 0, 0, 2)]) + + def testSecondlyByMonthAndMonthDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 0, 0, 0), + datetime(1998, 1, 5, 0, 0, 1), + datetime(1998, 1, 5, 0, 0, 2)]) + + def testSecondlyByWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyByNWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 0), + datetime(1997, 9, 2, 9, 0, 1), + datetime(1997, 9, 2, 9, 0, 2)]) + + def testSecondlyByMonthAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthAndNWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByMonthAndMonthDayAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 1, 0, 0, 0), + datetime(1998, 1, 1, 0, 0, 1), + datetime(1998, 1, 1, 0, 0, 2)]) + + def testSecondlyByYearDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0, 0), + datetime(1997, 12, 31, 0, 0, 1), + datetime(1997, 12, 31, 0, 0, 2), + datetime(1997, 12, 31, 0, 0, 3)]) + + def testSecondlyByYearDayNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 31, 0, 0, 0), + datetime(1997, 12, 31, 0, 0, 1), + datetime(1997, 12, 31, 0, 0, 2), + datetime(1997, 12, 31, 0, 0, 3)]) + + def testSecondlyByMonthAndYearDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0, 0), + datetime(1998, 4, 10, 0, 0, 1), + datetime(1998, 4, 10, 0, 0, 2), + datetime(1998, 4, 10, 0, 0, 3)]) + + def testSecondlyByMonthAndYearDayNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 10, 0, 0, 0), + datetime(1998, 4, 10, 0, 0, 1), + datetime(1998, 4, 10, 0, 0, 2), + datetime(1998, 4, 10, 0, 0, 3)]) + + def testSecondlyByWeekNo(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 5, 11, 0, 0, 0), + datetime(1998, 5, 11, 0, 0, 1), + datetime(1998, 5, 11, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDay(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 29, 0, 0, 0), + datetime(1997, 12, 29, 0, 0, 1), + datetime(1997, 12, 29, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDayLarge(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0, 0), + datetime(1997, 12, 28, 0, 0, 1), + datetime(1997, 12, 28, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDayLast(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 12, 28, 0, 0, 0), + datetime(1997, 12, 28, 0, 0, 1), + datetime(1997, 12, 28, 0, 0, 2)]) + + def testSecondlyByWeekNoAndWeekDay53(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 12, 28, 0, 0, 0), + datetime(1998, 12, 28, 0, 0, 1), + datetime(1998, 12, 28, 0, 0, 2)]) + + def testSecondlyByEaster(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 12, 0, 0, 0), + datetime(1998, 4, 12, 0, 0, 1), + datetime(1998, 4, 12, 0, 0, 2)]) + + def testSecondlyByEasterPos(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 13, 0, 0, 0), + datetime(1998, 4, 13, 0, 0, 1), + datetime(1998, 4, 13, 0, 0, 2)]) + + def testSecondlyByEasterNeg(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 4, 11, 0, 0, 0), + datetime(1998, 4, 11, 0, 0, 1), + datetime(1998, 4, 11, 0, 0, 2)]) + + def testSecondlyByHour(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 0), + datetime(1997, 9, 2, 18, 0, 1), + datetime(1997, 9, 2, 18, 0, 2)]) + + def testSecondlyByMinute(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 0), + datetime(1997, 9, 2, 9, 6, 1), + datetime(1997, 9, 2, 9, 6, 2)]) + + def testSecondlyBySecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0, 6), + datetime(1997, 9, 2, 9, 0, 18), + datetime(1997, 9, 2, 9, 1, 6)]) + + def testSecondlyByHourAndMinute(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 0), + datetime(1997, 9, 2, 18, 6, 1), + datetime(1997, 9, 2, 18, 6, 2)]) + + def testSecondlyByHourAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 0, 6), + datetime(1997, 9, 2, 18, 0, 18), + datetime(1997, 9, 2, 18, 1, 6)]) + + def testSecondlyByMinuteAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 6, 6), + datetime(1997, 9, 2, 9, 6, 18), + datetime(1997, 9, 2, 9, 18, 6)]) + + def testSecondlyByHourAndMinuteAndSecond(self): + self.assertEqual(list(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 18, 6, 6), + datetime(1997, 9, 2, 18, 6, 18), + datetime(1997, 9, 2, 18, 18, 6)]) + + def testSecondlyByHourAndMinuteAndSecondBug(self): + # This explores a bug found by Mathieu Bridon. + self.assertEqual(list(rrule(SECONDLY, + count=3, + bysecond=(0,), + byminute=(1,), + dtstart=datetime(2010, 3, 22, 12, 1))), + [datetime(2010, 3, 22, 12, 1), + datetime(2010, 3, 22, 13, 1), + datetime(2010, 3, 22, 14, 1)]) + + def testLongIntegers(self): + if not PY3: # There is no longs in python3 + self.assertEqual(list(rrule(MINUTELY, + count=long(2), + interval=long(2), + bymonth=long(2), + byweekday=long(3), + byhour=long(6), + byminute=long(6), + bysecond=long(6), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 2, 5, 6, 6, 6), + datetime(1998, 2, 12, 6, 6, 6)]) + self.assertEqual(list(rrule(YEARLY, + count=long(2), + bymonthday=long(5), + byweekno=long(2), + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1998, 1, 5, 9, 0), + datetime(2004, 1, 5, 9, 0)]) + + def testHourlyBadRRule(self): + """ + When `byhour` is specified with `freq=HOURLY`, there are certain + combinations of `dtstart` and `byhour` which result in an rrule with no + valid values. + + See https://github.com/dateutil/dateutil/issues/4 + """ + + self.assertRaises(ValueError, rrule, HOURLY, + **dict(interval=4, byhour=(7, 11, 15, 19), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testMinutelyBadRRule(self): + """ + See :func:`testHourlyBadRRule` for details. + """ + + self.assertRaises(ValueError, rrule, MINUTELY, + **dict(interval=12, byminute=(10, 11, 25, 39, 50), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testSecondlyBadRRule(self): + """ + See :func:`testHourlyBadRRule` for details. + """ + + self.assertRaises(ValueError, rrule, SECONDLY, + **dict(interval=10, bysecond=(2, 15, 37, 42, 59), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testMinutelyBadComboRRule(self): + """ + Certain values of :param:`interval` in :class:`rrule`, when combined + with certain values of :param:`byhour` create rules which apply to no + valid dates. The library should detect this case in the iterator and + raise a :exception:`ValueError`. + """ + + # In Python 2.7 you can use a context manager for this. + def make_bad_rrule(): + list(rrule(MINUTELY, interval=120, byhour=(10, 12, 14, 16), + count=2, dtstart=datetime(1997, 9, 2, 9, 0))) + + self.assertRaises(ValueError, make_bad_rrule) + + def testSecondlyBadComboRRule(self): + """ + See :func:`testMinutelyBadComboRRule' for details. + """ + + # In Python 2.7 you can use a context manager for this. + def make_bad_minute_rrule(): + list(rrule(SECONDLY, interval=360, byminute=(10, 28, 49), + count=4, dtstart=datetime(1997, 9, 2, 9, 0))) + + def make_bad_hour_rrule(): + list(rrule(SECONDLY, interval=43200, byhour=(2, 10, 18, 23), + count=4, dtstart=datetime(1997, 9, 2, 9, 0))) + + self.assertRaises(ValueError, make_bad_minute_rrule) + self.assertRaises(ValueError, make_bad_hour_rrule) + + def testBadUntilCountRRule(self): + """ + See rfc-5545 3.3.10 - This checks for the deprecation warning, and will + eventually check for an error. + """ + with self.assertWarns(DeprecationWarning): + rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0), + count=3, until=datetime(1997, 9, 4, 9, 0)) + + def testUntilNotMatching(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 5, 8, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testUntilMatching(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 4, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testUntilSingle(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0)]) + + def testUntilEmpty(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=datetime(1997, 9, 1, 9, 0))), + []) + + def testUntilWithDate(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0), + until=date(1997, 9, 5))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testWkStIntervalMO(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + byweekday=(TU, SU), + wkst=MO, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testWkStIntervalSU(self): + self.assertEqual(list(rrule(WEEKLY, + count=3, + interval=2, + byweekday=(TU, SU), + wkst=SU, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testDTStartIsDate(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=date(1997, 9, 2))), + [datetime(1997, 9, 2, 0, 0), + datetime(1997, 9, 3, 0, 0), + datetime(1997, 9, 4, 0, 0)]) + + def testDTStartWithMicroseconds(self): + self.assertEqual(list(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0, 0, 500000))), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testMaxYear(self): + self.assertEqual(list(rrule(YEARLY, + count=3, + bymonth=2, + bymonthday=31, + dtstart=datetime(9997, 9, 2, 9, 0, 0))), + []) + + def testGetItem(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[0], + datetime(1997, 9, 2, 9, 0)) + + def testGetItemNeg(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[-1], + datetime(1997, 9, 4, 9, 0)) + + def testGetItemSlice(self): + self.assertEqual(rrule(DAILY, + # count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[1:2], + [datetime(1997, 9, 3, 9, 0)]) + + def testGetItemSliceEmpty(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[:], + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0)]) + + def testGetItemSliceStep(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))[::-2], + [datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 2, 9, 0)]) + + def testCount(self): + self.assertEqual(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0)).count(), + 3) + + def testCountZero(self): + self.assertEqual(rrule(YEARLY, + count=0, + dtstart=datetime(1997, 9, 2, 9, 0)).count(), + 0) + + def testContains(self): + rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testContainsNot(self): + rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) not in rr, False) + + def testBefore(self): + self.assertEqual(rrule(DAILY, # count=5 + dtstart=datetime(1997, 9, 2, 9, 0)).before(datetime(1997, 9, 5, 9, 0)), + datetime(1997, 9, 4, 9, 0)) + + def testBeforeInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .before(datetime(1997, 9, 5, 9, 0), inc=True), + datetime(1997, 9, 5, 9, 0)) + + def testAfter(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .after(datetime(1997, 9, 4, 9, 0)), + datetime(1997, 9, 5, 9, 0)) + + def testAfterInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .after(datetime(1997, 9, 4, 9, 0), inc=True), + datetime(1997, 9, 4, 9, 0)) + + def testXAfter(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0)) + .xafter(datetime(1997, 9, 8, 9, 0), count=12)), + [datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 17, 9, 0), + datetime(1997, 9, 18, 9, 0), + datetime(1997, 9, 19, 9, 0), + datetime(1997, 9, 20, 9, 0)]) + + def testXAfterInc(self): + self.assertEqual(list(rrule(DAILY, + dtstart=datetime(1997, 9, 2, 9, 0)) + .xafter(datetime(1997, 9, 8, 9, 0), count=12, inc=True)), + [datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0), + datetime(1997, 9, 17, 9, 0), + datetime(1997, 9, 18, 9, 0), + datetime(1997, 9, 19, 9, 0)]) + + def testBetween(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .between(datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 6, 9, 0)), + [datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0)]) + + def testBetweenInc(self): + self.assertEqual(rrule(DAILY, + #count=5, + dtstart=datetime(1997, 9, 2, 9, 0)) + .between(datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 6, 9, 0), inc=True), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0)]) + + def testCachePre(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(list(rr), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePost(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(list(rr), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePostInternal(self): + rr = rrule(DAILY, count=15, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(rr._cache, + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 3, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 5, 9, 0), + datetime(1997, 9, 6, 9, 0), + datetime(1997, 9, 7, 9, 0), + datetime(1997, 9, 8, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 10, 9, 0), + datetime(1997, 9, 11, 9, 0), + datetime(1997, 9, 12, 9, 0), + datetime(1997, 9, 13, 9, 0), + datetime(1997, 9, 14, 9, 0), + datetime(1997, 9, 15, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testCachePreContains(self): + rr = rrule(DAILY, count=3, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testCachePostContains(self): + rr = rrule(DAILY, count=3, cache=True, + dtstart=datetime(1997, 9, 2, 9, 0)) + for x in rr: pass + self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) + + def testStr(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrWithTZID(self): + NYC = tz.gettz('America/New_York') + self.assertEqual(list(rrulestr( + "DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + )), + [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)]) + + def testStrWithTZIDMapping(self): + rrstr = ("DTSTART;TZID=Eastern:19970902T090000\n" + + "RRULE:FREQ=YEARLY;COUNT=3") + + NYC = tz.gettz('America/New_York') + rr = rrulestr(rrstr, tzids={'Eastern': NYC}) + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=NYC), + datetime(1998, 9, 2, 9, 0, tzinfo=NYC), + datetime(1999, 9, 2, 9, 0, tzinfo=NYC)] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallable(self): + rrstr = ('DTSTART;TZID=UTC+04:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + TZ = tz.tzstr('UTC+04') + def parse_tzstr(tzstr): + if tzstr is None: + raise ValueError('Invalid tzstr') + + return tz.tzstr(tzstr) + + rr = rrulestr(rrstr, tzids=parse_tzstr) + + exp = [datetime(1997, 9, 2, 9, 0, tzinfo=TZ), + datetime(1998, 9, 2, 9, 0, tzinfo=TZ), + datetime(1999, 9, 2, 9, 0, tzinfo=TZ),] + + self.assertEqual(list(rr), exp) + + def testStrWithTZIDCallableFailure(self): + rrstr = ('DTSTART;TZID=America/New_York:19970902T090000\n' + + 'RRULE:FREQ=YEARLY;COUNT=3') + + class TzInfoError(Exception): + pass + + def tzinfos(tzstr): + if tzstr == 'America/New_York': + raise TzInfoError('Invalid!') + return None + + with self.assertRaises(TzInfoError): + rrulestr(rrstr, tzids=tzinfos) + + def testStrWithConflictingTZID(self): + # RFC 5545 Section 3.3.5, FORM #2: DATE WITH UTC TIME + # https://tools.ietf.org/html/rfc5545#section-3.3.5 + # The "TZID" property parameter MUST NOT be applied to DATE-TIME + with self.assertRaises(ValueError): + rrulestr("DTSTART;TZID=America/New_York:19970902T090000Z\n"+ + "RRULE:FREQ=YEARLY;COUNT=3\n") + + def testStrType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + ), rrule), True) + + def testStrForceSetType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3\n" + , forceset=True), rruleset), True) + + def testStrSetType(self): + self.assertEqual(isinstance(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" + ), rruleset), True) + + def testStrCase(self): + self.assertEqual(list(rrulestr( + "dtstart:19970902T090000\n" + "rrule:freq=yearly;count=3\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSpaces(self): + self.assertEqual(list(rrulestr( + " DTSTART:19970902T090000 " + " RRULE:FREQ=YEARLY;COUNT=3 " + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSpacesAndLines(self): + self.assertEqual(list(rrulestr( + " DTSTART:19970902T090000 \n" + " \n" + " RRULE:FREQ=YEARLY;COUNT=3 \n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrNoDTStart(self): + self.assertEqual(list(rrulestr( + "RRULE:FREQ=YEARLY;COUNT=3\n" + , dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrValueOnly(self): + self.assertEqual(list(rrulestr( + "FREQ=YEARLY;COUNT=3\n" + , dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrUnfold(self): + self.assertEqual(list(rrulestr( + "FREQ=YEA\n RLY;COUNT=3\n", unfold=True, + dtstart=datetime(1997, 9, 2, 9, 0))), + [datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0)]) + + def testStrSet(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testStrSetDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testStrSetExRule(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetExDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE:19970904T090000\n" + "EXDATE:19970911T090000\n" + "EXDATE:19970918T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetDateAndExDate(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RDATE:19970902T090000\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + "RDATE:19970911T090000\n" + "RDATE:19970916T090000\n" + "RDATE:19970918T090000\n" + "EXDATE:19970904T090000\n" + "EXDATE:19970911T090000\n" + "EXDATE:19970918T090000\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrSetDateAndExRule(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RDATE:19970902T090000\n" + "RDATE:19970904T090000\n" + "RDATE:19970909T090000\n" + "RDATE:19970911T090000\n" + "RDATE:19970916T090000\n" + "RDATE:19970918T090000\n" + "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" + )), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testStrKeywords(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;" + "BYMONTH=3;BYWEEKDAY=TH;BYMONTHDAY=3;" + "BYHOUR=3;BYMINUTE=3;BYSECOND=3\n" + )), + [datetime(2033, 3, 3, 3, 3, 3), + datetime(2039, 3, 3, 3, 3, 3), + datetime(2072, 3, 3, 3, 3, 3)]) + + def testStrNWeekDay(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n" + )), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testStrUntil(self): + self.assertEqual(list(rrulestr( + "DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n" + )), + [datetime(1997, 12, 25, 9, 0), + datetime(1998, 1, 6, 9, 0), + datetime(1998, 12, 31, 9, 0)]) + + def testStrValueDatetime(self): + rr = rrulestr("DTSTART;VALUE=DATE-TIME:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 9, 0, 0), + datetime(1998, 9, 2, 9, 0, 0)]) + + def testStrValueDate(self): + rr = rrulestr("DTSTART;VALUE=DATE:19970902\n" + "RRULE:FREQ=YEARLY;COUNT=2") + + self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0), + datetime(1998, 9, 2, 0, 0, 0)]) + + def testStrInvalidUntil(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n")) + + def testStrUntilMustBeUTC(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART;TZID=America/New_York:19970902T090000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n")) + + def testStrUntilWithTZ(self): + NYC = tz.gettz('America/New_York') + rr = list(rrulestr("DTSTART;TZID=America/New_York:19970101T000000\n" + "RRULE:FREQ=YEARLY;" + "UNTIL=19990101T000000Z\n")) + self.assertEqual(list(rr), [datetime(1997, 1, 1, 0, 0, 0, tzinfo=NYC), + datetime(1998, 1, 1, 0, 0, 0, tzinfo=NYC)]) + + def testStrEmptyByDay(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "FREQ=WEEKLY;" + "BYDAY=;" # This part is invalid + "WKST=SU")) + + def testStrInvalidByDay(self): + with self.assertRaises(ValueError): + list(rrulestr("DTSTART:19970902T090000\n" + "FREQ=WEEKLY;" + "BYDAY=-1OK;" # This part is invalid + "WKST=SU")) + + def testBadBySetPos(self): + self.assertRaises(ValueError, + rrule, MONTHLY, + count=1, + bysetpos=0, + dtstart=datetime(1997, 9, 2, 9, 0)) + + def testBadBySetPosMany(self): + self.assertRaises(ValueError, + rrule, MONTHLY, + count=1, + bysetpos=(-1, 0, 1), + dtstart=datetime(1997, 9, 2, 9, 0)) + + # Tests to ensure that str(rrule) works + def testToStrYearly(self): + rule = rrule(YEARLY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) + self._rrulestr_reverse_test(rule) + + def testToStrYearlyInterval(self): + rule = rrule(YEARLY, count=3, interval=2, + dtstart=datetime(1997, 9, 2, 9, 0)) + self._rrulestr_reverse_test(rule) + + def testToStrYearlyByMonth(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndNWeekDayLarge(self): + # This is interesting because the TH(-3) ends up before + # the TU(3). + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByYearDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEaster(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHour(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMinute(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyBySecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrYearlyBySetPos(self): + self._rrulestr_reverse_test(rrule(YEARLY, + count=3, + bymonthday=15, + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthly(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyInterval(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + interval=18, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonth(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + # Third Monday of the month + self.assertEqual(rrule(MONTHLY, + byweekday=(MO(+3)), + dtstart=datetime(1997, 9, 1)).between(datetime(1997, + 9, + 1), + datetime(1997, + 12, + 1)), + [datetime(1997, 9, 15, 0, 0), + datetime(1997, 10, 20, 0, 0), + datetime(1997, 11, 17, 0, 0)]) + + def testToStrMonthlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndNWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(3), TH(-3)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByYearDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEaster(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHour(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMinute(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyBySecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMonthlyBySetPos(self): + self._rrulestr_reverse_test(rrule(MONTHLY, + count=3, + bymonthday=(13, 17), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeekly(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyInterval(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + interval=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonth(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndWeekDay(self): + # This test is interesting, because it crosses the year + # boundary in a weekly period to find day '1' as a + # valid recurrence. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByYearDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNo(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEaster(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEasterPos(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHour(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMinute(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyBySecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrWeeklyBySetPos(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + byweekday=(TU, TH), + byhour=(6, 18), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDaily(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyInterval(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + interval=92, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonth(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByYearDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=4, + bymonth=(1, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNo(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDay(self): + # That's a nice one. The first days of week number one + # may be in the last year. + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDayLarge(self): + # Another nice test. The last days of week number 52/53 + # may be in the next year. + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEaster(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEasterPos(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHour(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMinute(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyBySecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrDailyBySetPos(self): + self._rrulestr_reverse_test(rrule(DAILY, + count=3, + byhour=(6, 18), + byminute=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourly(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyInterval(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + interval=769, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonth(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByYearDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEaster(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHour(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMinute(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyBySecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrHourlyBySetPos(self): + self._rrulestr_reverse_test(rrule(HOURLY, + count=3, + byminute=(15, 45), + bysecond=(15, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutely(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyInterval(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + interval=1501, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonth(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByYearDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNo(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEaster(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEasterPos(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHour(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMinute(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyBySecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrMinutelyBySetPos(self): + self._rrulestr_reverse_test(rrule(MINUTELY, + count=3, + bysecond=(15, 30, 45), + bysetpos=(3, -3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondly(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyInterval(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + interval=2, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyIntervalLarge(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + interval=90061, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonth(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndMonthDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(5, 7), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByNWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndNWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + byweekday=(TU(1), TH(-1)), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndMonthDayAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bymonth=(1, 3), + bymonthday=(1, 3), + byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByYearDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByYearDayNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndYearDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(1, 100, 200, 365), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMonthAndYearDayNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=4, + bymonth=(4, 7), + byyearday=(-365, -266, -166, -1), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNo(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=20, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDay(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=1, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDayLarge(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=52, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDayLast(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=-1, + byweekday=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByWeekNoAndWeekDay53(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byweekno=53, + byweekday=MO, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEaster(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=0, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEasterPos(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByEasterNeg(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byeaster=-1, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHour(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMinute(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyBySecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinute(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinuteAndSecond(self): + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + byhour=(6, 18), + byminute=(6, 18), + bysecond=(6, 18), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrSecondlyByHourAndMinuteAndSecondBug(self): + # This explores a bug found by Mathieu Bridon. + self._rrulestr_reverse_test(rrule(SECONDLY, + count=3, + bysecond=(0,), + byminute=(1,), + dtstart=datetime(2010, 3, 22, 12, 1))) + + def testToStrWithWkSt(self): + self._rrulestr_reverse_test(rrule(WEEKLY, + count=3, + wkst=SU, + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testToStrLongIntegers(self): + if not PY3: # There is no longs in python3 + self._rrulestr_reverse_test(rrule(MINUTELY, + count=long(2), + interval=long(2), + bymonth=long(2), + byweekday=long(3), + byhour=long(6), + byminute=long(6), + bysecond=long(6), + dtstart=datetime(1997, 9, 2, 9, 0))) + + self._rrulestr_reverse_test(rrule(YEARLY, + count=long(2), + bymonthday=long(5), + byweekno=long(2), + dtstart=datetime(1997, 9, 2, 9, 0))) + + def testReplaceIfSet(self): + rr = rrule(YEARLY, + count=1, + bymonthday=5, + dtstart=datetime(1997, 1, 1)) + newrr = rr.replace(bymonthday=6) + self.assertEqual(list(rr), [datetime(1997, 1, 5)]) + self.assertEqual(list(newrr), + [datetime(1997, 1, 6)]) + + def testReplaceIfNotSet(self): + rr = rrule(YEARLY, + count=1, + dtstart=datetime(1997, 1, 1)) + newrr = rr.replace(bymonthday=6) + self.assertEqual(list(rr), [datetime(1997, 1, 1)]) + self.assertEqual(list(newrr), + [datetime(1997, 1, 6)]) + + +@pytest.mark.rrule +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart(): + dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC) + UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC) + + rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL) + rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL) + assert list(rule_without_dtstart) == list(rule_with_dtstart) + + +@pytest.mark.rrule +@pytest.mark.rrulestr +@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637") +@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)) +def test_generated_aware_dtstart_rrulestr(): + rrule_without_dtstart = rrule(freq=HOURLY, + until=datetime(2018, 3, 6, 8, 0, + tzinfo=tz.UTC)) + rrule_r = rrulestr(str(rrule_without_dtstart)) + + assert list(rrule_r) == list(rrule_without_dtstart) + + +@pytest.mark.rruleset +class RRuleSetTest(unittest.TestCase): + def testSet(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetDate(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=1, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetExRule(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetExDate(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exdate(datetime(1997, 9, 4, 9)) + rrset.exdate(datetime(1997, 9, 11, 9)) + rrset.exdate(datetime(1997, 9, 18, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetExDateRevOrder(self): + rrset = rruleset() + rrset.rrule(rrule(MONTHLY, count=5, bymonthday=10, + dtstart=datetime(2004, 1, 1, 9, 0))) + rrset.exdate(datetime(2004, 4, 10, 9, 0)) + rrset.exdate(datetime(2004, 2, 10, 9, 0)) + self.assertEqual(list(rrset), + [datetime(2004, 1, 10, 9, 0), + datetime(2004, 3, 10, 9, 0), + datetime(2004, 5, 10, 9, 0)]) + + def testSetDateAndExDate(self): + rrset = rruleset() + rrset.rdate(datetime(1997, 9, 2, 9)) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + rrset.rdate(datetime(1997, 9, 11, 9)) + rrset.rdate(datetime(1997, 9, 16, 9)) + rrset.rdate(datetime(1997, 9, 18, 9)) + rrset.exdate(datetime(1997, 9, 4, 9)) + rrset.exdate(datetime(1997, 9, 11, 9)) + rrset.exdate(datetime(1997, 9, 18, 9)) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetDateAndExRule(self): + rrset = rruleset() + rrset.rdate(datetime(1997, 9, 2, 9)) + rrset.rdate(datetime(1997, 9, 4, 9)) + rrset.rdate(datetime(1997, 9, 9, 9)) + rrset.rdate(datetime(1997, 9, 11, 9)) + rrset.rdate(datetime(1997, 9, 16, 9)) + rrset.rdate(datetime(1997, 9, 18, 9)) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)]) + + def testSetCount(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(rrset.count(), 3) + + def testSetCachePre(self): + rrset = rruleset() + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetCachePost(self): + rrset = rruleset(cache=True) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + for x in rrset: pass + self.assertEqual(list(rrset), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetCachePostInternal(self): + rrset = rruleset(cache=True) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, + dtstart=datetime(1997, 9, 2, 9, 0))) + rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, + dtstart=datetime(1997, 9, 2, 9, 0))) + for x in rrset: pass + self.assertEqual(list(rrset._cache), + [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 4, 9, 0), + datetime(1997, 9, 9, 9, 0)]) + + def testSetRRuleCount(self): + # Test that the count is updated when an rrule is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.rrule(rrule(MONTHLY, count=3, dtstart=datetime(1994, 1, 3))) + + self.assertEqual(rrset.count(), 9) + self.assertEqual(rrset.count(), 9) + + def testSetRDateCount(self): + # Test that the count is updated when an rdate is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.rdate(datetime(1993, 2, 14)) + + self.assertEqual(rrset.count(), 7) + self.assertEqual(rrset.count(), 7) + + def testSetExRuleCount(self): + # Test that the count is updated when an exrule is added + rrset = rruleset(cache=False) + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.exrule(rrule(WEEKLY, count=2, interval=2, + dtstart=datetime(1991, 6, 14))) + + self.assertEqual(rrset.count(), 4) + self.assertEqual(rrset.count(), 4) + + def testSetExDateCount(self): + # Test that the count is updated when an rdate is added + for cache in (True, False): + rrset = rruleset(cache=cache) + rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, + dtstart=datetime(1983, 4, 1))) + rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, + dtstart=datetime(1991, 6, 3))) + + # Check the length twice - first one sets a cache, second reads it + self.assertEqual(rrset.count(), 6) + self.assertEqual(rrset.count(), 6) + + # This should invalidate the cache and force an update + rrset.exdate(datetime(1991, 6, 28)) + + self.assertEqual(rrset.count(), 5) + self.assertEqual(rrset.count(), 5) + + +class WeekdayTest(unittest.TestCase): + def testInvalidNthWeekday(self): + with self.assertRaises(ValueError): + FR(0) + + def testWeekdayCallable(self): + # Calling a weekday instance generates a new weekday instance with the + # value of n changed. + from dateutil.rrule import weekday + self.assertEqual(MO(1), weekday(0, 1)) + + # Calling a weekday instance with the identical n returns the original + # object + FR_3 = weekday(4, 3) + self.assertIs(FR_3(3), FR_3) + + def testWeekdayEquality(self): + # Two weekday objects are not equal if they have different values for n + self.assertNotEqual(TH, TH(-1)) + self.assertNotEqual(SA(3), SA(2)) + + def testWeekdayEqualitySubclass(self): + # Two weekday objects equal if their "weekday" and "n" attributes are + # available and the same + class BasicWeekday(object): + def __init__(self, weekday): + self.weekday = weekday + + class BasicNWeekday(BasicWeekday): + def __init__(self, weekday, n=None): + super(BasicNWeekday, self).__init__(weekday) + self.n = n + + MO_Basic = BasicWeekday(0) + + self.assertNotEqual(MO, MO_Basic) + self.assertNotEqual(MO(1), MO_Basic) + + TU_BasicN = BasicNWeekday(1) + + self.assertEqual(TU, TU_BasicN) + self.assertNotEqual(TU(3), TU_BasicN) + + WE_Basic3 = BasicNWeekday(2, 3) + self.assertEqual(WE(3), WE_Basic3) + self.assertNotEqual(WE(2), WE_Basic3) + + def testWeekdayReprNoN(self): + no_n_reprs = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU') + no_n_wdays = (MO, TU, WE, TH, FR, SA, SU) + + for repstr, wday in zip(no_n_reprs, no_n_wdays): + self.assertEqual(repr(wday), repstr) + + def testWeekdayReprWithN(self): + with_n_reprs = ('WE(+1)', 'TH(-2)', 'SU(+3)') + with_n_wdays = (WE(1), TH(-2), SU(+3)) + + for repstr, wday in zip(with_n_reprs, with_n_wdays): + self.assertEqual(repr(wday), repstr) diff --git a/lib/dateutil/test/test_tz.py b/lib/dateutil/test/test_tz.py new file mode 100644 index 0000000..54dfb1b --- /dev/null +++ b/lib/dateutil/test/test_tz.py @@ -0,0 +1,2603 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from ._common import PicklableMixin +from ._common import TZEnvContext, TZWinContext +from ._common import WarningTestMixin +from ._common import ComparesEqual + +from datetime import datetime, timedelta +from datetime import time as dt_time +from datetime import tzinfo +from six import BytesIO, StringIO +import unittest + +import sys +import base64 +import copy + +from functools import partial + +IS_WIN = sys.platform.startswith('win') + +import pytest + +# dateutil imports +from dateutil.relativedelta import relativedelta, SU, TH +from dateutil.parser import parse +from dateutil import tz as tz +from dateutil import zoneinfo + +try: + from dateutil import tzwin +except ImportError as e: + if IS_WIN: + raise e + else: + pass + +MISSING_TARBALL = ("This test fails if you don't have the dateutil " + "timezone file installed. Please read the README") + +TZFILE_EST5EDT = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh +ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e +S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 +YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg +yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db +wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW +8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b +YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g +BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR +iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x +znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H +cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w +Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH +JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM +jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w +4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg +b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8 +o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU +AEVQVAAAAAABAAAAAQ== +""" + +EUROPE_HELSINKI = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV +I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM +VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8 +kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q +Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK +46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV +RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u +kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ +czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC +AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD +BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME +AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ== +""" + +NEW_YORK = b""" +VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh +ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e +S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 +YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg +yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db +wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW +8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b +YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g +BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR +iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x +zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H +gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG +Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH +LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV +yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr +d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3 +b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8 +fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA +AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB +AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU +AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G +AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A +AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA +ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE= +""" + +TZICAL_EST5EDT = """ +BEGIN:VTIMEZONE +TZID:US-Eastern +LAST-MODIFIED:19870101T000000Z +TZURL:http://zones.stds_r_us.net/tz/US-Eastern +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +END:DAYLIGHT +END:VTIMEZONE +""" + +TZICAL_PST8PDT = """ +BEGIN:VTIMEZONE +TZID:US-Pacific +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE +""" + +EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0)) +EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1)) + + +### +# Helper functions +def get_timezone_tuple(dt): + """Retrieve a (tzname, utcoffset, dst) tuple for a given DST""" + return dt.tzname(), dt.utcoffset(), dt.dst() + + +### +# Mix-ins +class context_passthrough(object): + def __init__(*args, **kwargs): + pass + + def __enter__(*args, **kwargs): + pass + + def __exit__(*args, **kwargs): + pass + + +class TzFoldMixin(object): + """ Mix-in class for testing ambiguous times """ + def gettz(self, tzname): + raise NotImplementedError + + def _get_tzname(self, tzname): + return tzname + + def _gettz_context(self, tzname): + return context_passthrough() + + def testFoldPositiveUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD = self.gettz(tzname) + + t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.tzutc()) # AEST + t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc()) # AEDT + + t0_syd0 = t0_u.astimezone(SYD) + t1_syd1 = t1_u.astimezone(SYD) + + self.assertEqual(t0_syd0.replace(tzinfo=None), + datetime(2012, 4, 1, 2, 30)) + + self.assertEqual(t1_syd1.replace(tzinfo=None), + datetime(2012, 4, 1, 2, 30)) + + self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11)) + self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10)) + + def testGapPositiveUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD = self.gettz(tzname) + + t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.tzutc()) # AEST + t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.tzutc()) # AEDT + + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2012, 10, 7, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2012, 10, 7, 3, 30)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=10)) + self.assertEqual(t1.utcoffset(), timedelta(hours=11)) + + def testFoldNegativeUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = self._get_tzname('America/Toronto') + + with self._gettz_context(tzname): + TOR = self.gettz(tzname) + + t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.tzutc()) + t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.tzutc()) + + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) + + self.assertEqual(t0_tor.replace(tzinfo=None), + datetime(2011, 11, 6, 1, 30)) + + self.assertEqual(t1_tor.replace(tzinfo=None), + datetime(2011, 11, 6, 1, 30)) + + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) + + def testGapNegativeUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = self._get_tzname('America/Toronto') + + with self._gettz_context(tzname): + TOR = self.gettz(tzname) + + t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.tzutc()) + t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.tzutc()) + + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2011, 3, 13, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2011, 3, 13, 3, 30)) + + self.assertNotEqual(t0, t1) + self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) + self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) + + def testFoldLondon(self): + tzname = self._get_tzname('Europe/London') + + with self._gettz_context(tzname): + LON = self.gettz(tzname) + UTC = tz.tzutc() + + t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC) # BST + t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC) # GMT + + t0 = t0_u.astimezone(LON) + t1 = t1_u.astimezone(LON) + + self.assertEqual(t0.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t1.replace(tzinfo=None), + datetime(2013, 10, 27, 1, 30)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=1)) + self.assertEqual(t1.utcoffset(), timedelta(hours=0)) + + def testFoldIndependence(self): + tzname = self._get_tzname('America/New_York') + + with self._gettz_context(tzname): + NYC = self.gettz(tzname) + UTC = tz.tzutc() + hour = timedelta(hours=1) + + # Firmly 2015-11-01 0:30 EDT-4 + pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC) + + # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5 + in_dst = pre_dst + hour + in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT + + # Doing the arithmetic in UTC creates a date that is unambiguously + # 2015-11-01 1:30 EDT-5 + in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC) + + # Make sure the dates are actually ambiguous + self.assertEqual(in_dst, in_dst_via_utc) + + # Make sure we got the right folding behavior + self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0) + + # Now check to make sure in_dst's tzname hasn't changed + self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + + tzname = self._get_tzname('America/New_York') + + with self._gettz_context(tzname): + NYC = self.gettz(tzname) + UTC = tz.tzutc() + + dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) + + def _test_ambiguous_time(self, dt, tzid, ambiguous): + # This is a test to check that the individual is_ambiguous values + # on the _tzinfo subclasses work. + tzname = self._get_tzname(tzid) + + with self._gettz_context(tzname): + tzi = self.gettz(tzname) + + self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous) + + def testAmbiguousNegativeUTCOffset(self): + self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30), + 'America/New_York', True) + + def testAmbiguousPositiveUTCOffset(self): + self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30), + 'Australia/Sydney', True) + + def testUnambiguousNegativeUTCOffset(self): + self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30), + 'America/New_York', False) + + def testUnambiguousPositiveUTCOffset(self): + self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30), + 'Australia/Sydney', False) + + def testUnambiguousGapNegativeUTCOffset(self): + # Imaginary time + self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30), + 'America/New_York', False) + + def testUnambiguousGapPositiveUTCOffset(self): + # Imaginary time + self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30), + 'Australia/Sydney', False) + + def _test_imaginary_time(self, dt, tzid, exists): + tzname = self._get_tzname(tzid) + with self._gettz_context(tzname): + tzi = self.gettz(tzname) + + self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists) + + def testImaginaryNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2011, 3, 13, 2, 30), + 'America/New_York', False) + + def testNotImaginaryNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2011, 3, 13, 1, 30), + 'America/New_York', True) + + def testImaginaryPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 10, 7, 2, 30), + 'Australia/Sydney', False) + + def testNotImaginaryPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 10, 7, 1, 30), + 'Australia/Sydney', True) + + def testNotImaginaryFoldNegativeUTCOffset(self): + self._test_imaginary_time(datetime(2015, 11, 1, 1, 30), + 'America/New_York', True) + + def testNotImaginaryFoldPositiveUTCOffset(self): + self._test_imaginary_time(datetime(2012, 4, 1, 3, 30), + 'Australia/Sydney', True) + + @unittest.skip("Known failure in Python 3.6.") + def testEqualAmbiguousComparison(self): + tzname = self._get_tzname('Australia/Sydney') + + with self._gettz_context(tzname): + SYD0 = self.gettz(tzname) + SYD1 = self.gettz(tzname) + + t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.tzutc()) # AEST + + t0_syd0 = t0_u.astimezone(SYD0) + t0_syd1 = t0_u.astimezone(SYD1) + + # This is considered an "inter-zone comparison" because it's an + # ambiguous datetime. + self.assertEqual(t0_syd0, t0_syd1) + + +class TzWinFoldMixin(object): + def get_args(self, tzname): + return (tzname, ) + + class context(object): + def __init__(*args, **kwargs): + pass + + def __enter__(*args, **kwargs): + pass + + def __exit__(*args, **kwargs): + pass + + def get_utc_transitions(self, tzi, year, gap): + dston, dstoff = tzi.transitions(year) + if gap: + t_n = dston - timedelta(minutes=30) + + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t1_u = t0_u + timedelta(hours=1) + else: + # Get 1 hour before the first ambiguous date + t_n = dstoff - timedelta(minutes=30) + + t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc()) + t_n += timedelta(hours=1) # Naive ambiguous date + t0_u = t0_u + timedelta(hours=1) # First ambiguous date + t1_u = t0_u + timedelta(hours=1) # Second ambiguous date + + return t_n, t0_u, t1_u + + def testFoldPositiveUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = 'AUS Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + # Calling fromutc() alters the tzfile object + SYD = self.tzclass(*args) + + # Get the transition time in UTC from the object, because + # Windows doesn't store historical info + t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False) + + # Using fresh tzfiles + t0_syd = t0_u.astimezone(SYD) + t1_syd = t1_u.astimezone(SYD) + + self.assertEqual(t0_syd.replace(tzinfo=None), t_n) + + self.assertEqual(t1_syd.replace(tzinfo=None), t_n) + + self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11)) + self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10)) + self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname()) + + def testGapPositiveUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = 'AUS Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + SYD = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True) + + t0 = t0_u.astimezone(SYD) + t1 = t1_u.astimezone(SYD) + + self.assertEqual(t0.replace(tzinfo=None), t_n) + + self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2)) + + self.assertEqual(t0.utcoffset(), timedelta(hours=10)) + self.assertEqual(t1.utcoffset(), timedelta(hours=11)) + + def testFoldNegativeUTCOffset(self): + # Test that we can resolve ambiguous times + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + TOR = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False) + + t0_tor = t0_u.astimezone(TOR) + t1_tor = t1_u.astimezone(TOR) + + self.assertEqual(t0_tor.replace(tzinfo=None), t_n) + self.assertEqual(t1_tor.replace(tzinfo=None), t_n) + + self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname()) + self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0)) + self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0)) + + def testGapNegativeUTCOffset(self): + # Test that we don't have a problem around gaps. + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + TOR = self.tzclass(*args) + + t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True) + + t0 = t0_u.astimezone(TOR) + t1 = t1_u.astimezone(TOR) + + self.assertEqual(t0.replace(tzinfo=None), + t_n) + + self.assertEqual(t1.replace(tzinfo=None), + t_n + timedelta(hours=2)) + + self.assertNotEqual(t0.tzname(), t1.tzname()) + self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0)) + self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0)) + + def testFoldIndependence(self): + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + NYC = self.tzclass(*args) + UTC = tz.tzutc() + hour = timedelta(hours=1) + + # Firmly 2015-11-01 0:30 EDT-4 + t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False) + + pre_dst = (t_n - hour).replace(tzinfo=NYC) + + # Currently, there's no way around the fact that this resolves to an + # ambiguous date, which defaults to EST. I'm not hard-coding in the + # answer, though, because the preferred behavior would be that this + # results in a time on the EDT side. + + # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5 + in_dst = pre_dst + hour + in_dst_tzname_0 = in_dst.tzname() # Stash the tzname - EDT + + # Doing the arithmetic in UTC creates a date that is unambiguously + # 2015-11-01 1:30 EDT-5 + in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC) + + # Make sure we got the right folding behavior + self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0) + + # Now check to make sure in_dst's tzname hasn't changed + self.assertEqual(in_dst_tzname_0, in_dst.tzname()) + + def testInZoneFoldEquality(self): + # Two datetimes in the same zone are considered to be equal if their + # wall times are equal, even if they have different absolute times. + tzname = 'Eastern Standard Time' + args = self.get_args(tzname) + + with self.context(tzname): + NYC = self.tzclass(*args) + UTC = tz.tzutc() + + t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False) + + dt0 = t_n.replace(tzinfo=NYC) + dt1 = tz.enfold(dt0, fold=1) + + # Make sure these actually represent different times + self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC)) + + # Test that they compare equal + self.assertEqual(dt0, dt1) + +### +# Test Cases +class TzUTCTest(unittest.TestCase): + def testSingleton(self): + UTC_0 = tz.tzutc() + UTC_1 = tz.tzutc() + + self.assertIs(UTC_0, UTC_1) + + def testOffset(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + + self.assertEqual(ct.utcoffset(), timedelta(seconds=0)) + + def testDst(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + + self.assertEqual(ct.dst(), timedelta(seconds=0)) + + def testTzName(self): + ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc()) + self.assertEqual(ct.tzname(), 'UTC') + + def testEquality(self): + UTC0 = tz.tzutc() + UTC1 = tz.tzutc() + + self.assertEqual(UTC0, UTC1) + + def testInequality(self): + UTC = tz.tzutc() + UTCp4 = tz.tzoffset('UTC+4', 14400) + + self.assertNotEqual(UTC, UTCp4) + + def testInequalityInteger(self): + self.assertFalse(tz.tzutc() == 7) + self.assertNotEqual(tz.tzutc(), 7) + + def testInequalityUnsupported(self): + self.assertEqual(tz.tzutc(), ComparesEqual) + + def testRepr(self): + UTC = tz.tzutc() + self.assertEqual(repr(UTC), 'tzutc()') + + def testTimeOnlyUTC(self): + # https://github.com/dateutil/dateutil/issues/132 + # tzutc doesn't care + tz_utc = tz.tzutc() + self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(), + timedelta(0)) + + def testAmbiguity(self): + # Pick an arbitrary datetime, this should always return False. + dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc()) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + +@pytest.mark.tzoffset +class TzOffsetTest(unittest.TestCase): + def testTimedeltaOffset(self): + est = tz.tzoffset('EST', timedelta(hours=-5)) + est_s = tz.tzoffset('EST', -18000) + + self.assertEqual(est, est_s) + + def testTzNameNone(self): + gmt5 = tz.tzoffset(None, -18000) # -5:00 + self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(), + None) + + def testTimeOnlyOffset(self): + # tzoffset doesn't care + tz_offset = tz.tzoffset('+3', 3600) + self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(), + timedelta(seconds=3600)) + + def testTzOffsetRepr(self): + tname = 'EST' + tzo = tz.tzoffset(tname, -5 * 3600) + self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)") + + def testEquality(self): + utc = tz.tzoffset('UTC', 0) + gmt = tz.tzoffset('GMT', 0) + + self.assertEqual(utc, gmt) + + def testUTCEquality(self): + utc = tz.tzutc() + o_utc = tz.tzoffset('UTC', 0) + + self.assertEqual(utc, o_utc) + self.assertEqual(o_utc, utc) + + def testInequalityInvalid(self): + tzo = tz.tzoffset('-3', -3 * 3600) + self.assertFalse(tzo == -3) + self.assertNotEqual(tzo, -3) + + def testInequalityUnsupported(self): + tzo = tz.tzoffset('-5', -5 * 3600) + + self.assertTrue(tzo == ComparesEqual) + self.assertFalse(tzo != ComparesEqual) + self.assertEqual(tzo, ComparesEqual) + + def testAmbiguity(self): + # Pick an arbitrary datetime, this should always return False. + dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600)) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + def testTzOffsetInstance(self): + tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5)) + + assert tz1 is not tz2 + + def testTzOffsetSingletonDifferent(self): + tz1 = tz.tzoffset('EST', timedelta(hours=-5)) + tz2 = tz.tzoffset('EST', -18000) + + assert tz1 is tz2 + +@pytest.mark.tzoffset +@pytest.mark.parametrize('args', [ + ('UTC', 0), + ('EST', -18000), + ('EST', timedelta(hours=-5)), + (None, timedelta(hours=3)), +]) +def test_tzoffset_singleton(args): + tz1 = tz.tzoffset(*args) + tz2 = tz.tzoffset(*args) + + assert tz1 is tz2 + +@pytest.mark.tzlocal +class TzLocalTest(unittest.TestCase): + def testEquality(self): + tz1 = tz.tzlocal() + tz2 = tz.tzlocal() + + # Explicitly calling == and != here to ensure the operators work + self.assertTrue(tz1 == tz2) + self.assertFalse(tz1 != tz2) + + def testInequalityFixedOffset(self): + tzl = tz.tzlocal() + tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds()) + tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds()) + + self.assertFalse(tzl == tzos) + self.assertFalse(tzl == tzod) + self.assertTrue(tzl != tzos) + self.assertTrue(tzl != tzod) + + def testInequalityInvalid(self): + tzl = tz.tzlocal() + + self.assertTrue(tzl != 1) + self.assertFalse(tzl == 1) + + # TODO: Use some sort of universal local mocking so that it's clear + # that we're expecting tzlocal to *not* be Pacific/Kiritimati + LINT = tz.gettz('Pacific/Kiritimati') + self.assertTrue(tzl != LINT) + self.assertFalse(tzl == LINT) + + def testInequalityUnsupported(self): + tzl = tz.tzlocal() + + self.assertTrue(tzl == ComparesEqual) + self.assertFalse(tzl != ComparesEqual) + + def testRepr(self): + tzl = tz.tzlocal() + + self.assertEqual(repr(tzl), 'tzlocal()') + + +@pytest.mark.parametrize('args,kwargs', [ + (('EST', -18000), {}), + (('EST', timedelta(hours=-5)), {}), + (('EST',), {'offset': -18000}), + (('EST',), {'offset': timedelta(hours=-5)}), + (tuple(), {'name': 'EST', 'offset': -18000}) +]) +def test_tzoffset_is(args, kwargs): + tz_ref = tz.tzoffset('EST', -18000) + assert tz.tzoffset(*args, **kwargs) is tz_ref + + +def test_tzoffset_is_not(): + assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000) + + +@pytest.mark.tzlocal +@unittest.skipIf(IS_WIN, "requires Unix") +@unittest.skipUnless(TZEnvContext.tz_change_allowed(), + TZEnvContext.tz_change_disallowed_message()) +class TzLocalNixTest(unittest.TestCase, TzFoldMixin): + # This is a set of tests for `tzlocal()` on *nix systems + + # POSIX string indicating change to summer time on the 2nd Sunday in March + # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007) + TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2' + + # POSIX string for AEST/AEDT (valid >= 2008) + TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + + # POSIX string for BST/GMT + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + + # POSIX string for UTC + UTC = 'UTC' + + def gettz(self, tzname): + # Actual time zone changes are handled by the _gettz_context function + return tz.tzlocal() + + def _gettz_context(self, tzname): + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return TZEnvContext(tzname_map.get(tzname, tzname)) + + def _testTzFunc(self, tzval, func, std_val, dst_val): + """ + This generates tests about how the behavior of a function ``func`` + changes between STD and DST (e.g. utcoffset, tzname, dst). + + It assume that DST starts the 2nd Sunday in March and ends the 1st + Sunday in November + """ + with TZEnvContext(tzval): + dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal()) # STD + dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal()) # DST + + self.assertEqual(func(dt1), std_val) + self.assertEqual(func(dt2), dst_val) + + def _testTzName(self, tzval, std_name, dst_name): + func = datetime.tzname + + self._testTzFunc(tzval, func, std_name, dst_name) + + def testTzNameDST(self): + # Test tzname in a zone with DST + self._testTzName(self.TZ_EST, 'EST', 'EDT') + + def testTzNameUTC(self): + # Test tzname in a zone without DST + self._testTzName(self.UTC, 'UTC', 'UTC') + + def _testOffset(self, tzval, std_off, dst_off): + func = datetime.utcoffset + + self._testTzFunc(tzval, func, std_off, dst_off) + + def testOffsetDST(self): + self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4)) + + def testOffsetUTC(self): + self._testOffset(self.UTC, timedelta(0), timedelta(0)) + + def _testDST(self, tzval, dst_dst): + func = datetime.dst + std_dst = timedelta(0) + + self._testTzFunc(tzval, func, std_dst, dst_dst) + + def testDSTDST(self): + self._testDST(self.TZ_EST, timedelta(hours=1)) + + def testDSTUTC(self): + self._testDST(self.UTC, timedelta(0)) + + def testTimeOnlyOffsetLocalUTC(self): + with TZEnvContext(self.UTC): + self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(), + timedelta(0)) + + def testTimeOnlyOffsetLocalDST(self): + with TZEnvContext(self.TZ_EST): + self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(), + None) + + def testTimeOnlyDSTLocalUTC(self): + with TZEnvContext(self.UTC): + self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(), + timedelta(0)) + + def testTimeOnlyDSTLocalDST(self): + with TZEnvContext(self.TZ_EST): + self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(), + None) + + def testUTCEquality(self): + with TZEnvContext(self.UTC): + assert tz.tzlocal() == tz.tzutc() + + +# TODO: Maybe a better hack than this? +def mark_tzlocal_nix(f): + marks = [ + pytest.mark.tzlocal, + pytest.mark.skipif(IS_WIN, reason='requires Unix'), + pytest.mark.skipif(not TZEnvContext.tz_change_allowed, + reason=TZEnvContext.tz_change_disallowed_message()) + ] + + for mark in reversed(marks): + f = mark(f) + + return f + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0']) +def test_tzlocal_utc_equal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() == tz.UTC + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar', [ + 'Europe/London', 'America/New_York', + 'GMT0BST', 'EST5EDT']) +def test_tzlocal_utc_unequal(tzvar): + with TZEnvContext(tzvar): + assert tz.tzlocal() != tz.UTC + + +@mark_tzlocal_nix +def test_tzlocal_local_time_trim_colon(): + with TZEnvContext(':/etc/localtime'): + assert tz.gettz() is not None + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5', tz.tzoffset('EST', -18000)), + ('GMT', tz.tzoffset('GMT', 0)), + ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))), + ('JST-9', tz.tzoffset('JST', timedelta(hours=9))), +]) +def test_tzlocal_offset_equal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() == tzoff + assert not (tz.tzlocal() != tzoff) + + +@mark_tzlocal_nix +@pytest.mark.parametrize('tzvar, tzoff', [ + ('EST5EDT', tz.tzoffset('EST', -18000)), + ('GMT0BST', tz.tzoffset('GMT', 0)), + ('EST5', tz.tzoffset('EST', -14400)), + ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))), + ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))), +]) +def test_tzlocal_offset_unequal(tzvar, tzoff): + with TZEnvContext(tzvar): + # Including both to test both __eq__ and __ne__ + assert tz.tzlocal() != tzoff + assert not (tz.tzlocal() == tzoff) + + +@pytest.mark.gettz +class GettzTest(unittest.TestCase, TzFoldMixin): + gettz = staticmethod(tz.gettz) + + def testGettz(self): + # bug 892569 + str(self.gettz('UTC')) + + def testGetTzEquality(self): + self.assertEqual(self.gettz('UTC'), self.gettz('UTC')) + + def testTimeOnlyGettz(self): + # gettz returns None + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None) + + def testTimeOnlyGettzDST(self): + # gettz returns None + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None) + + def testTimeOnlyGettzTzName(self): + tz_get = self.gettz('Europe/Minsk') + self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None) + + def testTimeOnlyFormatZ(self): + tz_get = self.gettz('Europe/Minsk') + t = dt_time(13, 20, tzinfo=tz_get) + + self.assertEqual(t.strftime('%H%M%Z'), '1320') + + def testPortugalDST(self): + # In 1996, Portugal changed from CET to WET + PORTUGAL = self.gettz('Portugal') + + t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL) + + self.assertEqual(t_cet.tzname(), 'CET') + self.assertEqual(t_cet.utcoffset(), timedelta(hours=1)) + self.assertEqual(t_cet.dst(), timedelta(0)) + + t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL) + + self.assertEqual(t_west.tzname(), 'WEST') + self.assertEqual(t_west.utcoffset(), timedelta(hours=1)) + self.assertEqual(t_west.dst(), timedelta(hours=1)) + + def testGettzCacheTzFile(self): + NYC1 = tz.gettz('America/New_York') + NYC2 = tz.gettz('America/New_York') + + assert NYC1 is NYC2 + + def testGettzCacheTzLocal(self): + local1 = tz.gettz() + local2 = tz.gettz() + + assert local1 is not local2 + +@pytest.mark.gettz +@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached') +def test_gettz_cache_clear(): + NYC1 = tz.gettz('America/New_York') + tz.gettz.cache_clear() + + NYC2 = tz.gettz('America/New_York') + + assert NYC1 is not NYC2 + + +class ZoneInfoGettzTest(GettzTest, WarningTestMixin): + def gettz(self, name): + zoneinfo_file = zoneinfo.get_zonefile_instance() + return zoneinfo_file.get(name) + + def testZoneInfoFileStart1(self): + tz = self.gettz("EST5EDT") + self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST", + MISSING_TARBALL) + self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT") + + def testZoneInfoFileEnd1(self): + tzc = self.gettz("EST5EDT") + self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), + "EDT", MISSING_TARBALL) + + end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1) + self.assertEqual(end_est.tzname(), "EST") + + def testZoneInfoOffsetSignal(self): + utc = self.gettz("UTC") + nyc = self.gettz("America/New_York") + self.assertNotEqual(utc, None, MISSING_TARBALL) + self.assertNotEqual(nyc, None) + t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc) + t1 = t0.astimezone(utc) + t2 = t1.astimezone(nyc) + self.assertEqual(t0, t2) + self.assertEqual(nyc.dst(t0), timedelta(hours=1)) + + def testZoneInfoCopy(self): + # copy.copy() called on a ZoneInfo file was returning the same instance + CHI = self.gettz('America/Chicago') + CHI_COPY = copy.copy(CHI) + + self.assertIsNot(CHI, CHI_COPY) + self.assertEqual(CHI, CHI_COPY) + + def testZoneInfoDeepCopy(self): + CHI = self.gettz('America/Chicago') + CHI_COPY = copy.deepcopy(CHI) + + self.assertIsNot(CHI, CHI_COPY) + self.assertEqual(CHI, CHI_COPY) + + def testZoneInfoInstanceCaching(self): + zif_0 = zoneinfo.get_zonefile_instance() + zif_1 = zoneinfo.get_zonefile_instance() + + self.assertIs(zif_0, zif_1) + + def testZoneInfoNewInstance(self): + zif_0 = zoneinfo.get_zonefile_instance() + zif_1 = zoneinfo.get_zonefile_instance(new_instance=True) + zif_2 = zoneinfo.get_zonefile_instance() + + self.assertIsNot(zif_0, zif_1) + self.assertIs(zif_1, zif_2) + + def testZoneInfoDeprecated(self): + with self.assertWarns(DeprecationWarning): + zoneinfo.gettz('US/Eastern') + + def testZoneInfoMetadataDeprecated(self): + with self.assertWarns(DeprecationWarning): + zoneinfo.gettz_db_metadata() + + +class TZRangeTest(unittest.TestCase, TzFoldMixin): + TZ_EST = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4), + start=relativedelta(month=3, day=1, hour=2, + weekday=SU(+2)), + end=relativedelta(month=11, day=1, hour=1, + weekday=SU(+1))) + + TZ_AEST = tz.tzrange('AEST', timedelta(hours=10), + 'AEDT', timedelta(hours=11), + start=relativedelta(month=10, day=1, hour=2, + weekday=SU(+1)), + end=relativedelta(month=4, day=1, hour=2, + weekday=SU(+1))) + + TZ_LON = tz.tzrange('GMT', timedelta(hours=0), + 'BST', timedelta(hours=1), + start=relativedelta(month=3, day=31, weekday=SU(-1), + hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), + hours=1)) + # POSIX string for UTC + UTC = 'UTC' + + def gettz(self, tzname): + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return tzname_map[tzname] + + def testRangeCmp1(self): + self.assertEqual(tz.tzstr("EST5EDT"), + tz.tzrange("EST", -18000, "EDT", -14400, + relativedelta(hours=+2, + month=4, day=1, + weekday=SU(+1)), + relativedelta(hours=+1, + month=10, day=31, + weekday=SU(-1)))) + + def testRangeCmp2(self): + self.assertEqual(tz.tzstr("EST5EDT"), + tz.tzrange("EST", -18000, "EDT")) + + def testRangeOffsets(self): + TZR = tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(hours=2, month=4, day=1, + weekday=SU(+2)), + end=relativedelta(hours=1, month=10, day=31, + weekday=SU(-1))) + + dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR) # STD + dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR) # DST + + dst_zero = timedelta(0) + dst_hour = timedelta(hours=1) + + std_offset = timedelta(hours=-5) + dst_offset = timedelta(hours=-4) + + # Check dst() + self.assertEqual(dt_std.dst(), dst_zero) + self.assertEqual(dt_dst.dst(), dst_hour) + + # Check utcoffset() + self.assertEqual(dt_std.utcoffset(), std_offset) + self.assertEqual(dt_dst.utcoffset(), dst_offset) + + # Check tzname + self.assertEqual(dt_std.tzname(), 'EST') + self.assertEqual(dt_dst.tzname(), 'EDT') + + def testTimeOnlyRangeFixed(self): + # This is a fixed-offset zone, so tzrange allows this + tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3)) + self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(), + timedelta(hours=-3)) + + def testTimeOnlyRange(self): + # tzrange returns None because this zone has DST + tz_range = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4)) + self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None) + + def testBrokenIsDstHandling(self): + # tzrange._isdst() was using a date() rather than a datetime(). + # Issue reported by Lennart Regebro. + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")), + datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) + + def testRangeTimeDelta(self): + # Test that tzrange can be specified with a timedelta instead of an int. + EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5), + 'EDT', timedelta(hours=-4)) + + EST5EDT_sec = tz.tzrange('EST', -18000, + 'EDT', -14400) + + self.assertEqual(EST5EDT_td, EST5EDT_sec) + + def testRangeEquality(self): + TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400) + + # Standard abbreviation different + TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400) + self.assertNotEqual(TZR1, TZR2) + + # DST abbreviation different + TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400) + self.assertNotEqual(TZR1, TZR3) + + # STD offset different + TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400) + self.assertNotEqual(TZR1, TZR4) + + # DST offset different + TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000) + self.assertNotEqual(TZR1, TZR5) + + # Start delta different + TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(hours=+1, month=3, + day=1, weekday=SU(+2))) + self.assertNotEqual(TZR1, TZR6) + + # End delta different + TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400, + end=relativedelta(hours=+1, month=11, + day=1, weekday=SU(+2))) + self.assertNotEqual(TZR1, TZR7) + + def testRangeInequalityUnsupported(self): + TZR = tz.tzrange('EST', -18000, 'EDT', -14400) + + self.assertFalse(TZR == 4) + self.assertTrue(TZR == ComparesEqual) + self.assertFalse(TZR != ComparesEqual) + + +@pytest.mark.tzstr +class TZStrTest(unittest.TestCase, TzFoldMixin): + # POSIX string indicating change to summer time on the 2nd Sunday in March + # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007) + TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2' + + # POSIX string for AEST/AEDT (valid >= 2008) + TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3' + + # POSIX string for GMT/BST + TZ_LON = 'GMT0BST,M3.5.0,M10.5.0' + + def gettz(self, tzname): + # Actual time zone changes are handled by the _gettz_context function + tzname_map = {'Australia/Sydney': self.TZ_AEST, + 'America/Toronto': self.TZ_EST, + 'America/New_York': self.TZ_EST, + 'Europe/London': self.TZ_LON} + + return tz.tzstr(tzname_map[tzname]) + + def testStrStr(self): + # Test that tz.tzstr() won't throw an error if given a str instead + # of a unicode literal. + self.assertEqual(datetime(2003, 4, 6, 1, 59, + tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST") + self.assertEqual(datetime(2003, 4, 6, 2, 00, + tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT") + + def testStrInequality(self): + TZS1 = tz.tzstr('EST5EDT4') + + # Standard abbreviation different + TZS2 = tz.tzstr('ET5EDT4') + self.assertNotEqual(TZS1, TZS2) + + # DST abbreviation different + TZS3 = tz.tzstr('EST5EMT') + self.assertNotEqual(TZS1, TZS3) + + # STD offset different + TZS4 = tz.tzstr('EST4EDT4') + self.assertNotEqual(TZS1, TZS4) + + # DST offset different + TZS5 = tz.tzstr('EST5EDT3') + self.assertNotEqual(TZS1, TZS5) + + def testStrInequalityStartEnd(self): + TZS1 = tz.tzstr('EST5EDT4') + + # Start delta different + TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00') + self.assertNotEqual(TZS1, TZS2) + + # End delta different + TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00') + self.assertNotEqual(TZS1, TZS3) + + def testPosixOffset(self): + TZ1 = tz.tzstr('UTC-3') + self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(), + timedelta(hours=-3)) + + TZ2 = tz.tzstr('UTC-3', posix_offset=True) + self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(), + timedelta(hours=+3)) + + def testStrInequalityUnsupported(self): + TZS = tz.tzstr('EST5EDT') + + self.assertFalse(TZS == 4) + self.assertTrue(TZS == ComparesEqual) + self.assertFalse(TZS != ComparesEqual) + + def testTzStrRepr(self): + TZS1 = tz.tzstr('EST5EDT4') + TZS2 = tz.tzstr('EST') + + self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")") + self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")") + + def testTzStrFailure(self): + with self.assertRaises(ValueError): + tz.tzstr('InvalidString;439999') + + def testTzStrSingleton(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr('CST4CST') + tz3 = tz.tzstr('EST5EDT') + + self.assertIsNot(tz1, tz2) + self.assertIs(tz1, tz3) + + def testTzStrSingletonPosix(self): + tz_t1 = tz.tzstr('GMT+3', posix_offset=True) + tz_f1 = tz.tzstr('GMT+3', posix_offset=False) + + tz_t2 = tz.tzstr('GMT+3', posix_offset=True) + tz_f2 = tz.tzstr('GMT+3', posix_offset=False) + + self.assertIs(tz_t1, tz_t2) + self.assertIsNot(tz_t1, tz_f1) + + self.assertIs(tz_f1, tz_f2) + + def testTzStrInstance(self): + tz1 = tz.tzstr('EST5EDT') + tz2 = tz.tzstr.instance('EST5EDT') + tz3 = tz.tzstr.instance('EST5EDT') + + assert tz1 is not tz2 + assert tz2 is not tz3 + + # Ensure that these still are all the same zone + assert tz1 == tz2 == tz3 + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str,expected', [ + # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + ('', tz.tzrange(None)), # TODO: Should change this so tz.tzrange('') works + ('EST+5EDT,M3.2.0/2,M11.1.0/12', + tz.tzrange('EST', -18000, 'EDT', -14400, + start=relativedelta(month=3, day=1, weekday=SU(2), hours=2), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))), + ('WART4WARST,J1/0,J365/25', # This is DST all year, Western Argentina Summer Time + tz.tzrange('WART', timedelta(hours=-4), 'WARST', + start=relativedelta(month=1, day=1, hours=0), + end=relativedelta(month=12, day=31, days=1))), + ('IST-2IDT,M3.4.4/26,M10.5.0', # Israel Standard / Daylight Time + tz.tzrange('IST', timedelta(hours=2), 'IDT', + start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))), + ('WGT3WGST,M3.5.0/2,M10.5.0/1', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST', + start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2), + end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))), + + # Different offset specifications + ('WGT0300WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('WGT03:00WGST', + tz.tzrange('WGT', timedelta(hours=-3), 'WGST')), + ('AEST-1100AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + ('AEST-11:00AEDT', + tz.tzrange('AEST', timedelta(hours=11), 'AEDT')), + + # Different time formats + ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), + ('EST5EDT,M3.2.0/0400,M11.1.0/0300', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=3, day=1, weekday=SU(2), hours=4), + end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))), +]) +def test_valid_GNU_tzstr(tz_str, expected): + tzi = tz.tzstr(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str, expected', [ + ('EST5EDT,5,4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2), + end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))), + ('EST5EDT,5,-4,0,7200,11,3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)), + end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3), + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))), + ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600', + tz.tzrange('EST', timedelta(hours=-5), 'EDT', + start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)), + end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))), +]) +def test_valid_dateutil_format(tz_str, expected): + # This tests the dateutil-specific format that is used widely in the tests + # and examples. It is unclear where this format originated from. + with pytest.warns(tz.DeprecatedTzFormatWarning): + tzi = tz.tzstr.instance(tz_str) + + assert tzi == expected + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', [ + 'hdfiughdfuig,dfughdfuigpu87ñ::', + ',dfughdfuigpu87ñ::', + '-1:WART4WARST,J1,J365/25', + 'WART4WARST,J1,J365/-25', + 'IST-2IDT,M3.4.-1/26,M10.5.0', + 'IST-2IDT,M3,2000,1/26,M10,5,0' +]) +def test_invalid_GNU_tzstr(tz_str): + with pytest.raises(ValueError): + tz.tzstr(tz_str) + + +# Different representations of the same default rule set +DEFAULT_TZSTR_RULES_EQUIV_2003 = [ + 'EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00', + 'EST5EDT4,95/02:00:00,298/02:00', + 'EST5EDT4,J96/02:00:00,J299/02:00', + 'EST5EDT4,J96/02:00:00,J299/02' +] + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_start(tz_str): + tzi = tz.tzstr(tz_str) + dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi) + dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_std) == EST_TUPLE + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003) +def test_tzstr_default_end(tz_str): + tzi = tz.tzstr(tz_str) + dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi) + dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi) + dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1) + dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi) + + assert get_timezone_tuple(dt_dst) == EDT_TUPLE + assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE + assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE + assert get_timezone_tuple(dt_std) == EST_TUPLE + + +@pytest.mark.tzstr +@pytest.mark.parametrize('tzstr_1', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +@pytest.mark.parametrize('tzstr_2', ['EST5EDT', + 'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00']) +def test_tzstr_default_cmp(tzstr_1, tzstr_2): + tz1 = tz.tzstr(tzstr_1) + tz2 = tz.tzstr(tzstr_2) + + assert tz1 == tz2 + +class TZICalTest(unittest.TestCase, TzFoldMixin): + def _gettz_str_tuple(self, tzname): + TZ_EST = ( + 'BEGIN:VTIMEZONE', + 'TZID:US-Eastern', + 'BEGIN:STANDARD', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11', + 'TZOFFSETFROM:-0400', + 'TZOFFSETTO:-0500', + 'TZNAME:EST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19980301T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03', + 'TZOFFSETFROM:-0500', + 'TZOFFSETTO:-0400', + 'TZNAME:EDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_PST = ( + 'BEGIN:VTIMEZONE', + 'TZID:US-Pacific', + 'BEGIN:STANDARD', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11', + 'TZOFFSETFROM:-0700', + 'TZOFFSETTO:-0800', + 'TZNAME:PST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19980301T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03', + 'TZOFFSETFROM:-0800', + 'TZOFFSETTO:-0700', + 'TZNAME:PDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_AEST = ( + 'BEGIN:VTIMEZONE', + 'TZID:Australia-Sydney', + 'BEGIN:STANDARD', + 'DTSTART:19980301T030000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04', + 'TZOFFSETFROM:+1100', + 'TZOFFSETTO:+1000', + 'TZNAME:AEST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19971029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10', + 'TZOFFSETFROM:+1000', + 'TZOFFSETTO:+1100', + 'TZNAME:AEDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + TZ_LON = ( + 'BEGIN:VTIMEZONE', + 'TZID:Europe-London', + 'BEGIN:STANDARD', + 'DTSTART:19810301T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'TZNAME:GMT', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19961001T030000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01', + 'TZOFFSETFROM:+0000', + 'TZOFFSETTO:+0100', + 'TZNAME:BST', + 'END:DAYLIGHT', + 'END:VTIMEZONE' + ) + + tzname_map = {'Australia/Sydney': TZ_AEST, + 'America/Toronto': TZ_EST, + 'America/New_York': TZ_EST, + 'America/Los_Angeles': TZ_PST, + 'Europe/London': TZ_LON} + + return tzname_map[tzname] + + def _gettz_str(self, tzname): + return '\n'.join(self._gettz_str_tuple(tzname)) + + def _tzstr_dtstart_with_params(self, tzname, param_str): + # Adds parameters to the DTSTART values of a given tzstr + tz_str_tuple = self._gettz_str_tuple(tzname) + + out_tz = [] + for line in tz_str_tuple: + if line.startswith('DTSTART'): + name, value = line.split(':', 1) + line = name + ';' + param_str + ':' + value + + out_tz.append(line) + + return '\n'.join(out_tz) + + def gettz(self, tzname): + tz_str = self._gettz_str(tzname) + + tzc = tz.tzical(StringIO(tz_str)).get() + + return tzc + + def testRepr(self): + instr = StringIO(TZICAL_PST8PDT) + instr.name = 'StringIO(PST8PDT)' + tzc = tz.tzical(instr) + + self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")") + + # Test performance + def _test_us_zone(self, tzc, func, values, start): + if start: + dt1 = datetime(2003, 3, 9, 1, 59) + dt2 = datetime(2003, 3, 9, 2, 00) + fold = [0, 0] + else: + dt1 = datetime(2003, 11, 2, 0, 59) + dt2 = datetime(2003, 11, 2, 1, 00) + fold = [0, 1] + + dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f) + for dt, f in zip((dt1, dt2), fold)) + + for value, dt in zip(values, dts): + self.assertEqual(func(dt), value) + + def _test_multi_zones(self, tzstrs, tzids, func, values, start): + tzic = tz.tzical(StringIO('\n'.join(tzstrs))) + for tzid, vals in zip(tzids, values): + tzc = tzic.get(tzid) + + self._test_us_zone(tzc, func, vals, start) + + def _prepare_EST(self): + tz_str = self._gettz_str('America/New_York') + return tz.tzical(StringIO(tz_str)).get() + + def _testEST(self, start, test_type, tzc=None): + if tzc is None: + tzc = self._prepare_EST() + + argdict = { + 'name': (datetime.tzname, ('EST', 'EDT')), + 'offset': (datetime.utcoffset, (timedelta(hours=-5), + timedelta(hours=-4))), + 'dst': (datetime.dst, (timedelta(hours=0), + timedelta(hours=1))) + } + + func, values = argdict[test_type] + + if not start: + values = reversed(values) + + self._test_us_zone(tzc, func, values, start=start) + + def testESTStartName(self): + self._testEST(start=True, test_type='name') + + def testESTEndName(self): + self._testEST(start=False, test_type='name') + + def testESTStartOffset(self): + self._testEST(start=True, test_type='offset') + + def testESTEndOffset(self): + self._testEST(start=False, test_type='offset') + + def testESTStartDST(self): + self._testEST(start=True, test_type='dst') + + def testESTEndDST(self): + self._testEST(start=False, test_type='dst') + + def testESTValueDatetime(self): + # Violating one-test-per-test rule because we're not set up to do + # parameterized tests and the manual proliferation is getting a bit + # out of hand. + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE-TIME') + + tzc = tz.tzical(StringIO(tz_str)).get() + + for start in (True, False): + for test_type in ('name', 'offset', 'dst'): + self._testEST(start=start, test_type=test_type, tzc=tzc) + + def _testMultizone(self, start, test_type): + tzstrs = (self._gettz_str('America/New_York'), + self._gettz_str('America/Los_Angeles')) + tzids = ('US-Eastern', 'US-Pacific') + + argdict = { + 'name': (datetime.tzname, (('EST', 'EDT'), + ('PST', 'PDT'))), + 'offset': (datetime.utcoffset, ((timedelta(hours=-5), + timedelta(hours=-4)), + (timedelta(hours=-8), + timedelta(hours=-7)))), + 'dst': (datetime.dst, ((timedelta(hours=0), + timedelta(hours=1)), + (timedelta(hours=0), + timedelta(hours=1)))) + } + + func, values = argdict[test_type] + + if not start: + values = map(reversed, values) + + self._test_multi_zones(tzstrs, tzids, func, values, start) + + def testMultiZoneStartName(self): + self._testMultizone(start=True, test_type='name') + + def testMultiZoneEndName(self): + self._testMultizone(start=False, test_type='name') + + def testMultiZoneStartOffset(self): + self._testMultizone(start=True, test_type='offset') + + def testMultiZoneEndOffset(self): + self._testMultizone(start=False, test_type='offset') + + def testMultiZoneStartDST(self): + self._testMultizone(start=True, test_type='dst') + + def testMultiZoneEndDST(self): + self._testMultizone(start=False, test_type='dst') + + def testMultiZoneKeys(self): + est_str = self._gettz_str('America/New_York') + pst_str = self._gettz_str('America/Los_Angeles') + tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str)))) + + # Sort keys because they are in a random order, being dictionary keys + keys = sorted(tzic.keys()) + + self.assertEqual(keys, ['US-Eastern', 'US-Pacific']) + + # Test error conditions + def testEmptyString(self): + with self.assertRaises(ValueError): + tz.tzical(StringIO("")) + + def testMultiZoneGet(self): + tzic = tz.tzical(StringIO(TZICAL_EST5EDT + TZICAL_PST8PDT)) + + with self.assertRaises(ValueError): + tzic.get() + + def testDtstartDate(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'VALUE=DATE') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartTzid(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'TZID=UTC') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + def testDtstartBadParam(self): + tz_str = self._tzstr_dtstart_with_params('America/New_York', + 'FOO=BAR') + with self.assertRaises(ValueError): + tz.tzical(StringIO(tz_str)) + + # Test Parsing + def testGap(self): + tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT)))) + + keys = sorted(tzic.keys()) + self.assertEqual(keys, ['US-Eastern', 'US-Pacific']) + + +class TZTest(unittest.TestCase): + def testFileStart1(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST") + self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT") + + def testFileEnd1(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), + "EDT") + end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc)) + self.assertEqual(end_est.tzname(), "EST") + + def testFileLastTransition(self): + # After the last transition, it goes to standard time in perpetuity + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertEqual(datetime(2037, 10, 25, 0, 59, tzinfo=tzc).tzname(), + "EDT") + + last_date = tz.enfold(datetime(2037, 10, 25, 1, 00, tzinfo=tzc), fold=1) + self.assertEqual(last_date.tzname(), + "EST") + + self.assertEqual(datetime(2038, 5, 25, 12, 0, tzinfo=tzc).tzname(), + "EST") + + def testInvalidFile(self): + # Should throw a ValueError if an invalid file is passed + with self.assertRaises(ValueError): + tz.tzfile(BytesIO(b'BadFile')) + + def testFilestreamWithNameRepr(self): + # If fileobj is a filestream with a "name" attribute this name should + # be reflected in the tz object's repr + fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT)) + fileobj.name = 'foo' + tzc = tz.tzfile(fileobj) + self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')') + + def testRoundNonFullMinutes(self): + # This timezone has an offset of 5992 seconds in 1900-01-01. + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)), + "1900-01-01 00:00:00+01:40") + + def testLeapCountDecodesProperly(self): + # This timezone has leapcnt, and failed to decode until + # Eugene Oden notified about the issue. + + # As leap information is currently unused (and unstored) by tzfile() we + # can only indirectly test this: Take advantage of tzfile() not closing + # the input file if handed in as an opened file and assert that the + # full file content has been read by tzfile(). Note: For this test to + # work NEW_YORK must be in TZif version 1 format i.e. no more data + # after TZif v1 header + data has been read + fileobj = BytesIO(base64.b64decode(NEW_YORK)) + tz.tzfile(fileobj) + # we expect no remaining file content now, i.e. zero-length; if there's + # still data we haven't read the file format correctly + remaining_tzfile_content = fileobj.read() + self.assertEqual(len(remaining_tzfile_content), 0) + + def testIsStd(self): + # NEW_YORK tzfile contains this isstd information: + isstd_expected = (0, 0, 0, 1) + tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK))) + # gather the actual information as parsed by the tzfile class + isstd = [] + for ttinfo in tzc._ttinfo_list: + # ttinfo objects contain boolean values + isstd.append(int(ttinfo.isstd)) + # ttinfo list may contain more entries than isstd file content + isstd = tuple(isstd[:len(isstd_expected)]) + self.assertEqual( + isstd_expected, isstd, + "isstd UTC/local indicators parsed: %s != tzfile contents: %s" + % (isstd, isstd_expected)) + + def testGMTHasNoDaylight(self): + # tz.tzstr("GMT+2") improperly considered daylight saving time. + # Issue reported by Lennart Regebro. + dt = datetime(2007, 8, 6, 4, 10) + self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0)) + + def testGMTOffset(self): + # GMT and UTC offsets have inverted signal when compared to the + # usual TZ variable handling. + dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) + self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")), + datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) + self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")), + datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2"))) + + @unittest.skipIf(IS_WIN, "requires Unix") + @unittest.skipUnless(TZEnvContext.tz_change_allowed(), + TZEnvContext.tz_change_disallowed_message()) + def testTZSetDoesntCorrupt(self): + # if we start in non-UTC then tzset UTC make sure parse doesn't get + # confused + with TZEnvContext('UTC'): + # this should parse to UTC timezone not the original timezone + dt = parse('2014-07-20T12:34:56+00:00') + self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00') + + +@unittest.skipUnless(IS_WIN, "Requires Windows") +class TzWinTest(unittest.TestCase, TzWinFoldMixin): + def setUp(self): + self.tzclass = tzwin.tzwin + + def testTzResLoadName(self): + # This may not work right on non-US locales. + tzr = tzwin.tzres() + self.assertEqual(tzr.load_name(112), "Eastern Standard Time") + + def testTzResNameFromString(self): + tzr = tzwin.tzres() + self.assertEqual(tzr.name_from_string('@tzres.dll,-221'), + 'Alaskan Daylight Time') + + self.assertEqual(tzr.name_from_string('Samoa Daylight Time'), + 'Samoa Daylight Time') + + with self.assertRaises(ValueError): + tzr.name_from_string('@tzres.dll,100') + + def testIsdstZoneWithNoDaylightSaving(self): + tz = tzwin.tzwin("UTC") + dt = parse("2013-03-06 19:08:15") + self.assertFalse(tz._isdst(dt)) + + def testOffset(self): + tz = tzwin.tzwin("Cape Verde Standard Time") + self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)), + timedelta(-1, 82800)) + + def testTzwinName(self): + # https://github.com/dateutil/dateutil/issues/143 + tw = tz.tzwin('Eastern Standard Time') + + # Cover the transitions for at least two years. + ESTs = 'Eastern Standard Time' + EDTs = 'Eastern Daylight Time' + transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), + (datetime(2015, 3, 8, 3, 1), EDTs), + (datetime(2015, 11, 1, 0, 59), EDTs), + (datetime(2015, 11, 1, 3, 1), ESTs), + (datetime(2016, 3, 13, 0, 59), ESTs), + (datetime(2016, 3, 13, 3, 1), EDTs), + (datetime(2016, 11, 6, 0, 59), EDTs), + (datetime(2016, 11, 6, 3, 1), ESTs)] + + for t_date, expected in transition_dates: + self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) + + def testTzwinRepr(self): + tw = tz.tzwin('Yakutsk Standard Time') + self.assertEqual(repr(tw), 'tzwin(' + + repr('Yakutsk Standard Time') + ')') + + def testTzWinEquality(self): + # https://github.com/dateutil/dateutil/issues/151 + tzwin_names = ('Eastern Standard Time', + 'West Pacific Standard Time', + 'Yakutsk Standard Time', + 'Iran Standard Time', + 'UTC') + + for tzwin_name in tzwin_names: + # Get two different instances to compare + tw1 = tz.tzwin(tzwin_name) + tw2 = tz.tzwin(tzwin_name) + + self.assertEqual(tw1, tw2) + + def testTzWinInequality(self): + # https://github.com/dateutil/dateutil/issues/151 + # Note these last two currently differ only in their name. + tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'), + ('Greenwich Standard Time', 'GMT Standard Time'), + ('GMT Standard Time', 'UTC'), + ('E. South America Standard Time', + 'Argentina Standard Time')) + + for tzwn1, tzwn2 in tzwin_names: + # Get two different instances to compare + tw1 = tz.tzwin(tzwn1) + tw2 = tz.tzwin(tzwn2) + + self.assertNotEqual(tw1, tw2) + + def testTzWinEqualityInvalid(self): + # Compare to objects that do not implement comparison with this + # (should default to False) + UTC = tz.tzutc() + EST = tz.tzwin('Eastern Standard Time') + + self.assertFalse(EST == UTC) + self.assertFalse(EST == 1) + self.assertFalse(UTC == EST) + + self.assertTrue(EST != UTC) + self.assertTrue(EST != 1) + + def testTzWinInequalityUnsupported(self): + # Compare it to an object that is promiscuous about equality, but for + # which tzwin does not implement an equality operator. + EST = tz.tzwin('Eastern Standard Time') + self.assertTrue(EST == ComparesEqual) + self.assertFalse(EST != ComparesEqual) + + def testTzwinTimeOnlyDST(self): + # For zones with DST, .dst() should return None + tw_est = tz.tzwin('Eastern Standard Time') + self.assertIs(dt_time(14, 10, tzinfo=tw_est).dst(), None) + + # This zone has no DST, so .dst() can return 0 + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).dst(), + timedelta(0)) + + def testTzwinTimeOnlyUTCOffset(self): + # For zones with DST, .utcoffset() should return None + tw_est = tz.tzwin('Eastern Standard Time') + self.assertIs(dt_time(14, 10, tzinfo=tw_est).utcoffset(), None) + + # This zone has no DST, so .utcoffset() returns standard offset + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).utcoffset(), + timedelta(hours=2)) + + def testTzwinTimeOnlyTZName(self): + # For zones with DST, the name defaults to standard time + tw_est = tz.tzwin('Eastern Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_est).tzname(), + 'Eastern Standard Time') + + # For zones with no DST, this should work normally. + tw_sast = tz.tzwin('South Africa Standard Time') + self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).tzname(), + 'South Africa Standard Time') + + +@unittest.skipUnless(IS_WIN, "Requires Windows") +@unittest.skipUnless(TZWinContext.tz_change_allowed(), + TZWinContext.tz_change_disallowed_message()) +class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin): + + def setUp(self): + self.tzclass = tzwin.tzwinlocal + self.context = TZWinContext + + def get_args(self, tzname): + return () + + def testLocal(self): + # Not sure how to pin a local time zone, so for now we're just going + # to run this and make sure it doesn't raise an error + # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135 + datetime.now(tzwin.tzwinlocal()) + + def testTzwinLocalUTCOffset(self): + with TZWinContext('Eastern Standard Time'): + tzwl = tzwin.tzwinlocal() + self.assertEqual(datetime(2014, 3, 11, tzinfo=tzwl).utcoffset(), + timedelta(hours=-4)) + + def testTzwinLocalName(self): + # https://github.com/dateutil/dateutil/issues/143 + ESTs = 'Eastern Standard Time' + EDTs = 'Eastern Daylight Time' + transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), + (datetime(2015, 3, 8, 3, 1), EDTs), + (datetime(2015, 11, 1, 0, 59), EDTs), + (datetime(2015, 11, 1, 3, 1), ESTs), + (datetime(2016, 3, 13, 0, 59), ESTs), + (datetime(2016, 3, 13, 3, 1), EDTs), + (datetime(2016, 11, 6, 0, 59), EDTs), + (datetime(2016, 11, 6, 3, 1), ESTs)] + + with TZWinContext('Eastern Standard Time'): + tw = tz.tzwinlocal() + + for t_date, expected in transition_dates: + self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) + + def testTzWinLocalRepr(self): + tw = tz.tzwinlocal() + self.assertEqual(repr(tw), 'tzwinlocal()') + + def testTzwinLocalRepr(self): + # https://github.com/dateutil/dateutil/issues/143 + with TZWinContext('Eastern Standard Time'): + tw = tz.tzwinlocal() + + self.assertEqual(str(tw), 'tzwinlocal(' + + repr('Eastern Standard Time') + ')') + + with TZWinContext('Pacific Standard Time'): + tw = tz.tzwinlocal() + + self.assertEqual(str(tw), 'tzwinlocal(' + + repr('Pacific Standard Time') + ')') + + def testTzwinLocalEquality(self): + tw_est = tz.tzwin('Eastern Standard Time') + tw_pst = tz.tzwin('Pacific Standard Time') + + with TZWinContext('Eastern Standard Time'): + twl1 = tz.tzwinlocal() + twl2 = tz.tzwinlocal() + + self.assertEqual(twl1, twl2) + self.assertEqual(twl1, tw_est) + self.assertNotEqual(twl1, tw_pst) + + with TZWinContext('Pacific Standard Time'): + twl1 = tz.tzwinlocal() + twl2 = tz.tzwinlocal() + tw = tz.tzwin('Pacific Standard Time') + + self.assertEqual(twl1, twl2) + self.assertEqual(twl1, tw) + self.assertEqual(twl1, tw_pst) + self.assertNotEqual(twl1, tw_est) + + def testTzwinLocalTimeOnlyDST(self): + # For zones with DST, .dst() should return None + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertIs(dt_time(14, 10, tzinfo=twl).dst(), None) + + # This zone has no DST, so .dst() can return 0 + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).dst(), timedelta(0)) + + def testTzwinLocalTimeOnlyUTCOffset(self): + # For zones with DST, .utcoffset() should return None + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertIs(dt_time(14, 10, tzinfo=twl).utcoffset(), None) + + # This zone has no DST, so .utcoffset() returns standard offset + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).utcoffset(), + timedelta(hours=2)) + + def testTzwinLocalTimeOnlyTZName(self): + # For zones with DST, the name defaults to standard time + with TZWinContext('Eastern Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(), + 'Eastern Standard Time') + + # For zones with no DST, this should work normally. + with TZWinContext('South Africa Standard Time'): + twl = tz.tzwinlocal() + self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(), + 'South Africa Standard Time') + + +class TzPickleTest(PicklableMixin, unittest.TestCase): + _asfile = False + + def setUp(self): + self.assertPicklable = partial(self.assertPicklable, + asfile=self._asfile) + + def testPickleTzUTC(self): + self.assertPicklable(tz.tzutc(), singleton=True) + + def testPickleTzOffsetZero(self): + self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True) + + def testPickleTzOffsetPos(self): + self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True) + + def testPickleTzOffsetNeg(self): + self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True) + + @pytest.mark.tzlocal + def testPickleTzLocal(self): + self.assertPicklable(tz.tzlocal()) + + def testPickleTzFileEST5EDT(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) + self.assertPicklable(tzc) + + def testPickleTzFileEurope_Helsinki(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) + self.assertPicklable(tzc) + + def testPickleTzFileNew_York(self): + tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK))) + self.assertPicklable(tzc) + + @unittest.skip("Known failure") + def testPickleTzICal(self): + tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get() + self.assertPicklable(tzc) + + def testPickleTzGettz(self): + self.assertPicklable(tz.gettz('America/New_York')) + + def testPickleZoneFileGettz(self): + zoneinfo_file = zoneinfo.get_zonefile_instance() + tzi = zoneinfo_file.get('America/New_York') + self.assertIsNot(tzi, None) + self.assertPicklable(tzi) + + +class TzPickleFileTest(TzPickleTest): + """ Run all the TzPickleTest tests, using a temporary file """ + _asfile = True + + +class DatetimeAmbiguousTest(unittest.TestCase): + """ Test the datetime_exists / datetime_ambiguous functions """ + + def testNoTzSpecified(self): + with self.assertRaises(ValueError): + tz.datetime_ambiguous(datetime(2016, 4, 1, 2, 9)) + + def _get_no_support_tzinfo_class(self, dt_start, dt_end, dst_only=False): + # Generates a class of tzinfo with no support for is_ambiguous + # where dates between dt_start and dt_end are ambiguous. + + class FoldingTzInfo(tzinfo): + def utcoffset(self, dt): + if not dst_only: + dt_n = dt.replace(tzinfo=None) + + if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0): + return timedelta(hours=-1) + + return timedelta(hours=0) + + def dst(self, dt): + dt_n = dt.replace(tzinfo=None) + + if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0): + return timedelta(hours=1) + else: + return timedelta(0) + + return FoldingTzInfo + + def _get_no_support_tzinfo(self, dt_start, dt_end, dst_only=False): + return self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)() + + def testNoSupportAmbiguityFoldNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testNoSupportAmbiguityFoldAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30, + tzinfo=tzi))) + + def testNoSupportAmbiguityUnambiguousNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testNoSupportAmbiguityUnambiguousAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30, + tzinfo=tzi))) + + def testNoSupportAmbiguityFoldDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testNoSupportAmbiguityUnambiguousDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testSupportAmbiguityFoldNaive(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 1, 30) + + self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi)) + + def testSupportAmbiguityFoldAware(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 1, 30, tzinfo=tzi) + + self.assertTrue(tz.datetime_ambiguous(dt)) + + def testSupportAmbiguityUnambiguousAware(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 4, 30) + + self.assertFalse(tz.datetime_ambiguous(dt, tz=tzi)) + + def testSupportAmbiguityUnambiguousNaive(self): + tzi = tz.gettz('US/Eastern') + + dt = datetime(2011, 11, 6, 4, 30, tzinfo=tzi) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + def _get_ambig_error_tzinfo(self, dt_start, dt_end, dst_only=False): + cTzInfo = self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only) + + # Takes the wrong number of arguments and raises an error anyway. + class FoldTzInfoRaises(cTzInfo): + def is_ambiguous(self, dt, other_arg): + raise NotImplementedError('This is not implemented') + + return FoldTzInfoRaises() + + def testIncompatibleAmbiguityFoldNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testIncompatibleAmbiguityFoldAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30, + tzinfo=tzi))) + + def testIncompatibleAmbiguityUnambiguousNaive(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testIncompatibleAmbiguityUnambiguousAware(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30, + tzinfo=tzi))) + + def testIncompatibleAmbiguityFoldDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30), + tz=tzi)) + + def testIncompatibleAmbiguityUnambiguousDSTOnly(self): + dt_start = datetime(2018, 9, 1, 1, 0) + dt_end = datetime(2018, 9, 1, 2, 0) + + tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True) + + self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30), + tz=tzi)) + + def testSpecifiedTzOverridesAttached(self): + # If a tz is specified, the datetime will be treated as naive. + + # This is not ambiguous in the local zone + dt = datetime(2011, 11, 6, 1, 30, tzinfo=tz.gettz('Australia/Sydney')) + + self.assertFalse(tz.datetime_ambiguous(dt)) + + tzi = tz.gettz('US/Eastern') + self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi)) + + +class DatetimeExistsTest(unittest.TestCase): + def testNoTzSpecified(self): + with self.assertRaises(ValueError): + tz.datetime_exists(datetime(2016, 4, 1, 2, 9)) + + def testInGapNaive(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30) + + self.assertFalse(tz.datetime_exists(dt, tz=tzi)) + + def testInGapAware(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30, tzinfo=tzi) + + self.assertFalse(tz.datetime_exists(dt)) + + def testExistsNaive(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 10, 30) + + self.assertTrue(tz.datetime_exists(dt, tz=tzi)) + + def testExistsAware(self): + tzi = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 10, 30, tzinfo=tzi) + + self.assertTrue(tz.datetime_exists(dt)) + + def testSpecifiedTzOverridesAttached(self): + EST = tz.gettz('US/Eastern') + AEST = tz.gettz('Australia/Sydney') + + dt = datetime(2012, 10, 7, 2, 30, tzinfo=EST) # This time exists + + self.assertFalse(tz.datetime_exists(dt, tz=AEST)) + + +class EnfoldTest(unittest.TestCase): + def testEnterFoldDefault(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32)) + + self.assertEqual(dt.fold, 1) + + def testEnterFold(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1) + + self.assertEqual(dt.fold, 1) + + def testExitFold(self): + dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0) + + # Before Python 3.6, dt.fold won't exist if fold is 0. + self.assertEqual(getattr(dt, 'fold', 0), 0) + + +@pytest.mark.tz_resolve_imaginary +class ImaginaryDateTest(unittest.TestCase): + def testCanberraForward(self): + tzi = tz.gettz('Australia/Canberra') + dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testLondonForward(self): + tzi = tz.gettz('Europe/London') + dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + def testKeivForward(self): + tzi = tz.gettz('Europe/Kiev') + dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi) + dt_act = tz.resolve_imaginary(dt) + dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi) + self.assertEqual(dt_act, dt_exp) + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')), +]) +def test_resolve_imaginary_ambiguous(dt): + assert tz.resolve_imaginary(dt) is dt + + dt_f = tz.enfold(dt) + assert dt is not dt_f + assert tz.resolve_imaginary(dt_f) is dt_f + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('dt', [ + datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')), + datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')), + datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzutc()), + datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)), + datetime(2019, 3, 4, tzinfo=None) +]) +def test_resolve_imaginary_existing(dt): + assert tz.resolve_imaginary(dt) is dt + + +def __get_kiritimati_resolve_imaginary_test(): + # In the 2018d release of the IANA database, the Kiritimati "imaginary day" + # data was corrected, so if the system zoneinfo is older than 2018d, the + # Kiritimati test will fail. + + tzi = tz.gettz('Pacific/Kiritimati') + new_version = False + if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi): + zif = zoneinfo.get_zonefile_instance() + if zif.metadata is not None: + new_version = zif.metadata['tzversion'] >= '2018d' + + if new_version: + tzi = zif.get('Pacific/Kiritimati') + else: + new_version = True + + if new_version: + dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30)) + else: + dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30)) + + return (tzi, ) + dates + + +@pytest.mark.tz_resolve_imaginary +@pytest.mark.parametrize('tzi, dt, dt_exp', [ + (tz.gettz('Europe/London'), + datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)), + (tz.gettz('America/New_York'), + datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)), + (tz.gettz('Australia/Sydney'), + datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)), + __get_kiritimati_resolve_imaginary_test(), +]) +def test_resolve_imaginary(tzi, dt, dt_exp): + dt = dt.replace(tzinfo=tzi) + dt_exp = dt_exp.replace(tzinfo=tzi) + + dt_r = tz.resolve_imaginary(dt) + assert dt_r == dt_exp + assert dt_r.tzname() == dt_exp.tzname() + assert dt_r.utcoffset() == dt_exp.utcoffset() + + +@pytest.mark.xfail +@pytest.mark.tz_resolve_imaginary +def test_resolve_imaginary_monrovia(): + # See GH #582 - When that is resolved, move this into test_resolve_imaginary + tzi = tz.gettz('Africa/Monrovia') + dt = datetime(1972, 1, 7, hour=0, minute=30, second=0, tzinfo=tzi) + dt_exp = datetime(1972, 1, 7, hour=1, minute=14, second=30, tzinfo=tzi) + + dt_r = tz.resolve_imaginary(dt) + assert dt_r == dt_exp + assert dt_r.tzname() == dt_exp.tzname() + assert dt_r.utcoffset() == dt_exp.utcoffset() diff --git a/lib/dateutil/test/test_utils.py b/lib/dateutil/test/test_utils.py new file mode 100644 index 0000000..fcdec1a --- /dev/null +++ b/lib/dateutil/test/test_utils.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from datetime import timedelta, datetime + +import unittest + +from dateutil import tz +from dateutil import utils +from dateutil.utils import within_delta + +from freezegun import freeze_time + +UTC = tz.tzutc() +NYC = tz.gettz("America/New_York") + + +class UtilsTest(unittest.TestCase): + @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) + def testToday(self): + self.assertEqual(utils.today(), datetime(2014, 12, 15, 0, 0, 0)) + + @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) + def testTodayTzInfo(self): + self.assertEqual(utils.today(NYC), + datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC)) + + @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) + def testTodayTzInfoDifferentDay(self): + self.assertEqual(utils.today(UTC), + datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC)) + + def testDefaultTZInfoNaive(self): + dt = datetime(2014, 9, 14, 9, 30) + self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, + NYC) + + def testDefaultTZInfoAware(self): + dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) + self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, + UTC) + + def testWithinDelta(self): + d1 = datetime(2016, 1, 1, 12, 14, 1, 9) + d2 = d1.replace(microsecond=15) + + self.assertTrue(within_delta(d1, d2, timedelta(seconds=1))) + self.assertFalse(within_delta(d1, d2, timedelta(microseconds=1))) + + def testWithinDeltaWithNegativeDelta(self): + d1 = datetime(2016, 1, 1) + d2 = datetime(2015, 12, 31) + + self.assertTrue(within_delta(d2, d1, timedelta(days=-1))) diff --git a/lib/dateutil/tz/__init__.py b/lib/dateutil/tz/__init__.py new file mode 100644 index 0000000..5a2d9cd --- /dev/null +++ b/lib/dateutil/tz/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/lib/dateutil/tz/_common.py b/lib/dateutil/tz/_common.py new file mode 100644 index 0000000..ccabb7d --- /dev/null +++ b/lib/dateutil/tz/_common.py @@ -0,0 +1,415 @@ +from six import PY3 + +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 + """ + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None and not PY3: + name = name.encode() + + return name + + return adjust_encoding + + +# 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 + occurence, 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 + occurance, 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__ diff --git a/lib/dateutil/tz/_factories.py b/lib/dateutil/tz/_factories.py new file mode 100644 index 0000000..de2e0c1 --- /dev/null +++ b/lib/dateutil/tz/_factories.py @@ -0,0 +1,49 @@ +from datetime import timedelta + + +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 = {} + + 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)) + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = {} + + 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)) + return instance + diff --git a/lib/dateutil/tz/tz.py b/lib/dateutil/tz/tz.py new file mode 100644 index 0000000..ac82b9c --- /dev/null +++ b/lib/dateutil/tz/tz.py @@ -0,0 +1,1785 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os +import bisect + +import six +from six import string_types +from six.moves import _thread +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs + +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime.utcfromtimestamp(0) +EPOCHORDINAL = EPOCH.toordinal() + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + 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 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + self._offset = datetime.timedelta(seconds=offset) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + 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 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + 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 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + `_ with the `zic time zone compiler + `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _ContextWrapper(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list_utc = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but seek for correct file position) + if leapcnt: + fileobj.seek(leapcnt * 8, os.SEEK_CUR) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # Build ttinfo list + out.ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + # Round to full-minutes if that's not the case. Python's + # datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 for some information. + gmtoff = 60 * ((gmtoff + 30) // 60) + tti = _ttinfo() + tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + out.ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list_utc: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: + break + else: + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst + + for tti in out.ttinfo_list: + if not tti.isdst: + out.ttinfo_before = tti + break + else: + out.ttinfo_before = out.ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + laststdoffset = None + out.trans_list = [] + for i, tti in enumerate(out.trans_idx): + if not tti.isdst: + offset = tti.offset + laststdoffset = offset + else: + if laststdoffset is not None: + # Store the DST offset as well and update it in the list + tti.dstoffset = tti.offset - laststdoffset + out.trans_idx[i] = tti + + offset = laststdoffset or 0 + + out.trans_list.append(out.trans_list_utc[i] + offset) + + # In case we missed any DST offsets on the way in for some reason, make + # a second pass over the list, looking for the /next/ DST offset. + laststdoffset = None + for i in reversed(range(len(out.trans_idx))): + tti = out.trans_idx[i] + if tti.isdst: + if not (tti.dstoffset or laststdoffset is None): + tti.dstoffset = tti.offset - laststdoffset + else: + laststdoffset = tti.offset + + if not isinstance(tti.dstoffset, datetime.timedelta): + tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) + + out.trans_idx[i] = tti + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) + + return out + + def _find_last_transition(self, dt, in_utc=False): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) >= len(self._trans_list): + return self._ttinfo_std + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: + return self._ttinfo_before + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + + def is_ambiguous(self, dt, idx=None): + """ + 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 idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # If it's ambiguous and we're in a fold, shift to a different index. + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset + + def utcoffset(self, dt): + if dt is None: + return None + + if not self._ttinfo_std: + return ZERO + + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if dt is None: + return None + + if not self._ttinfo_dst: + return ZERO + + tti = self._find_ttinfo(dt) + + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.dstoffset + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std or dt is None: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + 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 + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _ContextWrapper(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = {} + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None or isinstance(rv, tzlocal_classes)): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + self.__instances[name] = rv + + return rv + + def cache_clear(self): + with self._cache_lock: + self.__instances = {} + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name == ":": + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + if name.startswith(":"): + name = name[1:] + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except WindowsError: + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = tzutc() + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +class _ContextWrapper(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz/win.py b/lib/dateutil/tz/win.py new file mode 100644 index 0000000..def4353 --- /dev/null +++ b/lib/dateutil/tz/win.py @@ -0,0 +1,331 @@ +# 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 if 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 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): + + 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): + 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 diff --git a/lib/dateutil/tzwin.py b/lib/dateutil/tzwin.py new file mode 100644 index 0000000..cebc673 --- /dev/null +++ b/lib/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/lib/dateutil/utils.py b/lib/dateutil/utils.py new file mode 100644 index 0000000..ebcce6a --- /dev/null +++ b/lib/dateutil/utils.py @@ -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 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 a negilible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/lib/dateutil/zoneinfo/__init__.py b/lib/dateutil/zoneinfo/__init__.py new file mode 100644 index 0000000..34f11ad --- /dev/null +++ b/lib/dateutil/zoneinfo/__init__.py @@ -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 diff --git a/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e86b54fe2884553b2b5e3c7a453b4046a919a800 GIT binary patch literal 139130 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 jM+A@uWoevFAtg+T;9 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�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 E4~rF|;{X5v literal 0 HcmV?d00001 diff --git a/lib/dateutil/zoneinfo/rebuild.py b/lib/dateutil/zoneinfo/rebuild.py new file mode 100644 index 0000000..78f0d1a --- /dev/null +++ b/lib/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,53 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call +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] + try: + check_call(["zic", "-d", zonedir] + filepaths) + except OSError as e: + _print_on_nosuchfile(e) + raise + # 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 _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?") diff --git a/lib/six.py b/lib/six.py new file mode 100644 index 0000000..6bf4fd3 --- /dev/null +++ b/lib/six.py @@ -0,0 +1,891 @@ +# Copyright (c) 2010-2017 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.11.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + try: + if from_value is None: + raise value + raise value from from_value + finally: + value = None +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/time.ini b/time.ini new file mode 100644 index 0000000..8c42d7a --- /dev/null +++ b/time.ini @@ -0,0 +1,24 @@ +# +# Time package configuration file +# + +[main] +# Plugin's main configuration section. + +# Defines a custom label for the Time command +# +# Default: Time: +#item_label = Time: + +# List of python date format string in which dates are displayed. +# See https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior for possible values +# +#formats = %c +# %x + +# List of locale strings for which dates are display +# See https://docs.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=vs-2017 for possible values +# (Empty string is the system default) +# +#locales = +# C diff --git a/time.py b/time.py new file mode 100644 index 0000000..d7749bb --- /dev/null +++ b/time.py @@ -0,0 +1,211 @@ +import keypirinha as kp +import keypirinha_util as kpu +import datetime +import locale +import contextlib +import sys +import os +import re + +# insert lib directory to path to import modules normally +lib = os.path.join(os.path.dirname(__file__), "lib") +if lib not in sys.path: + sys.path.append(lib) +import dateutil.parser + + +class Time(kp.Plugin): + DEFAULT_FORMATS = [ + "%c", + "%x", + ] + DEFAULT_LOCALES = [ + "", + "C", + ] + DEFAULT_ITEM_LABEL = "Time:" + COPY_TO_CB = "(press Enter to copy to clipboard)" + + def __init__(self): + super().__init__() + self._formats = self.DEFAULT_FORMATS + self._locales = self.DEFAULT_LOCALES + self._item_label = self.DEFAULT_ITEM_LABEL + # self._debug = True + + def on_start(self): + self._read_config() + self.set_default_icon(self.load_icon("res://{}/clock.ico".format(self.package_full_name()))) + + def on_events(self, flags): + """Reloads the package config when its changed + """ + if flags & kp.Events.PACKCONFIG: + self._read_config() + + def _read_config(self): + """Reads the config + """ + self.dbg("Reading config") + settings = self.load_settings() + + self._formats = settings.get_multiline("formats", "main", self.DEFAULT_FORMATS) + self.dbg("Formats =", self._formats) + + self._locales = settings.get_multiline("locales", "main", self.DEFAULT_LOCALES, True) + self.dbg("Locales =", self._locales) + + self._item_label = settings.get("item_label", "main", self.DEFAULT_ITEM_LABEL) + self.dbg("item_label =", self._item_label) + + def on_catalog(self): + """Adds the kill command to the catalog + """ + catalog = [ + self.create_item( + category=kp.ItemCategory.KEYWORD, + label=self._item_label, + short_desc="Date and time parsing and formatting", + target="time", + args_hint=kp.ItemArgsHint.REQUIRED, + hit_hint=kp.ItemHitHint.KEEPALL + ) + ] + self.set_catalog(catalog) + + def _create_suggestions(self, timetoshow): + """Creates various catalog items with different formats and locales for a given datetime object + """ + suggestions = [] + + try: + suggestions.append(self.create_item( + category=kp.ItemCategory.KEYWORD, + label=str(int(timetoshow.timestamp())), + short_desc="Time as unix timestamp (seconds since Jan 01 1970. (UTC)) {}".format(self.COPY_TO_CB), + target="timestamp_int", + args_hint=kp.ItemArgsHint.FORBIDDEN, + hit_hint=kp.ItemHitHint.IGNORE + )) + suggestions.append(self.create_item( + category=kp.ItemCategory.KEYWORD, + label=str(int(timetoshow.timestamp() * 1000)), + short_desc="Time as timestamp (milliseconds since Jan 01 1970. (UTC)) {}".format(self.COPY_TO_CB), + target="timestamp_float", + args_hint=kp.ItemArgsHint.FORBIDDEN, + hit_hint=kp.ItemHitHint.IGNORE + )) + except OSError as ex: + self.dbg("Timestamp failed:", ex) + + suggestions.append(self.create_item( + category=kp.ItemCategory.KEYWORD, + label=str(timetoshow.isoformat(timespec="seconds")), + short_desc="Time in ISO 8601 format {}".format(self.COPY_TO_CB), + target="isoformat_s", + args_hint=kp.ItemArgsHint.FORBIDDEN, + hit_hint=kp.ItemHitHint.IGNORE + )) + suggestions.append(self.create_item( + category=kp.ItemCategory.KEYWORD, + label=str(timetoshow.isoformat(timespec="microseconds")), + short_desc="Time in ISO 8601 format {}".format(self.COPY_TO_CB), + target="isoformat_ms", + args_hint=kp.ItemArgsHint.FORBIDDEN, + hit_hint=kp.ItemHitHint.IGNORE + )) + + for idx, frmt in enumerate(self._formats): + for loc in self._locales: + try: + with self.__setlocale(loc): + item = self.create_item( + category=kp.ItemCategory.KEYWORD, + label=str(timetoshow.strftime(frmt)), + short_desc="Time in format '{}' in locale {} {}".format(frmt, + loc if loc else "system default", + self.COPY_TO_CB), + target="format_{}_{}".format(idx, loc), + args_hint=kp.ItemArgsHint.FORBIDDEN, + hit_hint=kp.ItemHitHint.IGNORE + ) + if not self.__contains_item(suggestions, item): + suggestions.append(item) + except locale.Error as ex: + self.warn("Error with format ", frmt, "on locale", loc, ":", ex) + + return suggestions + + def __contains_item(self, suggestions, search): + """Checks if a catalog item with the same label is already in the collection + """ + for item in suggestions: + if item.label() == search.label(): + return True + return False + + @contextlib.contextmanager + def __setlocale(self, name): + """Sets the locale for time formatting functions to be used in a with statement. + + :param name: See https://docs.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=vs-2017 for + possible values + """ + saved = locale.setlocale(locale.LC_TIME) + try: + yield locale.setlocale(locale.LC_TIME, name) + finally: + locale.setlocale(locale.LC_TIME, saved) + + def on_suggest(self, user_input, items_chain): + if not items_chain: + return + + if user_input: + timetoshow = self._tryparse(user_input) + else: + timetoshow = datetime.datetime.now().astimezone() + + if timetoshow: + suggestions = self._create_suggestions(timetoshow) + self.set_suggestions(suggestions, kp.Match.ANY, kp.Sort.NONE) + + def _tryparse(self, in_str): + """Tries to parse a string into a datetime object + """ + # Maybe its a timestamp + if re.match("^\d+$", in_str): + try: + return datetime.datetime.fromtimestamp(int(in_str)).astimezone() + except: + self.dbg("Parsing failed: ", sys.exc_info()[0]) + + try: + return datetime.datetime.fromtimestamp(int(in_str) / 1000).astimezone() + except OSError as ex: + self.dbg("Parsing failed: ", ex) + + if re.match("^\d+\.\d*$", in_str): + try: + return datetime.datetime.fromtimestamp(float(in_str)).astimezone() + except OSError as ex: + self.dbg("Parsing failed: ", ex) + + try: + return datetime.datetime.fromtimestamp(float(in_str) / 1000).astimezone() + except OSError as ex: + self.dbg("Parsing failed: ", ex) + + # do your magic dateutil + try: + return dateutil.parser.parse(in_str) + except (ValueError, OverflowError) as ex: + self.dbg("Parsing failed: ", ex) + + return None + + def on_execute(self, item, action): + """Copies the item label to the clipboard + """ + kpu.set_clipboard(item.label()) +