python-dateutil
Advanced tools
| repos: | ||
| - repo: https://github.com/akaihola/darker | ||
| rev: 1.7.1 | ||
| hooks: | ||
| - id: darker | ||
| args: [--isort] | ||
| additional_dependencies: | ||
| - "isort>5.9" | ||
| - repo: https://github.com/pre-commit/pre-commit-hooks | ||
| rev: v4.4.0 | ||
| hooks: | ||
| - id: trailing-whitespace | ||
| - id: debug-statements |
| version: 2 | ||
| sphinx: | ||
| configuration: docs/conf.py | ||
| build: | ||
| os: ubuntu-22.04 | ||
| tools: | ||
| python: "3.11" | ||
| python: | ||
| install: | ||
| - method: "pip" | ||
| path: "." | ||
| - requirements: "docs/requirements-docs.txt" |
| # -*- coding: utf-8 -*- | ||
| import sys | ||
| try: | ||
| from ._version import version as __version__ | ||
| except ImportError: | ||
| __version__ = 'unknown' | ||
| __all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', | ||
| 'utils', 'zoneinfo'] | ||
| def __getattr__(name): | ||
| import importlib | ||
| if name in __all__: | ||
| return importlib.import_module("." + name, __name__) | ||
| raise AttributeError( | ||
| "module {!r} has not attribute {!r}".format(__name__, name) | ||
| ) | ||
| def __dir__(): | ||
| # __dir__ should include all the lazy-importable modules as well. | ||
| return [x for x in globals() if x not in sys.modules] + __all__ |
| """ | ||
| 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 |
| # file generated by setuptools_scm | ||
| # don't change, don't track in version control | ||
| TYPE_CHECKING = False | ||
| if TYPE_CHECKING: | ||
| from typing import Tuple, Union | ||
| VERSION_TUPLE = Tuple[Union[int, str], ...] | ||
| else: | ||
| VERSION_TUPLE = object | ||
| version: str | ||
| __version__: str | ||
| __version_tuple__: VERSION_TUPLE | ||
| version_tuple: VERSION_TUPLE | ||
| __version__ = version = '2.9.0' | ||
| __version_tuple__ = version_tuple = (2, 9, 0) |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a generic Easter computing method for any given year, using | ||
| Western, Orthodox or Julian algorithms. | ||
| """ | ||
| import datetime | ||
| __all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] | ||
| EASTER_JULIAN = 1 | ||
| EASTER_ORTHODOX = 2 | ||
| EASTER_WESTERN = 3 | ||
| def easter(year, method=EASTER_WESTERN): | ||
| """ | ||
| This method was ported from the work done by GM Arts, | ||
| on top of the algorithm by Claus Tondering, which was | ||
| based in part on the algorithm of Ouding (1940), as | ||
| quoted in "Explanatory Supplement to the Astronomical | ||
| Almanac", P. Kenneth Seidelmann, editor. | ||
| This algorithm implements three different Easter | ||
| calculation methods: | ||
| 1. Original calculation in Julian calendar, valid in | ||
| dates after 326 AD | ||
| 2. Original method, with date converted to Gregorian | ||
| calendar, valid in years 1583 to 4099 | ||
| 3. Revised method, in Gregorian calendar, valid in | ||
| years 1583 to 4099 as well | ||
| These methods are represented by the constants: | ||
| * ``EASTER_JULIAN = 1`` | ||
| * ``EASTER_ORTHODOX = 2`` | ||
| * ``EASTER_WESTERN = 3`` | ||
| The default method is method 3. | ||
| More about the algorithm may be found at: | ||
| `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_ | ||
| and | ||
| `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_ | ||
| """ | ||
| if not (1 <= method <= 3): | ||
| raise ValueError("invalid method") | ||
| # g - Golden year - 1 | ||
| # c - Century | ||
| # h - (23 - Epact) mod 30 | ||
| # i - Number of days from March 21 to Paschal Full Moon | ||
| # j - Weekday for PFM (0=Sunday, etc) | ||
| # p - Number of days from March 21 to Sunday on or before PFM | ||
| # (-6 to 28 methods 1 & 3, to 56 for method 2) | ||
| # e - Extra days to add for method 2 (converting Julian | ||
| # date to Gregorian date) | ||
| y = year | ||
| g = y % 19 | ||
| e = 0 | ||
| if method < 3: | ||
| # Old method | ||
| i = (19*g + 15) % 30 | ||
| j = (y + y//4 + i) % 7 | ||
| if method == 2: | ||
| # Extra dates to convert Julian to Gregorian date | ||
| e = 10 | ||
| if y > 1600: | ||
| e = e + y//100 - 16 - (y//100 - 16)//4 | ||
| else: | ||
| # New method | ||
| c = y//100 | ||
| h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 | ||
| i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) | ||
| j = (y + y//4 + i + 2 - c + c//4) % 7 | ||
| # p can be from -6 to 56 corresponding to dates 22 March to 23 May | ||
| # (later dates apply to method 2, although 23 May never actually occurs) | ||
| p = i - j + e | ||
| d = 1 + (p + 27 + (p + 6)//40) % 31 | ||
| m = 3 + (p + 26)//30 | ||
| return datetime.date(int(y), int(m), int(d)) |
| # -*- coding: utf-8 -*- | ||
| from ._parser import parse, parser, parserinfo, ParserError | ||
| from ._parser import DEFAULTPARSER, DEFAULTTZPARSER | ||
| from ._parser import UnknownTimezoneWarning | ||
| from ._parser import __doc__ | ||
| from .isoparser import isoparser, isoparse | ||
| __all__ = ['parse', 'parser', 'parserinfo', | ||
| 'isoparse', 'isoparser', | ||
| 'ParserError', | ||
| 'UnknownTimezoneWarning'] | ||
| ### | ||
| # Deprecate portions of the private interface so that downstream code that | ||
| # is improperly relying on it is given *some* notice. | ||
| def __deprecated_private_func(f): | ||
| from functools import wraps | ||
| import warnings | ||
| msg = ('{name} is a private function and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=f.__name__) | ||
| @wraps(f) | ||
| def deprecated_func(*args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| return f(*args, **kwargs) | ||
| return deprecated_func | ||
| def __deprecate_private_class(c): | ||
| import warnings | ||
| msg = ('{name} is a private class and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=c.__name__) | ||
| class private_class(c): | ||
| __doc__ = c.__doc__ | ||
| def __init__(self, *args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| super(private_class, self).__init__(*args, **kwargs) | ||
| private_class.__name__ = c.__name__ | ||
| return private_class | ||
| from ._parser import _timelex, _resultbase | ||
| from ._parser import _tzparser, _parsetz | ||
| _timelex = __deprecate_private_class(_timelex) | ||
| _tzparser = __deprecate_private_class(_tzparser) | ||
| _resultbase = __deprecate_private_class(_resultbase) | ||
| _parsetz = __deprecated_private_func(_parsetz) |
| # -*- 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 | ||
| <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_ | ||
| - `W3C Date and Time Formats <https://www.w3.org/TR/NOTE-datetime>`_ | ||
| - `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_ | ||
| - `CPAN ParseDate module | ||
| <https://metacpan.org/pod/release/MUIR/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_ | ||
| - `Java SimpleDateFormat Class | ||
| <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_ | ||
| """ | ||
| 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 integer_types, text_type | ||
| from decimal import Decimal | ||
| from warnings import warn | ||
| from .. import relativedelta | ||
| from .. import tz | ||
| __all__ = ["parse", "parserinfo", "ParserError"] | ||
| # 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 isinstance(instream, (bytes, bytearray)): | ||
| 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", "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' 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 leap year | ||
| 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 precedence 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 ParserError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :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 ParserError("Unknown string format: %s", timestr) | ||
| if len(res) == 0: | ||
| raise ParserError("String does not contain a date: %s", timestr) | ||
| try: | ||
| ret = self._build_naive(res, default) | ||
| except ValueError as e: | ||
| six.raise_from(ParserError(str(e) + ": %s", timestr), e) | ||
| 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" preceding 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) | ||
| or token in self.info.UTCZONE)) | ||
| 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 *don't* 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 _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) | ||
| # ------------------------------------------------------------------ | ||
| # Handling for individual tokens. These are kept as methods instead | ||
| # of functions for the sake of customizability via subclassing. | ||
| def _parsems(self, value): | ||
| """Parse a I[.F] seconds value into (seconds, microseconds).""" | ||
| if "." not in value: | ||
| return int(value), 0 | ||
| else: | ||
| i, f = value.split(".") | ||
| return int(i), int(f.ljust(6, "0")[:6]) | ||
| def _to_decimal(self, val): | ||
| try: | ||
| decimal_value = Decimal(val) | ||
| # See GH 662, edge case, infinite value should not be converted | ||
| # via `_to_decimal` | ||
| if not decimal_value.is_finite(): | ||
| raise ValueError("Converted decimal value is infinite or NaN") | ||
| except Exception as e: | ||
| msg = "Could not convert %s to decimal" % val | ||
| six.raise_from(ValueError(msg), e) | ||
| else: | ||
| return decimal_value | ||
| # ------------------------------------------------------------------ | ||
| # Post-Parsing construction of datetime output. These are kept as | ||
| # methods instead of functions for the sake of customizability via | ||
| # subclassing. | ||
| def _build_tzinfo(self, tzinfos, tzname, tzoffset): | ||
| if callable(tzinfos): | ||
| 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) | ||
| else: | ||
| raise TypeError("Offset must be tzinfo subclass, tz string, " | ||
| "or int offset.") | ||
| 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.UTC) | ||
| elif res.tzoffset == 0: | ||
| aware = naive.replace(tzinfo=tz.UTC) | ||
| 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 _recombine_skipped(self, tokens, skipped_idxs): | ||
| """ | ||
| >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] | ||
| >>> skipped_idxs = [0, 1, 2, 5] | ||
| >>> _recombine_skipped(tokens, skipped_idxs) | ||
| ["foo bar", "baz"] | ||
| """ | ||
| skipped_tokens = [] | ||
| for i, idx in enumerate(sorted(skipped_idxs)): | ||
| if i > 0 and idx - 1 == skipped_idxs[i - 1]: | ||
| skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] | ||
| else: | ||
| skipped_tokens.append(tokens[idx]) | ||
| return skipped_tokens | ||
| DEFAULTPARSER = parser() | ||
| 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 ParserError: | ||
| Raised for invalid or unknown string formats, 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 ParserError(ValueError): | ||
| """Exception subclass used for any failure to parse a datetime string. | ||
| This is a subclass of :py:exc:`ValueError`, and should be raised any time | ||
| earlier versions of ``dateutil`` would have raised ``ValueError``. | ||
| .. versionadded:: 2.8.1 | ||
| """ | ||
| def __str__(self): | ||
| try: | ||
| return self.args[0] % self.args[1:] | ||
| except (TypeError, IndexError): | ||
| return super(ParserError, self).__str__() | ||
| def __repr__(self): | ||
| args = ", ".join("'%s'" % arg for arg in self.args) | ||
| return "%s(%s)" % (self.__class__.__name__, args) | ||
| class UnknownTimezoneWarning(RuntimeWarning): | ||
| """Raised when the parser finds a timezone it cannot parse into a tzinfo. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| # vim:ts=4:sw=4:et |
| # -*- 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`` | ||
| - ``YYYY-MM-DD`` or ``YYYYMMDD`` | ||
| Uncommon: | ||
| - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) | ||
| - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day | ||
| The ISO week and day numbering follows the same logic as | ||
| :func:`datetime.date.isocalendar`. | ||
| Supported time formats are: | ||
| - ``hh`` | ||
| - ``hh:mm`` or ``hhmm`` | ||
| - ``hh:mm:ss`` or ``hhmmss`` | ||
| - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) | ||
| Midnight is a special case for `hh`, as the standard supports both | ||
| 00:00 and 24:00 as a representation. The decimal separator can be | ||
| either a dot or a comma. | ||
| .. caution:: | ||
| Support for fractional components other than seconds is part of the | ||
| ISO-8601 standard, but is not currently implemented in this parser. | ||
| Supported time zone offset formats are: | ||
| - `Z` (UTC) | ||
| - `±HH:MM` | ||
| - `±HHMM` | ||
| - `±HH` | ||
| Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, | ||
| with the exception of UTC, which will be represented as | ||
| :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such | ||
| as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. | ||
| :param dt_str: | ||
| A string or stream containing only an ISO-8601 datetime string | ||
| :return: | ||
| Returns a :class:`datetime.datetime` representing the string. | ||
| Unspecified components default to their lowest value. | ||
| .. warning:: | ||
| As of version 2.7.0, the strictness of the parser should not be | ||
| considered a stable part of the contract. Any valid ISO-8601 string | ||
| that parses correctly with the default settings will continue to | ||
| parse correctly in future versions, but invalid strings that | ||
| currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not | ||
| guaranteed to continue failing in future versions if they encode | ||
| a valid date. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| components, pos = self._parse_isodate(dt_str) | ||
| if len(dt_str) > pos: | ||
| if self._sep is None or dt_str[pos:pos + 1] == self._sep: | ||
| components += self._parse_isotime(dt_str[pos + 1:]) | ||
| else: | ||
| raise ValueError('String contains unknown ISO components') | ||
| if len(components) > 3 and components[3] == 24: | ||
| components[3] = 0 | ||
| return datetime(*components) + timedelta(days=1) | ||
| return datetime(*components) | ||
| @_takes_ascii | ||
| def parse_isodate(self, datestr): | ||
| """ | ||
| Parse the date portion of an ISO string. | ||
| :param datestr: | ||
| The string portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.date` object | ||
| """ | ||
| components, pos = self._parse_isodate(datestr) | ||
| if pos < len(datestr): | ||
| raise ValueError('String contains unknown ISO ' + | ||
| 'components: {!r}'.format(datestr.decode('ascii'))) | ||
| return date(*components) | ||
| @_takes_ascii | ||
| def parse_isotime(self, timestr): | ||
| """ | ||
| Parse the time portion of an ISO string. | ||
| :param timestr: | ||
| The time portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.time` object | ||
| """ | ||
| components = self._parse_isotime(timestr) | ||
| if components[0] == 24: | ||
| components[0] = 0 | ||
| return time(*components) | ||
| @_takes_ascii | ||
| def parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| """ | ||
| Parse a valid ISO time zone string. | ||
| See :func:`isoparser.isoparse` for details on supported formats. | ||
| :param tzstr: | ||
| A string representing an ISO time zone offset | ||
| :param zero_as_utc: | ||
| Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones | ||
| :return: | ||
| Returns :class:`dateutil.tz.tzoffset` for offsets and | ||
| :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is | ||
| specified) offsets equivalent to UTC. | ||
| """ | ||
| return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| # Constants | ||
| _DATE_SEP = b'-' | ||
| _TIME_SEP = b':' | ||
| _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') | ||
| def _parse_isodate(self, dt_str): | ||
| try: | ||
| return self._parse_isodate_common(dt_str) | ||
| except ValueError: | ||
| return self._parse_isodate_uncommon(dt_str) | ||
| def _parse_isodate_common(self, dt_str): | ||
| len_str = len(dt_str) | ||
| components = [1, 1, 1] | ||
| if len_str < 4: | ||
| raise ValueError('ISO string too short') | ||
| # Year | ||
| components[0] = int(dt_str[0:4]) | ||
| pos = 4 | ||
| if pos >= len_str: | ||
| return components, pos | ||
| has_sep = dt_str[pos:pos + 1] == self._DATE_SEP | ||
| if has_sep: | ||
| pos += 1 | ||
| # Month | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common month') | ||
| components[1] = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| if pos >= len_str: | ||
| if has_sep: | ||
| return components, pos | ||
| else: | ||
| raise ValueError('Invalid ISO format') | ||
| if has_sep: | ||
| if dt_str[pos:pos + 1] != self._DATE_SEP: | ||
| raise ValueError('Invalid separator in ISO string') | ||
| pos += 1 | ||
| # Day | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common day') | ||
| components[2] = int(dt_str[pos:pos + 2]) | ||
| return components, pos + 2 | ||
| def _parse_isodate_uncommon(self, dt_str): | ||
| if len(dt_str) < 4: | ||
| raise ValueError('ISO string too short') | ||
| # All ISO formats start with the year | ||
| year = int(dt_str[0:4]) | ||
| has_sep = dt_str[4:5] == self._DATE_SEP | ||
| pos = 4 + has_sep # Skip '-' if it's there | ||
| if dt_str[pos:pos + 1] == b'W': | ||
| # YYYY-?Www-?D? | ||
| pos += 1 | ||
| weekno = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| dayno = 1 | ||
| if len(dt_str) > pos: | ||
| if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: | ||
| raise ValueError('Inconsistent use of dash separator') | ||
| pos += has_sep | ||
| dayno = int(dt_str[pos:pos + 1]) | ||
| pos += 1 | ||
| base_date = self._calculate_weekdate(year, weekno, dayno) | ||
| else: | ||
| # YYYYDDD or YYYY-DDD | ||
| if len(dt_str) - pos < 3: | ||
| raise ValueError('Invalid ordinal day') | ||
| ordinal_day = int(dt_str[pos:pos + 3]) | ||
| pos += 3 | ||
| if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): | ||
| raise ValueError('Invalid ordinal day' + | ||
| ' {} for year {}'.format(ordinal_day, year)) | ||
| base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) | ||
| components = [base_date.year, base_date.month, base_date.day] | ||
| return components, pos | ||
| def _calculate_weekdate(self, year, week, day): | ||
| """ | ||
| Calculate the day of corresponding to the ISO year-week-day calendar. | ||
| This function is effectively the inverse of | ||
| :func:`datetime.date.isocalendar`. | ||
| :param year: | ||
| The year in the ISO calendar | ||
| :param week: | ||
| The week in the ISO calendar - range is [1, 53] | ||
| :param day: | ||
| The day in the ISO calendar - range is [1 (MON), 7 (SUN)] | ||
| :return: | ||
| Returns a :class:`datetime.date` | ||
| """ | ||
| if not 0 < week < 54: | ||
| raise ValueError('Invalid week: {}'.format(week)) | ||
| if not 0 < day < 8: # Range is 1-7 | ||
| raise ValueError('Invalid weekday: {}'.format(day)) | ||
| # Get week 1 for the specific year: | ||
| jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it | ||
| week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) | ||
| # Now add the specific number of weeks and days to get what we want | ||
| week_offset = (week - 1) * 7 + (day - 1) | ||
| return week_1 + timedelta(days=week_offset) | ||
| def _parse_isotime(self, timestr): | ||
| len_str = len(timestr) | ||
| components = [0, 0, 0, 0, None] | ||
| pos = 0 | ||
| comp = -1 | ||
| if len_str < 2: | ||
| raise ValueError('ISO time too short') | ||
| has_sep = False | ||
| while pos < len_str and comp < 5: | ||
| comp += 1 | ||
| if timestr[pos:pos + 1] in b'-+Zz': | ||
| # Detect time zone boundary | ||
| components[-1] = self._parse_tzstr(timestr[pos:]) | ||
| pos = len_str | ||
| break | ||
| if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: | ||
| has_sep = True | ||
| pos += 1 | ||
| elif comp == 2 and has_sep: | ||
| if timestr[pos:pos+1] != self._TIME_SEP: | ||
| raise ValueError('Inconsistent use of colon separator') | ||
| pos += 1 | ||
| if comp < 3: | ||
| # Hour, minute, second | ||
| components[comp] = int(timestr[pos:pos + 2]) | ||
| pos += 2 | ||
| if comp == 3: | ||
| # Fraction of a second | ||
| frac = self._FRACTION_REGEX.match(timestr[pos:]) | ||
| if not frac: | ||
| continue | ||
| us_str = frac.group(1)[:6] # Truncate to microseconds | ||
| components[comp] = int(us_str) * 10**(6 - len(us_str)) | ||
| pos += len(frac.group()) | ||
| if pos < len_str: | ||
| raise ValueError('Unused components in ISO string') | ||
| if components[0] == 24: | ||
| # Standard supports 00:00 and 24:00 as representations of midnight | ||
| if any(component != 0 for component in components[1:4]): | ||
| raise ValueError('Hour may only be 24 at 24:00:00.000') | ||
| return components | ||
| def _parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| if tzstr == b'Z' or tzstr == b'z': | ||
| return tz.UTC | ||
| if len(tzstr) not in {3, 5, 6}: | ||
| raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') | ||
| if tzstr[0:1] == b'-': | ||
| mult = -1 | ||
| elif tzstr[0:1] == b'+': | ||
| mult = 1 | ||
| else: | ||
| raise ValueError('Time zone offset requires sign') | ||
| hours = int(tzstr[1:3]) | ||
| if len(tzstr) == 3: | ||
| minutes = 0 | ||
| else: | ||
| minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) | ||
| if zero_as_utc and hours == 0 and minutes == 0: | ||
| return tz.UTC | ||
| else: | ||
| if minutes > 59: | ||
| raise ValueError('Invalid minutes in time zone offset') | ||
| if hours > 23: | ||
| raise ValueError('Invalid hours in time zone offset') | ||
| return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) | ||
| DEFAULT_ISOPARSER = isoparser() | ||
| isoparse = DEFAULT_ISOPARSER.isoparse |
| # -*- coding: utf-8 -*- | ||
| import datetime | ||
| import calendar | ||
| import operator | ||
| from math import copysign | ||
| from six import integer_types | ||
| from warnings import warn | ||
| from ._common import weekday | ||
| MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) | ||
| __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] | ||
| class relativedelta(object): | ||
| """ | ||
| The relativedelta type is designed to be applied to an existing datetime and | ||
| can replace specific components of that datetime, or represents an interval | ||
| of time. | ||
| It is based on the specification of the excellent work done by M.-A. Lemburg | ||
| in his | ||
| `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension. | ||
| However, notice that this type does *NOT* implement the same algorithm as | ||
| his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. | ||
| There are two different ways to build a relativedelta instance. The | ||
| first one is passing it two date/datetime classes:: | ||
| relativedelta(datetime1, datetime2) | ||
| The second one is passing it any number of the following keyword arguments:: | ||
| relativedelta(arg1=x,arg2=y,arg3=z...) | ||
| year, month, day, hour, minute, second, microsecond: | ||
| Absolute information (argument is singular); adding or subtracting a | ||
| relativedelta with absolute information does not perform an arithmetic | ||
| operation, but rather REPLACES the corresponding value in the | ||
| original datetime with the value(s) in relativedelta. | ||
| years, months, weeks, days, hours, minutes, seconds, microseconds: | ||
| Relative information, may be negative (argument is plural); adding | ||
| or subtracting a relativedelta with relative information performs | ||
| the corresponding arithmetic operation on the original datetime value | ||
| with the information in the relativedelta. | ||
| weekday: | ||
| One of the weekday instances (MO, TU, etc) available in the | ||
| relativedelta module. These instances may receive a parameter N, | ||
| specifying the Nth weekday, which could be positive or negative | ||
| (like MO(+1) or MO(-2)). Not specifying it is the same as specifying | ||
| +1. You can also use an integer, where 0=MO. This argument is always | ||
| relative e.g. if the calculated date is already Monday, using MO(1) | ||
| or MO(-1) won't change the day. To effectively make it absolute, use | ||
| it in combination with the day argument (e.g. day=1, MO(1) for first | ||
| Monday of the month). | ||
| leapdays: | ||
| Will add given days to the date found, if year is a leap | ||
| year, and the date found is post 28 of february. | ||
| yearday, nlyearday: | ||
| Set the yearday or the non-leap year day (jump leap days). | ||
| These are converted to day/month/leapdays information. | ||
| There are relative and absolute forms of the keyword | ||
| arguments. The plural is relative, and the singular is | ||
| absolute. For each argument in the order below, the absolute form | ||
| is applied first (by setting each attribute to that value) and | ||
| then the relative form (by adding the value to the attribute). | ||
| The order of attributes considered when this relativedelta is | ||
| added to a datetime is: | ||
| 1. Year | ||
| 2. Month | ||
| 3. Day | ||
| 4. Hours | ||
| 5. Minutes | ||
| 6. Seconds | ||
| 7. Microseconds | ||
| Finally, weekday is applied, using the rule described above. | ||
| For example | ||
| >>> from datetime import datetime | ||
| >>> from dateutil.relativedelta import relativedelta, MO | ||
| >>> dt = datetime(2018, 4, 9, 13, 37, 0) | ||
| >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) | ||
| >>> dt + delta | ||
| datetime.datetime(2018, 4, 2, 14, 37) | ||
| First, the day is set to 1 (the first of the month), then 25 hours | ||
| are added, to get to the 2nd day and 14th hour, finally the | ||
| weekday is applied, but since the 2nd is already a Monday there is | ||
| no effect. | ||
| """ | ||
| def __init__(self, dt1=None, dt2=None, | ||
| years=0, months=0, days=0, leapdays=0, weeks=0, | ||
| hours=0, minutes=0, seconds=0, microseconds=0, | ||
| year=None, month=None, day=None, weekday=None, | ||
| yearday=None, nlyearday=None, | ||
| hour=None, minute=None, second=None, microsecond=None): | ||
| if dt1 and dt2: | ||
| # datetime is a subclass of date. So both must be date | ||
| if not (isinstance(dt1, datetime.date) and | ||
| isinstance(dt2, datetime.date)): | ||
| raise TypeError("relativedelta only diffs datetime/date") | ||
| # We allow two dates, or two datetimes, so we coerce them to be | ||
| # of the same type | ||
| if (isinstance(dt1, datetime.datetime) != | ||
| isinstance(dt2, datetime.datetime)): | ||
| if not isinstance(dt1, datetime.datetime): | ||
| dt1 = datetime.datetime.fromordinal(dt1.toordinal()) | ||
| elif not isinstance(dt2, datetime.datetime): | ||
| dt2 = datetime.datetime.fromordinal(dt2.toordinal()) | ||
| self.years = 0 | ||
| self.months = 0 | ||
| self.days = 0 | ||
| self.leapdays = 0 | ||
| self.hours = 0 | ||
| self.minutes = 0 | ||
| self.seconds = 0 | ||
| self.microseconds = 0 | ||
| self.year = None | ||
| self.month = None | ||
| self.day = None | ||
| self.weekday = None | ||
| self.hour = None | ||
| self.minute = None | ||
| self.second = None | ||
| self.microsecond = None | ||
| self._has_time = 0 | ||
| # Get year / month delta between the two | ||
| months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) | ||
| self._set_months(months) | ||
| # Remove the year/month delta so the timedelta is just well-defined | ||
| # time units (seconds, days and microseconds) | ||
| dtm = self.__radd__(dt2) | ||
| # If we've overshot our target, make an adjustment | ||
| if dt1 < dt2: | ||
| compare = operator.gt | ||
| increment = 1 | ||
| else: | ||
| compare = operator.lt | ||
| increment = -1 | ||
| while compare(dt1, dtm): | ||
| months += increment | ||
| self._set_months(months) | ||
| dtm = self.__radd__(dt2) | ||
| # Get the timedelta between the "months-adjusted" date and dt1 | ||
| delta = dt1 - dtm | ||
| self.seconds = delta.seconds + delta.days * 86400 | ||
| self.microseconds = delta.microseconds | ||
| else: | ||
| # Check for non-integer values in integer-only quantities | ||
| if any(x is not None and x != int(x) for x in (years, months)): | ||
| raise ValueError("Non-integer years and months are " | ||
| "ambiguous and not currently supported.") | ||
| # Relative information | ||
| self.years = int(years) | ||
| self.months = int(months) | ||
| self.days = days + weeks * 7 | ||
| self.leapdays = leapdays | ||
| self.hours = hours | ||
| self.minutes = minutes | ||
| self.seconds = seconds | ||
| self.microseconds = microseconds | ||
| # Absolute information | ||
| self.year = year | ||
| self.month = month | ||
| self.day = day | ||
| self.hour = hour | ||
| self.minute = minute | ||
| self.second = second | ||
| self.microsecond = microsecond | ||
| if any(x is not None and int(x) != x | ||
| for x in (year, month, day, hour, | ||
| minute, second, microsecond)): | ||
| # For now we'll deprecate floats - later it'll be an error. | ||
| warn("Non-integer value passed as absolute information. " + | ||
| "This is not a well-defined condition and will raise " + | ||
| "errors in future versions.", DeprecationWarning) | ||
| if isinstance(weekday, integer_types): | ||
| self.weekday = weekdays[weekday] | ||
| else: | ||
| self.weekday = weekday | ||
| yday = 0 | ||
| if nlyearday: | ||
| yday = nlyearday | ||
| elif yearday: | ||
| yday = yearday | ||
| if yearday > 59: | ||
| self.leapdays = -1 | ||
| if yday: | ||
| ydayidx = [31, 59, 90, 120, 151, 181, 212, | ||
| 243, 273, 304, 334, 366] | ||
| for idx, ydays in enumerate(ydayidx): | ||
| if yday <= ydays: | ||
| self.month = idx+1 | ||
| if idx == 0: | ||
| self.day = yday | ||
| else: | ||
| self.day = yday-ydayidx[idx-1] | ||
| break | ||
| else: | ||
| raise ValueError("invalid year day (%d)" % yday) | ||
| self._fix() | ||
| def _fix(self): | ||
| if abs(self.microseconds) > 999999: | ||
| s = _sign(self.microseconds) | ||
| div, mod = divmod(self.microseconds * s, 1000000) | ||
| self.microseconds = mod * s | ||
| self.seconds += div * s | ||
| if abs(self.seconds) > 59: | ||
| s = _sign(self.seconds) | ||
| div, mod = divmod(self.seconds * s, 60) | ||
| self.seconds = mod * s | ||
| self.minutes += div * s | ||
| if abs(self.minutes) > 59: | ||
| s = _sign(self.minutes) | ||
| div, mod = divmod(self.minutes * s, 60) | ||
| self.minutes = mod * s | ||
| self.hours += div * s | ||
| if abs(self.hours) > 23: | ||
| s = _sign(self.hours) | ||
| div, mod = divmod(self.hours * s, 24) | ||
| self.hours = mod * s | ||
| self.days += div * s | ||
| if abs(self.months) > 11: | ||
| s = _sign(self.months) | ||
| div, mod = divmod(self.months * s, 12) | ||
| self.months = mod * s | ||
| self.years += div * s | ||
| if (self.hours or self.minutes or self.seconds or self.microseconds | ||
| or self.hour is not None or self.minute is not None or | ||
| self.second is not None or self.microsecond is not None): | ||
| self._has_time = 1 | ||
| else: | ||
| self._has_time = 0 | ||
| @property | ||
| def weeks(self): | ||
| return int(self.days / 7.0) | ||
| @weeks.setter | ||
| def weeks(self, value): | ||
| self.days = self.days - (self.weeks * 7) + value * 7 | ||
| def _set_months(self, months): | ||
| self.months = months | ||
| if abs(self.months) > 11: | ||
| s = _sign(self.months) | ||
| div, mod = divmod(self.months * s, 12) | ||
| self.months = mod * s | ||
| self.years = div * s | ||
| else: | ||
| self.years = 0 | ||
| def normalized(self): | ||
| """ | ||
| Return a version of this object represented entirely using integer | ||
| values for the relative attributes. | ||
| >>> relativedelta(days=1.5, hours=2).normalized() | ||
| relativedelta(days=+1, hours=+14) | ||
| :return: | ||
| Returns a :class:`dateutil.relativedelta.relativedelta` object. | ||
| """ | ||
| # Cascade remainders down (rounding each to roughly nearest microsecond) | ||
| days = int(self.days) | ||
| hours_f = round(self.hours + 24 * (self.days - days), 11) | ||
| hours = int(hours_f) | ||
| minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) | ||
| minutes = int(minutes_f) | ||
| seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) | ||
| seconds = int(seconds_f) | ||
| microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) | ||
| # Constructor carries overflow back up with call to _fix() | ||
| return self.__class__(years=self.years, months=self.months, | ||
| days=days, hours=hours, minutes=minutes, | ||
| seconds=seconds, microseconds=microseconds, | ||
| leapdays=self.leapdays, year=self.year, | ||
| month=self.month, day=self.day, | ||
| weekday=self.weekday, hour=self.hour, | ||
| minute=self.minute, second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __add__(self, other): | ||
| if isinstance(other, relativedelta): | ||
| return self.__class__(years=other.years + self.years, | ||
| months=other.months + self.months, | ||
| days=other.days + self.days, | ||
| hours=other.hours + self.hours, | ||
| minutes=other.minutes + self.minutes, | ||
| seconds=other.seconds + self.seconds, | ||
| microseconds=(other.microseconds + | ||
| self.microseconds), | ||
| leapdays=other.leapdays or self.leapdays, | ||
| year=(other.year if other.year is not None | ||
| else self.year), | ||
| month=(other.month if other.month is not None | ||
| else self.month), | ||
| day=(other.day if other.day is not None | ||
| else self.day), | ||
| weekday=(other.weekday if other.weekday is not None | ||
| else self.weekday), | ||
| hour=(other.hour if other.hour is not None | ||
| else self.hour), | ||
| minute=(other.minute if other.minute is not None | ||
| else self.minute), | ||
| second=(other.second if other.second is not None | ||
| else self.second), | ||
| microsecond=(other.microsecond if other.microsecond | ||
| is not None else | ||
| self.microsecond)) | ||
| if isinstance(other, datetime.timedelta): | ||
| return self.__class__(years=self.years, | ||
| months=self.months, | ||
| days=self.days + other.days, | ||
| hours=self.hours, | ||
| minutes=self.minutes, | ||
| seconds=self.seconds + other.seconds, | ||
| microseconds=self.microseconds + other.microseconds, | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| if not isinstance(other, datetime.date): | ||
| return NotImplemented | ||
| elif self._has_time and not isinstance(other, datetime.datetime): | ||
| other = datetime.datetime.fromordinal(other.toordinal()) | ||
| year = (self.year or other.year)+self.years | ||
| month = self.month or other.month | ||
| if self.months: | ||
| assert 1 <= abs(self.months) <= 12 | ||
| month += self.months | ||
| if month > 12: | ||
| year += 1 | ||
| month -= 12 | ||
| elif month < 1: | ||
| year -= 1 | ||
| month += 12 | ||
| day = min(calendar.monthrange(year, month)[1], | ||
| self.day or other.day) | ||
| repl = {"year": year, "month": month, "day": day} | ||
| for attr in ["hour", "minute", "second", "microsecond"]: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| repl[attr] = value | ||
| days = self.days | ||
| if self.leapdays and month > 2 and calendar.isleap(year): | ||
| days += self.leapdays | ||
| ret = (other.replace(**repl) | ||
| + datetime.timedelta(days=days, | ||
| hours=self.hours, | ||
| minutes=self.minutes, | ||
| seconds=self.seconds, | ||
| microseconds=self.microseconds)) | ||
| if self.weekday: | ||
| weekday, nth = self.weekday.weekday, self.weekday.n or 1 | ||
| jumpdays = (abs(nth) - 1) * 7 | ||
| if nth > 0: | ||
| jumpdays += (7 - ret.weekday() + weekday) % 7 | ||
| else: | ||
| jumpdays += (ret.weekday() - weekday) % 7 | ||
| jumpdays *= -1 | ||
| ret += datetime.timedelta(days=jumpdays) | ||
| return ret | ||
| def __radd__(self, other): | ||
| return self.__add__(other) | ||
| def __rsub__(self, other): | ||
| return self.__neg__().__radd__(other) | ||
| def __sub__(self, other): | ||
| if not isinstance(other, relativedelta): | ||
| return NotImplemented # In case the other object defines __rsub__ | ||
| return self.__class__(years=self.years - other.years, | ||
| months=self.months - other.months, | ||
| days=self.days - other.days, | ||
| hours=self.hours - other.hours, | ||
| minutes=self.minutes - other.minutes, | ||
| seconds=self.seconds - other.seconds, | ||
| microseconds=self.microseconds - other.microseconds, | ||
| leapdays=self.leapdays or other.leapdays, | ||
| year=(self.year if self.year is not None | ||
| else other.year), | ||
| month=(self.month if self.month is not None else | ||
| other.month), | ||
| day=(self.day if self.day is not None else | ||
| other.day), | ||
| weekday=(self.weekday if self.weekday is not None else | ||
| other.weekday), | ||
| hour=(self.hour if self.hour is not None else | ||
| other.hour), | ||
| minute=(self.minute if self.minute is not None else | ||
| other.minute), | ||
| second=(self.second if self.second is not None else | ||
| other.second), | ||
| microsecond=(self.microsecond if self.microsecond | ||
| is not None else | ||
| other.microsecond)) | ||
| def __abs__(self): | ||
| return self.__class__(years=abs(self.years), | ||
| months=abs(self.months), | ||
| days=abs(self.days), | ||
| hours=abs(self.hours), | ||
| minutes=abs(self.minutes), | ||
| seconds=abs(self.seconds), | ||
| microseconds=abs(self.microseconds), | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __neg__(self): | ||
| return self.__class__(years=-self.years, | ||
| months=-self.months, | ||
| days=-self.days, | ||
| hours=-self.hours, | ||
| minutes=-self.minutes, | ||
| seconds=-self.seconds, | ||
| microseconds=-self.microseconds, | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __bool__(self): | ||
| return not (not self.years and | ||
| not self.months and | ||
| not self.days and | ||
| not self.hours and | ||
| not self.minutes and | ||
| not self.seconds and | ||
| not self.microseconds and | ||
| not self.leapdays and | ||
| self.year is None and | ||
| self.month is None and | ||
| self.day is None and | ||
| self.weekday is None and | ||
| self.hour is None and | ||
| self.minute is None and | ||
| self.second is None and | ||
| self.microsecond is None) | ||
| # Compatibility with Python 2.x | ||
| __nonzero__ = __bool__ | ||
| def __mul__(self, other): | ||
| try: | ||
| f = float(other) | ||
| except TypeError: | ||
| return NotImplemented | ||
| return self.__class__(years=int(self.years * f), | ||
| months=int(self.months * f), | ||
| days=int(self.days * f), | ||
| hours=int(self.hours * f), | ||
| minutes=int(self.minutes * f), | ||
| seconds=int(self.seconds * f), | ||
| microseconds=int(self.microseconds * f), | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| __rmul__ = __mul__ | ||
| def __eq__(self, other): | ||
| if not isinstance(other, relativedelta): | ||
| return NotImplemented | ||
| if self.weekday or other.weekday: | ||
| if not self.weekday or not other.weekday: | ||
| return False | ||
| if self.weekday.weekday != other.weekday.weekday: | ||
| return False | ||
| n1, n2 = self.weekday.n, other.weekday.n | ||
| if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): | ||
| return False | ||
| return (self.years == other.years and | ||
| self.months == other.months and | ||
| self.days == other.days and | ||
| self.hours == other.hours and | ||
| self.minutes == other.minutes and | ||
| self.seconds == other.seconds and | ||
| self.microseconds == other.microseconds and | ||
| self.leapdays == other.leapdays and | ||
| self.year == other.year and | ||
| self.month == other.month and | ||
| self.day == other.day and | ||
| self.hour == other.hour and | ||
| self.minute == other.minute and | ||
| self.second == other.second and | ||
| self.microsecond == other.microsecond) | ||
| def __hash__(self): | ||
| return hash(( | ||
| self.weekday, | ||
| self.years, | ||
| self.months, | ||
| self.days, | ||
| self.hours, | ||
| self.minutes, | ||
| self.seconds, | ||
| self.microseconds, | ||
| self.leapdays, | ||
| self.year, | ||
| self.month, | ||
| self.day, | ||
| self.hour, | ||
| self.minute, | ||
| self.second, | ||
| self.microsecond, | ||
| )) | ||
| def __ne__(self, other): | ||
| return not self.__eq__(other) | ||
| def __div__(self, other): | ||
| try: | ||
| reciprocal = 1 / float(other) | ||
| except TypeError: | ||
| return NotImplemented | ||
| return self.__mul__(reciprocal) | ||
| __truediv__ = __div__ | ||
| def __repr__(self): | ||
| l = [] | ||
| for attr in ["years", "months", "days", "leapdays", | ||
| "hours", "minutes", "seconds", "microseconds"]: | ||
| value = getattr(self, attr) | ||
| if value: | ||
| l.append("{attr}={value:+g}".format(attr=attr, value=value)) | ||
| for attr in ["year", "month", "day", "weekday", | ||
| "hour", "minute", "second", "microsecond"]: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| l.append("{attr}={value}".format(attr=attr, value=repr(value))) | ||
| return "{classname}({attrs})".format(classname=self.__class__.__name__, | ||
| attrs=", ".join(l)) | ||
| def _sign(x): | ||
| return int(copysign(1, x)) | ||
| # vim:ts=4:sw=4:et |
Sorry, the diff of this file is too big to display
| # -*- coding: utf-8 -*- | ||
| from .tz import * | ||
| from .tz import __doc__ | ||
| __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", | ||
| "enfold", "datetime_ambiguous", "datetime_exists", | ||
| "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] | ||
| class DeprecatedTzFormatWarning(Warning): | ||
| """Warning raised when time zones are parsed from deprecated formats.""" |
| from six import PY2 | ||
| from functools import wraps | ||
| from datetime import datetime, timedelta, tzinfo | ||
| ZERO = timedelta(0) | ||
| __all__ = ['tzname_in_python2', 'enfold'] | ||
| def tzname_in_python2(namefunc): | ||
| """Change unicode output into bytestrings in Python 2 | ||
| tzname() API changed in Python 3. It used to return bytes, but was changed | ||
| to unicode strings | ||
| """ | ||
| if PY2: | ||
| @wraps(namefunc) | ||
| def adjust_encoding(*args, **kwargs): | ||
| name = namefunc(*args, **kwargs) | ||
| if name is not None: | ||
| name = name.encode() | ||
| return name | ||
| return adjust_encoding | ||
| else: | ||
| return namefunc | ||
| # The following is adapted from Alexander Belopolsky's tz library | ||
| # https://github.com/abalkin/tz | ||
| if hasattr(datetime, 'fold'): | ||
| # This is the pre-python 3.6 fold situation | ||
| def enfold(dt, fold=1): | ||
| """ | ||
| Provides a unified interface for assigning the ``fold`` attribute to | ||
| datetimes both before and after the implementation of PEP-495. | ||
| :param fold: | ||
| The value for the ``fold`` attribute in the returned datetime. This | ||
| should be either 0 or 1. | ||
| :return: | ||
| Returns an object for which ``getattr(dt, 'fold', 0)`` returns | ||
| ``fold`` for all versions of Python. In versions prior to | ||
| Python 3.6, this is a ``_DatetimeWithFold`` object, which is a | ||
| subclass of :py:class:`datetime.datetime` with the ``fold`` | ||
| attribute added, if ``fold`` is 1. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| return dt.replace(fold=fold) | ||
| else: | ||
| class _DatetimeWithFold(datetime): | ||
| """ | ||
| This is a class designed to provide a PEP 495-compliant interface for | ||
| Python versions before 3.6. It is used only for dates in a fold, so | ||
| the ``fold`` attribute is fixed at ``1``. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| __slots__ = () | ||
| def replace(self, *args, **kwargs): | ||
| """ | ||
| Return a datetime with the same attributes, except for those | ||
| attributes given new values by whichever keyword arguments are | ||
| specified. Note that tzinfo=None can be specified to create a naive | ||
| datetime from an aware datetime with no conversion of date and time | ||
| data. | ||
| This is reimplemented in ``_DatetimeWithFold`` because pypy3 will | ||
| return a ``datetime.datetime`` even if ``fold`` is unchanged. | ||
| """ | ||
| argnames = ( | ||
| 'year', 'month', 'day', 'hour', 'minute', 'second', | ||
| 'microsecond', 'tzinfo' | ||
| ) | ||
| for arg, argname in zip(args, argnames): | ||
| if argname in kwargs: | ||
| raise TypeError('Duplicate argument: {}'.format(argname)) | ||
| kwargs[argname] = arg | ||
| for argname in argnames: | ||
| if argname not in kwargs: | ||
| kwargs[argname] = getattr(self, argname) | ||
| dt_class = self.__class__ if kwargs.get('fold', 1) else datetime | ||
| return dt_class(**kwargs) | ||
| @property | ||
| def fold(self): | ||
| return 1 | ||
| def enfold(dt, fold=1): | ||
| """ | ||
| Provides a unified interface for assigning the ``fold`` attribute to | ||
| datetimes both before and after the implementation of PEP-495. | ||
| :param fold: | ||
| The value for the ``fold`` attribute in the returned datetime. This | ||
| should be either 0 or 1. | ||
| :return: | ||
| Returns an object for which ``getattr(dt, 'fold', 0)`` returns | ||
| ``fold`` for all versions of Python. In versions prior to | ||
| Python 3.6, this is a ``_DatetimeWithFold`` object, which is a | ||
| subclass of :py:class:`datetime.datetime` with the ``fold`` | ||
| attribute added, if ``fold`` is 1. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| if getattr(dt, 'fold', 0) == fold: | ||
| return dt | ||
| args = dt.timetuple()[:6] | ||
| args += (dt.microsecond, dt.tzinfo) | ||
| if fold: | ||
| return _DatetimeWithFold(*args) | ||
| else: | ||
| return datetime(*args) | ||
| def _validate_fromutc_inputs(f): | ||
| """ | ||
| The CPython version of ``fromutc`` checks that the input is a ``datetime`` | ||
| object and that ``self`` is attached as its ``tzinfo``. | ||
| """ | ||
| @wraps(f) | ||
| def fromutc(self, dt): | ||
| if not isinstance(dt, datetime): | ||
| raise TypeError("fromutc() requires a datetime argument") | ||
| if dt.tzinfo is not self: | ||
| raise ValueError("dt.tzinfo is not self") | ||
| return f(self, dt) | ||
| return fromutc | ||
| class _tzinfo(tzinfo): | ||
| """ | ||
| Base class for all ``dateutil`` ``tzinfo`` objects. | ||
| """ | ||
| def is_ambiguous(self, dt): | ||
| """ | ||
| Whether or not the "wall time" of a given datetime is ambiguous in this | ||
| zone. | ||
| :param dt: | ||
| A :py:class:`datetime.datetime`, naive or time zone aware. | ||
| :return: | ||
| Returns ``True`` if ambiguous, ``False`` otherwise. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| dt = dt.replace(tzinfo=self) | ||
| wall_0 = enfold(dt, fold=0) | ||
| wall_1 = enfold(dt, fold=1) | ||
| same_offset = wall_0.utcoffset() == wall_1.utcoffset() | ||
| same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) | ||
| return same_dt and not same_offset | ||
| def _fold_status(self, dt_utc, dt_wall): | ||
| """ | ||
| Determine the fold status of a "wall" datetime, given a representation | ||
| of the same datetime as a (naive) UTC datetime. This is calculated based | ||
| on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all | ||
| datetimes, and that this offset is the actual number of hours separating | ||
| ``dt_utc`` and ``dt_wall``. | ||
| :param dt_utc: | ||
| Representation of the datetime as UTC | ||
| :param dt_wall: | ||
| Representation of the datetime as "wall time". This parameter must | ||
| either have a `fold` attribute or have a fold-naive | ||
| :class:`datetime.tzinfo` attached, otherwise the calculation may | ||
| fail. | ||
| """ | ||
| if self.is_ambiguous(dt_wall): | ||
| delta_wall = dt_wall - dt_utc | ||
| _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) | ||
| else: | ||
| _fold = 0 | ||
| return _fold | ||
| def _fold(self, dt): | ||
| return getattr(dt, 'fold', 0) | ||
| def _fromutc(self, dt): | ||
| """ | ||
| Given a timezone-aware datetime in a given timezone, calculates a | ||
| timezone-aware datetime in a new timezone. | ||
| Since this is the one time that we *know* we have an unambiguous | ||
| datetime object, we take this opportunity to determine whether the | ||
| datetime is ambiguous and in a "fold" state (e.g. if it's the first | ||
| occurrence, chronologically, of the ambiguous datetime). | ||
| :param dt: | ||
| A timezone-aware :class:`datetime.datetime` object. | ||
| """ | ||
| # Re-implement the algorithm from Python's datetime.py | ||
| dtoff = dt.utcoffset() | ||
| if dtoff is None: | ||
| raise ValueError("fromutc() requires a non-None utcoffset() " | ||
| "result") | ||
| # The original datetime.py code assumes that `dst()` defaults to | ||
| # zero during ambiguous times. PEP 495 inverts this presumption, so | ||
| # for pre-PEP 495 versions of python, we need to tweak the algorithm. | ||
| dtdst = dt.dst() | ||
| if dtdst is None: | ||
| raise ValueError("fromutc() requires a non-None dst() result") | ||
| delta = dtoff - dtdst | ||
| dt += delta | ||
| # Set fold=1 so we can default to being in the fold for | ||
| # ambiguous dates. | ||
| dtdst = enfold(dt, fold=1).dst() | ||
| if dtdst is None: | ||
| raise ValueError("fromutc(): dt.dst gave inconsistent " | ||
| "results; cannot convert") | ||
| return dt + dtdst | ||
| @_validate_fromutc_inputs | ||
| def fromutc(self, dt): | ||
| """ | ||
| Given a timezone-aware datetime in a given timezone, calculates a | ||
| timezone-aware datetime in a new timezone. | ||
| Since this is the one time that we *know* we have an unambiguous | ||
| datetime object, we take this opportunity to determine whether the | ||
| datetime is ambiguous and in a "fold" state (e.g. if it's the first | ||
| occurrence, chronologically, of the ambiguous datetime). | ||
| :param dt: | ||
| A timezone-aware :class:`datetime.datetime` object. | ||
| """ | ||
| dt_wall = self._fromutc(dt) | ||
| # Calculate the fold status given the two datetimes. | ||
| _fold = self._fold_status(dt, dt_wall) | ||
| # Set the default fold value for ambiguous dates | ||
| return enfold(dt_wall, fold=_fold) | ||
| class tzrangebase(_tzinfo): | ||
| """ | ||
| This is an abstract base class for time zones represented by an annual | ||
| transition into and out of DST. Child classes should implement the following | ||
| methods: | ||
| * ``__init__(self, *args, **kwargs)`` | ||
| * ``transitions(self, year)`` - this is expected to return a tuple of | ||
| datetimes representing the DST on and off transitions in standard | ||
| time. | ||
| A fully initialized ``tzrangebase`` subclass should also provide the | ||
| following attributes: | ||
| * ``hasdst``: Boolean whether or not the zone uses DST. | ||
| * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects | ||
| representing the respective UTC offsets. | ||
| * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short | ||
| abbreviations in DST and STD, respectively. | ||
| * ``_hasdst``: Whether or not the zone has DST. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| def __init__(self): | ||
| raise NotImplementedError('tzrangebase is an abstract base class') | ||
| def utcoffset(self, dt): | ||
| isdst = self._isdst(dt) | ||
| if isdst is None: | ||
| return None | ||
| elif isdst: | ||
| return self._dst_offset | ||
| else: | ||
| return self._std_offset | ||
| def dst(self, dt): | ||
| isdst = self._isdst(dt) | ||
| if isdst is None: | ||
| return None | ||
| elif isdst: | ||
| return self._dst_base_offset | ||
| else: | ||
| return ZERO | ||
| @tzname_in_python2 | ||
| def tzname(self, dt): | ||
| if self._isdst(dt): | ||
| return self._dst_abbr | ||
| else: | ||
| return self._std_abbr | ||
| def fromutc(self, dt): | ||
| """ Given a datetime in UTC, return local time """ | ||
| if not isinstance(dt, datetime): | ||
| raise TypeError("fromutc() requires a datetime argument") | ||
| if dt.tzinfo is not self: | ||
| raise ValueError("dt.tzinfo is not self") | ||
| # Get transitions - if there are none, fixed offset | ||
| transitions = self.transitions(dt.year) | ||
| if transitions is None: | ||
| return dt + self.utcoffset(dt) | ||
| # Get the transition times in UTC | ||
| dston, dstoff = transitions | ||
| dston -= self._std_offset | ||
| dstoff -= self._std_offset | ||
| utc_transitions = (dston, dstoff) | ||
| dt_utc = dt.replace(tzinfo=None) | ||
| isdst = self._naive_isdst(dt_utc, utc_transitions) | ||
| if isdst: | ||
| dt_wall = dt + self._dst_offset | ||
| else: | ||
| dt_wall = dt + self._std_offset | ||
| _fold = int(not isdst and self.is_ambiguous(dt_wall)) | ||
| return enfold(dt_wall, fold=_fold) | ||
| def is_ambiguous(self, dt): | ||
| """ | ||
| Whether or not the "wall time" of a given datetime is ambiguous in this | ||
| zone. | ||
| :param dt: | ||
| A :py:class:`datetime.datetime`, naive or time zone aware. | ||
| :return: | ||
| Returns ``True`` if ambiguous, ``False`` otherwise. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| if not self.hasdst: | ||
| return False | ||
| start, end = self.transitions(dt.year) | ||
| dt = dt.replace(tzinfo=None) | ||
| return (end <= dt < end + self._dst_base_offset) | ||
| def _isdst(self, dt): | ||
| if not self.hasdst: | ||
| return False | ||
| elif dt is None: | ||
| return None | ||
| transitions = self.transitions(dt.year) | ||
| if transitions is None: | ||
| return False | ||
| dt = dt.replace(tzinfo=None) | ||
| isdst = self._naive_isdst(dt, transitions) | ||
| # Handle ambiguous dates | ||
| if not isdst and self.is_ambiguous(dt): | ||
| return not self._fold(dt) | ||
| else: | ||
| return isdst | ||
| def _naive_isdst(self, dt, transitions): | ||
| dston, dstoff = transitions | ||
| dt = dt.replace(tzinfo=None) | ||
| if dston < dstoff: | ||
| isdst = dston <= dt < dstoff | ||
| else: | ||
| isdst = not dstoff <= dt < dston | ||
| return isdst | ||
| @property | ||
| def _dst_base_offset(self): | ||
| return self._dst_offset - self._std_offset | ||
| __hash__ = None | ||
| def __ne__(self, other): | ||
| return not (self == other) | ||
| def __repr__(self): | ||
| return "%s(...)" % self.__class__.__name__ | ||
| __reduce__ = object.__reduce__ |
| from datetime import timedelta | ||
| import weakref | ||
| from collections import OrderedDict | ||
| from six.moves import _thread | ||
| class _TzSingleton(type): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instance = None | ||
| super(_TzSingleton, cls).__init__(*args, **kwargs) | ||
| def __call__(cls): | ||
| if cls.__instance is None: | ||
| cls.__instance = super(_TzSingleton, cls).__call__() | ||
| return cls.__instance | ||
| class _TzFactory(type): | ||
| def instance(cls, *args, **kwargs): | ||
| """Alternate constructor that returns a fresh instance""" | ||
| return type.__call__(cls, *args, **kwargs) | ||
| class _TzOffsetFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
| cls._cache_lock = _thread.allocate_lock() | ||
| def __call__(cls, name, offset): | ||
| if isinstance(offset, timedelta): | ||
| key = (name, offset.total_seconds()) | ||
| else: | ||
| key = (name, offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(name, offset)) | ||
| # This lock may not be necessary in Python 3. See GH issue #901 | ||
| with cls._cache_lock: | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
| class _TzStrFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
| cls.__cache_lock = _thread.allocate_lock() | ||
| def __call__(cls, s, posix_offset=False): | ||
| key = (s, posix_offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(s, posix_offset)) | ||
| # This lock may not be necessary in Python 3. See GH issue #901 | ||
| with cls.__cache_lock: | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
| # -*- 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 weakref | ||
| from collections import OrderedDict | ||
| 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 | ||
| # For warning about rounding tzinfo | ||
| from warnings import warn | ||
| ZERO = datetime.timedelta(0) | ||
| EPOCH = datetime.datetime(1970, 1, 1, 0, 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__ | ||
| #: Convenience constant providing a :class:`tzutc()` instance | ||
| #: | ||
| #: .. versionadded:: 2.7.0 | ||
| UTC = tzutc() | ||
| @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=_get_supported_offset(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 that 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 | ||
| <https://data.iana.org/time-zones/tz-link.html>`_ for more information. | ||
| Time zone files can be compiled from the `IANA Time Zone database files | ||
| <https://www.iana.org/time-zones>`_ with the `zic time zone compiler | ||
| <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ | ||
| .. 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 = _nullcontext(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] | ||
| gmtoff = _get_supported_offset(gmtoff) | ||
| 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. | ||
| lastdst = None | ||
| lastoffset = None | ||
| lastdstoffset = None | ||
| lastbaseoffset = None | ||
| out.trans_list = [] | ||
| for i, tti in enumerate(out.trans_idx): | ||
| offset = tti.offset | ||
| dstoffset = 0 | ||
| if lastdst is not None: | ||
| if tti.isdst: | ||
| if not lastdst: | ||
| dstoffset = offset - lastoffset | ||
| if not dstoffset and lastdstoffset: | ||
| dstoffset = lastdstoffset | ||
| tti.dstoffset = datetime.timedelta(seconds=dstoffset) | ||
| lastdstoffset = dstoffset | ||
| # If a time zone changes its base offset during a DST transition, | ||
| # then you need to adjust by the previous base offset to get the | ||
| # transition time in local time. Otherwise you use the current | ||
| # base offset. Ideally, I would have some mathematical proof of | ||
| # why this is true, but I haven't really thought about it enough. | ||
| baseoffset = offset - dstoffset | ||
| adjustment = baseoffset | ||
| if (lastbaseoffset is not None and baseoffset != lastbaseoffset | ||
| and tti.isdst != lastdst): | ||
| # The base DST has changed | ||
| adjustment = lastbaseoffset | ||
| lastdst = tti.isdst | ||
| lastoffset = offset | ||
| lastbaseoffset = baseoffset | ||
| out.trans_list.append(out.trans_list_utc[i] + adjustment) | ||
| out.trans_idx = tuple(out.trans_idx) | ||
| out.trans_list = tuple(out.trans_list) | ||
| 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 "<tzicalvtz %s>" % 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 = _nullcontext(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 = weakref.WeakValueDictionary() | ||
| self.__strong_cache_size = 8 | ||
| self.__strong_cache = OrderedDict() | ||
| 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) | ||
| or rv is None): | ||
| # tzlocal is slightly more complicated than the other | ||
| # time zone providers because it depends on environment | ||
| # at construction time, so don't cache that. | ||
| # | ||
| # We also cannot store weak references to None, so we | ||
| # will also not store that. | ||
| self.__instances[name] = rv | ||
| else: | ||
| # No need for strong caching, return immediately | ||
| return rv | ||
| self.__strong_cache[name] = self.__strong_cache.pop(name, rv) | ||
| if len(self.__strong_cache) > self.__strong_cache_size: | ||
| self.__strong_cache.popitem(last=False) | ||
| return rv | ||
| def set_cache_size(self, size): | ||
| with self._cache_lock: | ||
| self.__strong_cache_size = size | ||
| while len(self.__strong_cache) > size: | ||
| self.__strong_cache.popitem(last=False) | ||
| def cache_clear(self): | ||
| with self._cache_lock: | ||
| self.__instances = weakref.WeakValueDictionary() | ||
| self.__strong_cache.clear() | ||
| @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 in ("", ":"): | ||
| 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: | ||
| try: | ||
| if name.startswith(":"): | ||
| name = name[1:] | ||
| except TypeError as e: | ||
| if isinstance(name, bytes): | ||
| new_msg = "gettz argument should be str, not bytes" | ||
| six.raise_from(TypeError(new_msg), e) | ||
| else: | ||
| raise | ||
| 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, UnicodeEncodeError): | ||
| # UnicodeEncodeError is for Python 2.7 compat | ||
| 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 = UTC | ||
| 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(UTC).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() | ||
| if sys.version_info >= (3, 6): | ||
| def _get_supported_offset(second_offset): | ||
| return second_offset | ||
| else: | ||
| def _get_supported_offset(second_offset): | ||
| # For python pre-3.6, round to full-minutes if that's not the case. | ||
| # Python's datetime doesn't accept sub-minute timezones. Check | ||
| # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 | ||
| # for some information. | ||
| old_offset = second_offset | ||
| calculated_offset = 60 * ((second_offset + 30) // 60) | ||
| return calculated_offset | ||
| try: | ||
| # Python 3.7 feature | ||
| from contextlib import nullcontext as _nullcontext | ||
| except ImportError: | ||
| class _nullcontext(object): | ||
| """ | ||
| Class for wrapping contexts so that they are passed through in a | ||
| with statement. | ||
| """ | ||
| def __init__(self, context): | ||
| self.context = context | ||
| def __enter__(self): | ||
| return self.context | ||
| def __exit__(*args, **kwargs): | ||
| pass | ||
| # vim:ts=4:sw=4:et |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module provides an interface to the native time zone data on Windows, | ||
| including :py:class:`datetime.tzinfo` implementations. | ||
| Attempting to import this module on a non-Windows platform will raise an | ||
| :py:obj:`ImportError`. | ||
| """ | ||
| # This code was originally contributed by Jeffrey Harris. | ||
| import datetime | ||
| import struct | ||
| from six.moves import winreg | ||
| from six import text_type | ||
| try: | ||
| import ctypes | ||
| from ctypes import wintypes | ||
| except ValueError: | ||
| # ValueError is raised on non-Windows systems for some horrible reason. | ||
| raise ImportError("Running tzwin on non-Windows system") | ||
| from ._common import tzrangebase | ||
| __all__ = ["tzwin", "tzwinlocal", "tzres"] | ||
| ONEWEEK = datetime.timedelta(7) | ||
| TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" | ||
| TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" | ||
| TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" | ||
| def _settzkeyname(): | ||
| handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) | ||
| try: | ||
| winreg.OpenKey(handle, TZKEYNAMENT).Close() | ||
| TZKEYNAME = TZKEYNAMENT | ||
| except WindowsError: | ||
| TZKEYNAME = TZKEYNAME9X | ||
| handle.Close() | ||
| return TZKEYNAME | ||
| TZKEYNAME = _settzkeyname() | ||
| class tzres(object): | ||
| """ | ||
| Class for accessing ``tzres.dll``, which contains timezone name related | ||
| resources. | ||
| .. versionadded:: 2.5.0 | ||
| """ | ||
| p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char | ||
| def __init__(self, tzres_loc='tzres.dll'): | ||
| # Load the user32 DLL so we can load strings from tzres | ||
| user32 = ctypes.WinDLL('user32') | ||
| # Specify the LoadStringW function | ||
| user32.LoadStringW.argtypes = (wintypes.HINSTANCE, | ||
| wintypes.UINT, | ||
| wintypes.LPWSTR, | ||
| ctypes.c_int) | ||
| self.LoadStringW = user32.LoadStringW | ||
| self._tzres = ctypes.WinDLL(tzres_loc) | ||
| self.tzres_loc = tzres_loc | ||
| def load_name(self, offset): | ||
| """ | ||
| Load a timezone name from a DLL offset (integer). | ||
| >>> from dateutil.tzwin import tzres | ||
| >>> tzr = tzres() | ||
| >>> print(tzr.load_name(112)) | ||
| 'Eastern Standard Time' | ||
| :param offset: | ||
| A positive integer value referring to a string from the tzres dll. | ||
| .. note:: | ||
| Offsets found in the registry are generally of the form | ||
| ``@tzres.dll,-114``. The offset in this case is 114, not -114. | ||
| """ | ||
| resource = self.p_wchar() | ||
| lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) | ||
| nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) | ||
| return resource[:nchar] | ||
| def name_from_string(self, tzname_str): | ||
| """ | ||
| Parse strings as returned from the Windows registry into the time zone | ||
| name as defined in the registry. | ||
| >>> from dateutil.tzwin import tzres | ||
| >>> tzr = tzres() | ||
| >>> print(tzr.name_from_string('@tzres.dll,-251')) | ||
| 'Dateline Daylight Time' | ||
| >>> print(tzr.name_from_string('Eastern Standard Time')) | ||
| 'Eastern Standard Time' | ||
| :param tzname_str: | ||
| A timezone name string as returned from a Windows registry key. | ||
| :return: | ||
| Returns the localized timezone string from tzres.dll if the string | ||
| is of the form `@tzres.dll,-offset`, else returns the input string. | ||
| """ | ||
| if not tzname_str.startswith('@'): | ||
| return tzname_str | ||
| name_splt = tzname_str.split(',-') | ||
| try: | ||
| offset = int(name_splt[1]) | ||
| except: | ||
| raise ValueError("Malformed timezone string.") | ||
| return self.load_name(offset) | ||
| class tzwinbase(tzrangebase): | ||
| """tzinfo class based on win32's timezones available in the registry.""" | ||
| def __init__(self): | ||
| raise NotImplementedError('tzwinbase is an abstract base class') | ||
| def __eq__(self, other): | ||
| # Compare on all relevant dimensions, including name. | ||
| if not isinstance(other, tzwinbase): | ||
| return NotImplemented | ||
| return (self._std_offset == other._std_offset and | ||
| self._dst_offset == other._dst_offset and | ||
| self._stddayofweek == other._stddayofweek and | ||
| self._dstdayofweek == other._dstdayofweek and | ||
| self._stdweeknumber == other._stdweeknumber and | ||
| self._dstweeknumber == other._dstweeknumber and | ||
| self._stdhour == other._stdhour and | ||
| self._dsthour == other._dsthour and | ||
| self._stdminute == other._stdminute and | ||
| self._dstminute == other._dstminute and | ||
| self._std_abbr == other._std_abbr and | ||
| self._dst_abbr == other._dst_abbr) | ||
| @staticmethod | ||
| def list(): | ||
| """Return a list of all time zones known to the system.""" | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| with winreg.OpenKey(handle, TZKEYNAME) as tzkey: | ||
| result = [winreg.EnumKey(tzkey, i) | ||
| for i in range(winreg.QueryInfoKey(tzkey)[0])] | ||
| return result | ||
| def display(self): | ||
| """ | ||
| Return the display name of the time zone. | ||
| """ | ||
| return self._display | ||
| def transitions(self, year): | ||
| """ | ||
| For a given year, get the DST on and off transition times, expressed | ||
| always on the standard time side. For zones with no transitions, this | ||
| function returns ``None``. | ||
| :param year: | ||
| The year whose transitions you would like to query. | ||
| :return: | ||
| Returns a :class:`tuple` of :class:`datetime.datetime` objects, | ||
| ``(dston, dstoff)`` for zones with an annual DST transition, or | ||
| ``None`` for fixed offset zones. | ||
| """ | ||
| if not self.hasdst: | ||
| return None | ||
| dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, | ||
| self._dsthour, self._dstminute, | ||
| self._dstweeknumber) | ||
| dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, | ||
| self._stdhour, self._stdminute, | ||
| self._stdweeknumber) | ||
| # Ambiguous dates default to the STD side | ||
| dstoff -= self._dst_base_offset | ||
| return dston, dstoff | ||
| def _get_hasdst(self): | ||
| return self._dstmonth != 0 | ||
| @property | ||
| def _dst_base_offset(self): | ||
| return self._dst_base_offset_ | ||
| class tzwin(tzwinbase): | ||
| """ | ||
| Time zone object created from the zone info in the Windows registry | ||
| These are similar to :py:class:`dateutil.tz.tzrange` objects in that | ||
| the time zone data is provided in the format of a single offset rule | ||
| for either 0 or 2 time zone transitions per year. | ||
| :param: name | ||
| The name of a Windows time zone key, e.g. "Eastern Standard Time". | ||
| The full list of keys can be retrieved with :func:`tzwin.list`. | ||
| """ | ||
| def __init__(self, name): | ||
| self._name = name | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) | ||
| with winreg.OpenKey(handle, tzkeyname) as tzkey: | ||
| keydict = valuestodict(tzkey) | ||
| self._std_abbr = keydict["Std"] | ||
| self._dst_abbr = keydict["Dlt"] | ||
| self._display = keydict["Display"] | ||
| # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm | ||
| tup = struct.unpack("=3l16h", keydict["TZI"]) | ||
| stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 | ||
| dstoffset = stdoffset-tup[2] # + DaylightBias * -1 | ||
| self._std_offset = datetime.timedelta(minutes=stdoffset) | ||
| self._dst_offset = datetime.timedelta(minutes=dstoffset) | ||
| # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs | ||
| # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx | ||
| (self._stdmonth, | ||
| self._stddayofweek, # Sunday = 0 | ||
| self._stdweeknumber, # Last = 5 | ||
| self._stdhour, | ||
| self._stdminute) = tup[4:9] | ||
| (self._dstmonth, | ||
| self._dstdayofweek, # Sunday = 0 | ||
| self._dstweeknumber, # Last = 5 | ||
| self._dsthour, | ||
| self._dstminute) = tup[12:17] | ||
| self._dst_base_offset_ = self._dst_offset - self._std_offset | ||
| self.hasdst = self._get_hasdst() | ||
| def __repr__(self): | ||
| return "tzwin(%s)" % repr(self._name) | ||
| def __reduce__(self): | ||
| return (self.__class__, (self._name,)) | ||
| class tzwinlocal(tzwinbase): | ||
| """ | ||
| Class representing the local time zone information in the Windows registry | ||
| While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` | ||
| module) to retrieve time zone information, ``tzwinlocal`` retrieves the | ||
| rules directly from the Windows registry and creates an object like | ||
| :class:`dateutil.tz.tzwin`. | ||
| Because Windows does not have an equivalent of :func:`time.tzset`, on | ||
| Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the | ||
| time zone settings *at the time that the process was started*, meaning | ||
| changes to the machine's time zone settings during the run of a program | ||
| on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. | ||
| Because ``tzwinlocal`` reads the registry directly, it is unaffected by | ||
| this issue. | ||
| """ | ||
| def __init__(self): | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: | ||
| keydict = valuestodict(tzlocalkey) | ||
| self._std_abbr = keydict["StandardName"] | ||
| self._dst_abbr = keydict["DaylightName"] | ||
| try: | ||
| tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, | ||
| sn=self._std_abbr) | ||
| with winreg.OpenKey(handle, tzkeyname) as tzkey: | ||
| _keydict = valuestodict(tzkey) | ||
| self._display = _keydict["Display"] | ||
| except OSError: | ||
| self._display = None | ||
| stdoffset = -keydict["Bias"]-keydict["StandardBias"] | ||
| dstoffset = stdoffset-keydict["DaylightBias"] | ||
| self._std_offset = datetime.timedelta(minutes=stdoffset) | ||
| self._dst_offset = datetime.timedelta(minutes=dstoffset) | ||
| # For reasons unclear, in this particular key, the day of week has been | ||
| # moved to the END of the SYSTEMTIME structure. | ||
| tup = struct.unpack("=8h", keydict["StandardStart"]) | ||
| (self._stdmonth, | ||
| self._stdweeknumber, # Last = 5 | ||
| self._stdhour, | ||
| self._stdminute) = tup[1:5] | ||
| self._stddayofweek = tup[7] | ||
| tup = struct.unpack("=8h", keydict["DaylightStart"]) | ||
| (self._dstmonth, | ||
| self._dstweeknumber, # Last = 5 | ||
| self._dsthour, | ||
| self._dstminute) = tup[1:5] | ||
| self._dstdayofweek = tup[7] | ||
| self._dst_base_offset_ = self._dst_offset - self._std_offset | ||
| self.hasdst = self._get_hasdst() | ||
| def __repr__(self): | ||
| return "tzwinlocal()" | ||
| def __str__(self): | ||
| # str will return the standard name, not the daylight name. | ||
| return "tzwinlocal(%s)" % repr(self._std_abbr) | ||
| def __reduce__(self): | ||
| return (self.__class__, ()) | ||
| def picknthweekday(year, month, dayofweek, hour, minute, whichweek): | ||
| """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ | ||
| first = datetime.datetime(year, month, 1, hour, minute) | ||
| # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), | ||
| # Because 7 % 7 = 0 | ||
| weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) | ||
| wd = weekdayone + ((whichweek - 1) * ONEWEEK) | ||
| if (wd.month != month): | ||
| wd -= ONEWEEK | ||
| return wd | ||
| def valuestodict(key): | ||
| """Convert a registry key's values to a dictionary.""" | ||
| dout = {} | ||
| size = winreg.QueryInfoKey(key)[1] | ||
| tz_res = None | ||
| for i in range(size): | ||
| key_name, value, dtype = winreg.EnumValue(key, i) | ||
| if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: | ||
| # If it's a DWORD (32-bit integer), it's stored as unsigned - convert | ||
| # that to a proper signed integer | ||
| if value & (1 << 31): | ||
| value = value - (1 << 32) | ||
| elif dtype == winreg.REG_SZ: | ||
| # If it's a reference to the tzres DLL, load the actual string | ||
| if value.startswith('@tzres'): | ||
| tz_res = tz_res or tzres() | ||
| value = tz_res.name_from_string(value) | ||
| value = value.rstrip('\x00') # Remove trailing nulls | ||
| dout[key_name] = value | ||
| return dout |
| # tzwin has moved to dateutil.tz.win | ||
| from .tz.win import * |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers general convenience and utility functions for dealing with | ||
| datetimes. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, time | ||
| def today(tzinfo=None): | ||
| """ | ||
| Returns a :py:class:`datetime` representing the current day at midnight | ||
| :param tzinfo: | ||
| The time zone to attach (also used to determine the current day). | ||
| :return: | ||
| A :py:class:`datetime.datetime` object representing the current day | ||
| at midnight. | ||
| """ | ||
| dt = datetime.now(tzinfo) | ||
| return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) | ||
| def default_tzinfo(dt, tzinfo): | ||
| """ | ||
| Sets the ``tzinfo`` parameter on naive datetimes only | ||
| This is useful for example when you are provided a datetime that may have | ||
| either an implicit or explicit time zone, such as when parsing a time zone | ||
| string. | ||
| .. doctest:: | ||
| >>> from dateutil.tz import tzoffset | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.utils import default_tzinfo | ||
| >>> dflt_tz = tzoffset("EST", -18000) | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) | ||
| 2014-01-01 12:30:00+00:00 | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) | ||
| 2014-01-01 12:30:00-05:00 | ||
| :param dt: | ||
| The datetime on which to replace the time zone | ||
| :param tzinfo: | ||
| The :py:class:`datetime.tzinfo` subclass instance to assign to | ||
| ``dt`` if (and only if) it is naive. | ||
| :return: | ||
| Returns an aware :py:class:`datetime.datetime`. | ||
| """ | ||
| if dt.tzinfo is not None: | ||
| return dt | ||
| else: | ||
| return dt.replace(tzinfo=tzinfo) | ||
| def within_delta(dt1, dt2, delta): | ||
| """ | ||
| Useful for comparing two datetimes that may have a negligible difference | ||
| to be considered equal. | ||
| """ | ||
| delta = abs(delta) | ||
| difference = dt1 - dt2 | ||
| return -delta <= difference <= delta |
| # -*- 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 |
Sorry, the diff of this file is not supported yet
| import logging | ||
| import os | ||
| import tempfile | ||
| import shutil | ||
| import json | ||
| from subprocess import check_call, check_output | ||
| from tarfile import TarFile | ||
| from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME | ||
| def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): | ||
| """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* | ||
| filename is the timezone tarball from ``ftp.iana.org/tz``. | ||
| """ | ||
| tmpdir = tempfile.mkdtemp() | ||
| zonedir = os.path.join(tmpdir, "zoneinfo") | ||
| moduledir = os.path.dirname(__file__) | ||
| try: | ||
| with TarFile.open(filename) as tf: | ||
| for name in zonegroups: | ||
| tf.extract(name, tmpdir) | ||
| filepaths = [os.path.join(tmpdir, n) for n in zonegroups] | ||
| _run_zic(zonedir, filepaths) | ||
| # write metadata file | ||
| with open(os.path.join(zonedir, METADATA_FN), 'w') as f: | ||
| json.dump(metadata, f, indent=4, sort_keys=True) | ||
| target = os.path.join(moduledir, ZONEFILENAME) | ||
| with TarFile.open(target, "w:%s" % format) as tf: | ||
| for entry in os.listdir(zonedir): | ||
| entrypath = os.path.join(zonedir, entry) | ||
| tf.add(entrypath, entry) | ||
| finally: | ||
| shutil.rmtree(tmpdir) | ||
| def _run_zic(zonedir, filepaths): | ||
| """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. | ||
| Recent versions of ``zic`` default to ``-b slim``, while older versions | ||
| don't even have the ``-b`` option (but default to "fat" binaries). The | ||
| current version of dateutil does not support Version 2+ TZif files, which | ||
| causes problems when used in conjunction with "slim" binaries, so this | ||
| function is used to ensure that we always get a "fat" binary. | ||
| """ | ||
| try: | ||
| help_text = check_output(["zic", "--help"]) | ||
| except OSError as e: | ||
| _print_on_nosuchfile(e) | ||
| raise | ||
| if b"-b " in help_text: | ||
| bloat_args = ["-b", "fat"] | ||
| else: | ||
| bloat_args = [] | ||
| check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) | ||
| def _print_on_nosuchfile(e): | ||
| """Print helpful troubleshooting message | ||
| e is an exception raised by subprocess.check_call() | ||
| """ | ||
| if e.errno == 2: | ||
| logging.error( | ||
| "Could not find zic. Perhaps you need to install " | ||
| "libc-bin or some other package that provides it, " | ||
| "or it's not in your PATH?") |
| Metadata-Version: 2.1 | ||
| Name: python-dateutil | ||
| Version: 2.9.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
| Home-page: https://github.com/dateutil/dateutil | ||
| Author: Gustavo Niemeyer | ||
| Author-email: gustavo@niemeyer.net | ||
| Maintainer: Paul Ganssle | ||
| Maintainer-email: dateutil@python.org | ||
| License: Dual License | ||
| Project-URL: Documentation, https://dateutil.readthedocs.io/en/stable/ | ||
| Project-URL: Source, https://github.com/dateutil/dateutil | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: License :: OSI Approved :: Apache Software License | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 2 | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.3 | ||
| Classifier: Programming Language :: Python :: 3.4 | ||
| Classifier: Programming Language :: Python :: 3.5 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,>=2.7 | ||
| Description-Content-Type: text/x-rst | ||
| License-File: LICENSE | ||
| Requires-Dist: six>=1.5 | ||
| dateutil - powerful extensions to datetime | ||
| ========================================== | ||
| |pypi| |support| |licence| | ||
| |gitter| |readthedocs| | ||
| |travis| |appveyor| |pipelines| |coverage| | ||
| .. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: pypi version | ||
| .. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: supported Python version | ||
| .. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build | ||
| :target: https://travis-ci.org/dateutil/dateutil | ||
| :alt: travis build status | ||
| .. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor | ||
| :target: https://ci.appveyor.com/project/dateutil/dateutil | ||
| :alt: appveyor build status | ||
| .. |pipelines| image:: https://dev.azure.com/pythondateutilazure/dateutil/_apis/build/status/dateutil.dateutil?branchName=master | ||
| :target: https://dev.azure.com/pythondateutilazure/dateutil/_build/latest?definitionId=1&branchName=master | ||
| :alt: azure pipelines build status | ||
| .. |coverage| image:: https://codecov.io/gh/dateutil/dateutil/branch/master/graphs/badge.svg?branch=master | ||
| :target: https://codecov.io/gh/dateutil/dateutil?branch=master | ||
| :alt: Code coverage | ||
| .. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg | ||
| :alt: Join the chat at https://gitter.im/dateutil/dateutil | ||
| :target: https://gitter.im/dateutil/dateutil | ||
| .. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: licence | ||
| .. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs | ||
| :alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/ | ||
| :target: https://dateutil.readthedocs.io/en/latest/ | ||
| The `dateutil` module provides powerful extensions to | ||
| the standard `datetime` module, available in Python. | ||
| Installation | ||
| ============ | ||
| `dateutil` can be installed from PyPI using `pip` (note that the package name is | ||
| different from the importable name):: | ||
| pip install python-dateutil | ||
| Download | ||
| ======== | ||
| dateutil is available on PyPI | ||
| https://pypi.org/project/python-dateutil/ | ||
| The documentation is hosted at: | ||
| https://dateutil.readthedocs.io/en/stable/ | ||
| Code | ||
| ==== | ||
| The code and issue tracker are hosted on GitHub: | ||
| https://github.com/dateutil/dateutil/ | ||
| Features | ||
| ======== | ||
| * Computing of relative deltas (next month, next year, | ||
| next Monday, last week of month, etc); | ||
| * Computing of relative deltas between two given | ||
| date and/or datetime objects; | ||
| * Computing of dates based on very flexible recurrence rules, | ||
| using a superset of the `iCalendar <https://www.ietf.org/rfc/rfc2445.txt>`_ | ||
| specification. Parsing of RFC strings is supported as well. | ||
| * Generic parsing of dates in almost any string format; | ||
| * Timezone (tzinfo) implementations for tzfile(5) format | ||
| files (/etc/localtime, /usr/share/zoneinfo, etc), TZ | ||
| environment string (in all known formats), iCalendar | ||
| format files, given ranges (with help from relative deltas), | ||
| local machine timezone, fixed offset timezone, UTC timezone, | ||
| and Windows registry-based time zones. | ||
| * Internal up-to-date world timezone information based on | ||
| Olson's database. | ||
| * Computing of Easter Sunday dates for any given year, | ||
| using Western, Orthodox or Julian algorithms; | ||
| * A comprehensive test suite. | ||
| Quick example | ||
| ============= | ||
| Here's a snapshot, just to give an idea about the power of the | ||
| package. For more examples, look at the documentation. | ||
| Suppose you want to know how much time is left, in | ||
| years/months/days/etc, before the next easter happening on a | ||
| year with a Friday 13th in August, and you want to get today's | ||
| date out of the "date" unix system command. Here is the code: | ||
| .. code-block:: python3 | ||
| >>> from dateutil.relativedelta import * | ||
| >>> from dateutil.easter import * | ||
| >>> from dateutil.rrule import * | ||
| >>> from dateutil.parser import * | ||
| >>> from datetime import * | ||
| >>> now = parse("Sat Oct 11 17:13:46 UTC 2003") | ||
| >>> today = now.date() | ||
| >>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year | ||
| >>> rdelta = relativedelta(easter(year), today) | ||
| >>> print("Today is: %s" % today) | ||
| Today is: 2003-10-11 | ||
| >>> print("Year with next Aug 13th on a Friday is: %s" % year) | ||
| Year with next Aug 13th on a Friday is: 2004 | ||
| >>> print("How far is the Easter of that year: %s" % rdelta) | ||
| How far is the Easter of that year: relativedelta(months=+6) | ||
| >>> print("And the Easter of that year is: %s" % (today+rdelta)) | ||
| And the Easter of that year is: 2004-04-11 | ||
| Being exactly 6 months ahead was **really** a coincidence :) | ||
| Contributing | ||
| ============ | ||
| We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository. | ||
| Author | ||
| ====== | ||
| The dateutil module was written by Gustavo Niemeyer <gustavo@niemeyer.net> | ||
| in 2003. | ||
| It is maintained by: | ||
| * Gustavo Niemeyer <gustavo@niemeyer.net> 2003-2011 | ||
| * Tomi Pieviläinen <tomi.pievilainen@iki.fi> 2012-2014 | ||
| * Yaron de Leeuw <me@jarondl.net> 2014-2016 | ||
| * Paul Ganssle <paul@ganssle.io> 2015- | ||
| Starting with version 2.4.1 and running until 2.8.2, all source and binary | ||
| distributions will be signed by a PGP key that has, at the very least, been | ||
| signed by the key which made the previous release. A table of release signing | ||
| keys can be found below: | ||
| =========== ============================ | ||
| Releases Signing key fingerprint | ||
| =========== ============================ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| =========== ============================ | ||
| New releases *may* have signed tags, but binary and source distributions | ||
| uploaded to PyPI will no longer have GPG signatures attached. | ||
| Contact | ||
| ======= | ||
| Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of | ||
| conduct <https://www.python.org/psf/conduct/>`_. | ||
| License | ||
| ======= | ||
| All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License. | ||
| .. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB: | ||
| https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB |
| .gitattributes | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .readthedocs.yml | ||
| .travis.yml | ||
| AUTHORS.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| MANIFEST.in | ||
| NEWS | ||
| README.rst | ||
| RELEASING | ||
| appveyor.yml | ||
| azure-pipelines.yml | ||
| codecov.yml | ||
| pyproject.toml | ||
| requirements-dev.txt | ||
| setup.cfg | ||
| setup.py | ||
| tox.ini | ||
| updatezinfo.py | ||
| zonefile_metadata.json | ||
| .github/pull_request_template.md | ||
| .github/workflows/publish.yml | ||
| .github/workflows/validate.yml | ||
| changelog.d/.gitignore | ||
| changelog.d/template.rst | ||
| ci_tools/make_zonefile_metadata.py | ||
| ci_tools/retry.bat | ||
| ci_tools/retry.sh | ||
| ci_tools/run_tz_master_env.sh | ||
| docs/Makefile | ||
| docs/changelog.rst | ||
| docs/conf.py | ||
| docs/easter.rst | ||
| docs/examples.rst | ||
| docs/index.rst | ||
| docs/make.bat | ||
| docs/parser.rst | ||
| docs/relativedelta.rst | ||
| docs/requirements-docs.txt | ||
| docs/rrule.rst | ||
| docs/tz.rst | ||
| docs/tzwin.rst | ||
| docs/utils.rst | ||
| docs/zoneinfo.rst | ||
| docs/exercises/index.rst | ||
| docs/exercises/solutions/mlk-day-rrule.rst | ||
| docs/exercises/solutions/mlk_day_rrule_solution.py | ||
| docs/samples/EST5EDT.ics | ||
| requirements/3.3/constraints.txt | ||
| requirements/3.3/requirements-dev.txt | ||
| src/dateutil/__init__.py | ||
| src/dateutil/_common.py | ||
| src/dateutil/_version.py | ||
| src/dateutil/easter.py | ||
| src/dateutil/relativedelta.py | ||
| src/dateutil/rrule.py | ||
| src/dateutil/tzwin.py | ||
| src/dateutil/utils.py | ||
| src/dateutil/parser/__init__.py | ||
| src/dateutil/parser/_parser.py | ||
| src/dateutil/parser/isoparser.py | ||
| src/dateutil/tz/__init__.py | ||
| src/dateutil/tz/_common.py | ||
| src/dateutil/tz/_factories.py | ||
| src/dateutil/tz/tz.py | ||
| src/dateutil/tz/win.py | ||
| src/dateutil/zoneinfo/__init__.py | ||
| src/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | ||
| src/dateutil/zoneinfo/rebuild.py | ||
| src/python_dateutil.egg-info/PKG-INFO | ||
| src/python_dateutil.egg-info/SOURCES.txt | ||
| src/python_dateutil.egg-info/dependency_links.txt | ||
| src/python_dateutil.egg-info/requires.txt | ||
| src/python_dateutil.egg-info/top_level.txt | ||
| src/python_dateutil.egg-info/zip-safe | ||
| tests/__init__.py | ||
| tests/_common.py | ||
| tests/conftest.py | ||
| tests/test_easter.py | ||
| tests/test_import_star.py | ||
| tests/test_imports.py | ||
| tests/test_internals.py | ||
| tests/test_isoparser.py | ||
| tests/test_parser.py | ||
| tests/test_relativedelta.py | ||
| tests/test_rrule.py | ||
| tests/test_tz.py | ||
| tests/test_utils.py | ||
| tests/property/test_isoparse_prop.py | ||
| tests/property/test_parser_prop.py | ||
| tests/property/test_tz_prop.py |
Sorry, the diff of this file is not supported yet
+233
| from __future__ import unicode_literals | ||
| import os | ||
| import time | ||
| import subprocess | ||
| import warnings | ||
| import tempfile | ||
| import pickle | ||
| import pytest | ||
| class PicklableMixin(object): | ||
| def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs): | ||
| """ | ||
| Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads`` | ||
| """ | ||
| pkl = pickle.dumps(obj, **dump_kwargs) | ||
| return pickle.loads(pkl, **load_kwargs) | ||
| def _get_nobj_file(self, obj, dump_kwargs, load_kwargs): | ||
| """ | ||
| Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on | ||
| a temporary file. | ||
| """ | ||
| with tempfile.TemporaryFile('w+b') as pkl: | ||
| pickle.dump(obj, pkl, **dump_kwargs) | ||
| pkl.seek(0) # Reset the file to the beginning to read it | ||
| nobj = pickle.load(pkl, **load_kwargs) | ||
| return nobj | ||
| def assertPicklable(self, obj, singleton=False, asfile=False, | ||
| dump_kwargs=None, load_kwargs=None): | ||
| """ | ||
| Assert that an object can be pickled and unpickled. This assertion | ||
| assumes that the desired behavior is that the unpickled object compares | ||
| equal to the original object, but is not the same object. | ||
| """ | ||
| get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes | ||
| dump_kwargs = dump_kwargs or {} | ||
| load_kwargs = load_kwargs or {} | ||
| nobj = get_nobj(obj, dump_kwargs, load_kwargs) | ||
| if not singleton: | ||
| self.assertIsNot(obj, nobj) | ||
| self.assertEqual(obj, nobj) | ||
| class TZContextBase(object): | ||
| """ | ||
| Base class for a context manager which allows changing of time zones. | ||
| Subclasses may define a guard variable to either block or or allow time | ||
| zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``. | ||
| The default is that the guard variable must be affirmatively set. | ||
| Subclasses must define ``get_current_tz`` and ``set_current_tz``. | ||
| """ | ||
| _guard_var_name = "DATEUTIL_MAY_CHANGE_TZ" | ||
| _guard_allows_change = True | ||
| def __init__(self, tzval): | ||
| self.tzval = tzval | ||
| self._old_tz = None | ||
| @classmethod | ||
| def tz_change_allowed(cls): | ||
| """ | ||
| Class method used to query whether or not this class allows time zone | ||
| changes. | ||
| """ | ||
| guard = bool(os.environ.get(cls._guard_var_name, False)) | ||
| # _guard_allows_change gives the "default" behavior - if True, the | ||
| # guard is overcoming a block. If false, the guard is causing a block. | ||
| # Whether tz_change is allowed is therefore the XNOR of the two. | ||
| return guard == cls._guard_allows_change | ||
| @classmethod | ||
| def tz_change_disallowed_message(cls): | ||
| """ Generate instructions on how to allow tz changes """ | ||
| msg = ('Changing time zone not allowed. Set {envar} to {gval} ' | ||
| 'if you would like to allow this behavior') | ||
| return msg.format(envar=cls._guard_var_name, | ||
| gval=cls._guard_allows_change) | ||
| def __enter__(self): | ||
| if not self.tz_change_allowed(): | ||
| msg = self.tz_change_disallowed_message() | ||
| pytest.skip(msg) | ||
| # If this is used outside of a test suite, we still want an error. | ||
| raise ValueError(msg) # pragma: no cover | ||
| self._old_tz = self.get_current_tz() | ||
| self.set_current_tz(self.tzval) | ||
| def __exit__(self, type, value, traceback): | ||
| if self._old_tz is not None: | ||
| self.set_current_tz(self._old_tz) | ||
| self._old_tz = None | ||
| def get_current_tz(self): | ||
| raise NotImplementedError | ||
| def set_current_tz(self): | ||
| raise NotImplementedError | ||
| class TZEnvContext(TZContextBase): | ||
| """ | ||
| Context manager that temporarily sets the `TZ` variable (for use on | ||
| *nix-like systems). Because the effect is local to the shell anyway, this | ||
| will apply *unless* a guard is set. | ||
| If you do not want the TZ environment variable set, you may set the | ||
| ``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value. | ||
| """ | ||
| _guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR" | ||
| _guard_allows_change = False | ||
| def get_current_tz(self): | ||
| return os.environ.get('TZ', UnsetTz) | ||
| def set_current_tz(self, tzval): | ||
| if tzval is UnsetTz and 'TZ' in os.environ: | ||
| del os.environ['TZ'] | ||
| else: | ||
| os.environ['TZ'] = tzval | ||
| time.tzset() | ||
| class TZWinContext(TZContextBase): | ||
| """ | ||
| Context manager for changing local time zone on Windows. | ||
| Because the effect of this is system-wide and global, it may have | ||
| unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment | ||
| variable to a truthy value before using this context manager. | ||
| """ | ||
| def get_current_tz(self): | ||
| p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE) | ||
| ctzname, err = p.communicate() | ||
| ctzname = ctzname.decode() # Popen returns | ||
| if p.returncode: | ||
| raise OSError('Failed to get current time zone: ' + err) | ||
| return ctzname | ||
| def set_current_tz(self, tzname): | ||
| p = subprocess.Popen('tzutil /s "' + tzname + '"') | ||
| out, err = p.communicate() | ||
| if p.returncode: | ||
| raise OSError('Failed to set current time zone: ' + | ||
| (err or 'Unknown error.')) | ||
| ### | ||
| # Utility classes | ||
| class NotAValueClass(object): | ||
| """ | ||
| A class analogous to NaN that has operations defined for any type. | ||
| """ | ||
| def _op(self, other): | ||
| return self # Operation with NotAValue returns NotAValue | ||
| def _cmp(self, other): | ||
| return False | ||
| __add__ = __radd__ = _op | ||
| __sub__ = __rsub__ = _op | ||
| __mul__ = __rmul__ = _op | ||
| __div__ = __rdiv__ = _op | ||
| __truediv__ = __rtruediv__ = _op | ||
| __floordiv__ = __rfloordiv__ = _op | ||
| __lt__ = __rlt__ = _op | ||
| __gt__ = __rgt__ = _op | ||
| __eq__ = __req__ = _op | ||
| __le__ = __rle__ = _op | ||
| __ge__ = __rge__ = _op | ||
| NotAValue = NotAValueClass() | ||
| class ComparesEqualClass(object): | ||
| """ | ||
| A class that is always equal to whatever you compare it to. | ||
| """ | ||
| def __eq__(self, other): | ||
| return True | ||
| def __ne__(self, other): | ||
| return False | ||
| def __le__(self, other): | ||
| return True | ||
| def __ge__(self, other): | ||
| return True | ||
| def __lt__(self, other): | ||
| return False | ||
| def __gt__(self, other): | ||
| return False | ||
| __req__ = __eq__ | ||
| __rne__ = __ne__ | ||
| __rle__ = __le__ | ||
| __rge__ = __ge__ | ||
| __rlt__ = __lt__ | ||
| __rgt__ = __gt__ | ||
| ComparesEqual = ComparesEqualClass() | ||
| class UnsetTzClass(object): | ||
| """ Sentinel class for unset time zone variable """ | ||
| pass | ||
| UnsetTz = UnsetTzClass() |
| import os | ||
| import pytest | ||
| # Configure pytest to ignore xfailing tests | ||
| # See: https://stackoverflow.com/a/53198349/467366 | ||
| def pytest_collection_modifyitems(items): | ||
| for item in items: | ||
| marker_getter = getattr(item, 'get_closest_marker', None) | ||
| # Python 3.3 support | ||
| if marker_getter is None: | ||
| marker_getter = item.get_marker | ||
| marker = marker_getter('xfail') | ||
| # Need to query the args because conditional xfail tests still have | ||
| # the xfail mark even if they are not expected to fail | ||
| if marker and (not marker.args or marker.args[0]): | ||
| item.add_marker(pytest.mark.no_cover) | ||
| def set_tzpath(): | ||
| """ | ||
| Sets the TZPATH variable if it's specified in an environment variable. | ||
| """ | ||
| tzpath = os.environ.get('DATEUTIL_TZPATH', None) | ||
| if tzpath is None: | ||
| return | ||
| path_components = tzpath.split(':') | ||
| print("Setting TZPATH to {}".format(path_components)) | ||
| from dateutil import tz | ||
| tz.TZPATHS.clear() | ||
| tz.TZPATHS.extend(path_components) | ||
| set_tzpath() |
| from hypothesis import given, assume | ||
| from hypothesis import strategies as st | ||
| from dateutil import tz | ||
| from dateutil.parser import isoparse | ||
| import pytest | ||
| # Strategies | ||
| TIME_ZONE_STRATEGY = st.sampled_from([None, tz.UTC] + | ||
| [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific', | ||
| 'Australia/Sydney', 'Europe/London')]) | ||
| ASCII_STRATEGY = st.characters(max_codepoint=127) | ||
| @pytest.mark.isoparser | ||
| @given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY) | ||
| def test_timespec_auto(dt, sep): | ||
| if dt.tzinfo is not None: | ||
| # Assume offset has no sub-second components | ||
| assume(dt.utcoffset().total_seconds() % 60 == 0) | ||
| sep = str(sep) # Python 2.7 requires bytes | ||
| dtstr = dt.isoformat(sep=sep) | ||
| dt_rt = isoparse(dtstr) | ||
| assert dt_rt == dt |
| 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 |
| from datetime import datetime, timedelta | ||
| import pytest | ||
| import six | ||
| from hypothesis import assume, given | ||
| from hypothesis import strategies as st | ||
| from dateutil import tz as tz | ||
| EPOCHALYPSE = datetime.fromtimestamp(2147483647) | ||
| NEGATIVE_EPOCHALYPSE = datetime.fromtimestamp(0) - timedelta(seconds=2147483648) | ||
| @pytest.mark.gettz | ||
| @pytest.mark.parametrize("gettz_arg", [None, ""]) | ||
| # TODO: Remove bounds when GH #590 is resolved | ||
| @given( | ||
| dt=st.datetimes( | ||
| min_value=NEGATIVE_EPOCHALYPSE, max_value=EPOCHALYPSE, timezones=st.just(tz.UTC), | ||
| ) | ||
| ) | ||
| def test_gettz_returns_local(gettz_arg, dt): | ||
| act_tz = tz.gettz(gettz_arg) | ||
| if isinstance(act_tz, tz.tzlocal): | ||
| return | ||
| dt_act = dt.astimezone(tz.gettz(gettz_arg)) | ||
| if six.PY2: | ||
| dt_exp = dt.astimezone(tz.tzlocal()) | ||
| else: | ||
| dt_exp = dt.astimezone() | ||
| assert dt_act == dt_exp | ||
| assert dt_act.tzname() == dt_exp.tzname() | ||
| assert dt_act.utcoffset() == dt_exp.utcoffset() |
| from dateutil.easter import easter | ||
| from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN | ||
| from datetime import date | ||
| import pytest | ||
| # List of easters between 1990 and 2050 | ||
| western_easter_dates = [ | ||
| date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11), | ||
| date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30), | ||
| date(1998, 4, 12), date(1999, 4, 4), | ||
| date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20), | ||
| date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8), | ||
| date(2008, 3, 23), date(2009, 4, 12), | ||
| date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31), | ||
| date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16), | ||
| date(2018, 4, 1), date(2019, 4, 21), | ||
| date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9), | ||
| date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28), | ||
| date(2028, 4, 16), date(2029, 4, 1), | ||
| date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17), | ||
| date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5), | ||
| date(2038, 4, 25), date(2039, 4, 10), | ||
| date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29), | ||
| date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14), | ||
| date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10) | ||
| ] | ||
| orthodox_easter_dates = [ | ||
| date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18), | ||
| date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27), | ||
| date(1998, 4, 19), date(1999, 4, 11), | ||
| date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27), | ||
| date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8), | ||
| date(2008, 4, 27), date(2009, 4, 19), | ||
| date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5), | ||
| date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16), | ||
| date(2018, 4, 8), date(2019, 4, 28), | ||
| date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16), | ||
| date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2), | ||
| date(2028, 4, 16), date(2029, 4, 8), | ||
| date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24), | ||
| date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5), | ||
| date(2038, 4, 25), date(2039, 4, 17), | ||
| date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3), | ||
| date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21), | ||
| date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17) | ||
| ] | ||
| # A random smattering of Julian dates. | ||
| # Pulled values from http://www.kevinlaughery.com/east4099.html | ||
| julian_easter_dates = [ | ||
| date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31), | ||
| date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19), | ||
| date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8), | ||
| date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14), | ||
| date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6), | ||
| date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30), | ||
| date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24), | ||
| date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17), | ||
| date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3), | ||
| date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14) | ||
| ] | ||
| @pytest.mark.parametrize("easter_date", western_easter_dates) | ||
| def test_easter_western(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_WESTERN) | ||
| @pytest.mark.parametrize("easter_date", orthodox_easter_dates) | ||
| def test_easter_orthodox(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_ORTHODOX) | ||
| @pytest.mark.parametrize("easter_date", julian_easter_dates) | ||
| def test_easter_julian(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_JULIAN) | ||
| def test_easter_bad_method(): | ||
| with pytest.raises(ValueError): | ||
| easter(1975, 4) |
| """Test for the "import *" functionality. | ||
| As import * can be only done at module level, it has been added in a separate file | ||
| """ | ||
| import pytest | ||
| prev_locals = list(locals()) | ||
| from dateutil import * | ||
| new_locals = {name:value for name,value in locals().items() | ||
| if name not in prev_locals} | ||
| new_locals.pop('prev_locals') | ||
| @pytest.mark.import_star | ||
| def test_imported_modules(): | ||
| """ Test that `from dateutil import *` adds modules in __all__ locally """ | ||
| import dateutil.easter | ||
| import dateutil.parser | ||
| import dateutil.relativedelta | ||
| import dateutil.rrule | ||
| import dateutil.tz | ||
| import dateutil.utils | ||
| import dateutil.zoneinfo | ||
| assert dateutil.easter == new_locals.pop("easter") | ||
| assert dateutil.parser == new_locals.pop("parser") | ||
| assert dateutil.relativedelta == new_locals.pop("relativedelta") | ||
| assert dateutil.rrule == new_locals.pop("rrule") | ||
| assert dateutil.tz == new_locals.pop("tz") | ||
| assert dateutil.utils == new_locals.pop("utils") | ||
| assert dateutil.zoneinfo == new_locals.pop("zoneinfo") | ||
| assert not new_locals |
| import sys | ||
| import unittest | ||
| import pytest | ||
| import six | ||
| MODULE_TYPE = type(sys) | ||
| # Tests live in datetutil/test which cause a RuntimeWarning for Python2 builds. | ||
| # But since we expect lazy imports tests to fail for Python < 3.7 we'll ignore those | ||
| # warnings with this filter. | ||
| if six.PY2: | ||
| filter_import_warning = pytest.mark.filterwarnings("ignore::RuntimeWarning") | ||
| else: | ||
| def filter_import_warning(f): | ||
| return f | ||
| @pytest.fixture(scope="function") | ||
| def clean_import(): | ||
| """Create a somewhat clean import base for lazy import tests""" | ||
| du_modules = { | ||
| mod_name: mod | ||
| for mod_name, mod in sys.modules.items() | ||
| if mod_name.startswith("dateutil") | ||
| } | ||
| other_modules = { | ||
| mod_name for mod_name in sys.modules if mod_name not in du_modules | ||
| } | ||
| for mod_name in du_modules: | ||
| del sys.modules[mod_name] | ||
| yield | ||
| # Delete anything that wasn't in the origin sys.modules list | ||
| for mod_name in list(sys.modules): | ||
| if mod_name not in other_modules: | ||
| del sys.modules[mod_name] | ||
| # Restore original modules | ||
| for mod_name, mod in du_modules.items(): | ||
| sys.modules[mod_name] = mod | ||
| @filter_import_warning | ||
| @pytest.mark.parametrize( | ||
| "module", | ||
| ["easter", "parser", "relativedelta", "rrule", "tz", "utils", "zoneinfo"], | ||
| ) | ||
| def test_lazy_import(clean_import, module): | ||
| """Test that dateutil.[submodule] works for py version > 3.7""" | ||
| import dateutil, importlib | ||
| if sys.version_info < (3, 7): | ||
| pytest.xfail("Lazy loading does not work for Python < 3.7") | ||
| mod_obj = getattr(dateutil, module, None) | ||
| assert isinstance(mod_obj, MODULE_TYPE) | ||
| mod_imported = importlib.import_module("dateutil.%s" % module) | ||
| assert mod_obj is mod_imported | ||
| HOST_IS_WINDOWS = sys.platform.startswith('win') | ||
| def test_import_version_str(): | ||
| """ Test that dateutil.__version__ can be imported""" | ||
| from dateutil import __version__ | ||
| def test_import_version_root(): | ||
| import dateutil | ||
| assert hasattr(dateutil, '__version__') | ||
| # Test that dateutil.easter-related imports work properly | ||
| def test_import_easter_direct(): | ||
| import dateutil.easter | ||
| def test_import_easter_from(): | ||
| from dateutil import easter | ||
| def test_import_easter_start(): | ||
| from dateutil.easter import easter | ||
| # Test that dateutil.parser-related imports work properly | ||
| def test_import_parser_direct(): | ||
| import dateutil.parser | ||
| def test_import_parser_from(): | ||
| from dateutil import parser | ||
| def test_import_parser_all(): | ||
| # All interface | ||
| from dateutil.parser import parse | ||
| from dateutil.parser import parserinfo | ||
| # Other public classes | ||
| from dateutil.parser import parser | ||
| for var in (parse, parserinfo, parser): | ||
| assert var is not None | ||
| # Test that dateutil.relativedelta-related imports work properly | ||
| def test_import_relative_delta_direct(): | ||
| import dateutil.relativedelta | ||
| def test_import_relative_delta_from(): | ||
| from dateutil import relativedelta | ||
| def test_import_relative_delta_all(): | ||
| from dateutil.relativedelta import relativedelta | ||
| from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU | ||
| for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): | ||
| assert var is not None | ||
| # In the public interface but not in all | ||
| from dateutil.relativedelta import weekday | ||
| assert weekday is not None | ||
| # Test that dateutil.rrule related imports work properly | ||
| def test_import_rrule_direct(): | ||
| import dateutil.rrule | ||
| def test_import_rrule_from(): | ||
| from dateutil import rrule | ||
| def test_import_rrule_all(): | ||
| from dateutil.rrule import rrule | ||
| from dateutil.rrule import rruleset | ||
| from dateutil.rrule import rrulestr | ||
| from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY | ||
| from dateutil.rrule import HOURLY, MINUTELY, SECONDLY | ||
| from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU | ||
| rr_all = (rrule, rruleset, rrulestr, | ||
| YEARLY, MONTHLY, WEEKLY, DAILY, | ||
| HOURLY, MINUTELY, SECONDLY, | ||
| MO, TU, WE, TH, FR, SA, SU) | ||
| for var in rr_all: | ||
| assert var is not None | ||
| # In the public interface but not in all | ||
| from dateutil.rrule import weekday | ||
| assert weekday is not None | ||
| # Test that dateutil.tz related imports work properly | ||
| def test_import_tztest_direct(): | ||
| import dateutil.tz | ||
| def test_import_tz_from(): | ||
| from dateutil import tz | ||
| def test_import_tz_all(): | ||
| from dateutil.tz import tzutc | ||
| from dateutil.tz import tzoffset | ||
| from dateutil.tz import tzlocal | ||
| from dateutil.tz import tzfile | ||
| from dateutil.tz import tzrange | ||
| from dateutil.tz import tzstr | ||
| from dateutil.tz import tzical | ||
| from dateutil.tz import gettz | ||
| from dateutil.tz import tzwin | ||
| from dateutil.tz import tzwinlocal | ||
| from dateutil.tz import UTC | ||
| from dateutil.tz import datetime_ambiguous | ||
| from dateutil.tz import datetime_exists | ||
| from dateutil.tz import resolve_imaginary | ||
| tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "gettz", "datetime_ambiguous", | ||
| "datetime_exists", "resolve_imaginary", "UTC"] | ||
| tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] | ||
| lvars = locals() | ||
| for var in tz_all: | ||
| assert lvars[var] is not None | ||
| # Test that dateutil.tzwin related imports work properly | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_direct(): | ||
| import dateutil.tzwin | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_from(): | ||
| from dateutil import tzwin | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_star(): | ||
| from dateutil.tzwin import tzwin | ||
| from dateutil.tzwin import tzwinlocal | ||
| tzwin_all = [tzwin, tzwinlocal] | ||
| for var in tzwin_all: | ||
| assert var is not None | ||
| # Test imports of Zone Info | ||
| def test_import_zone_info_direct(): | ||
| import dateutil.zoneinfo | ||
| def test_import_zone_info_from(): | ||
| from dateutil import zoneinfo | ||
| def test_import_zone_info_star(): | ||
| from dateutil.zoneinfo import gettz | ||
| from dateutil.zoneinfo import gettz_db_metadata | ||
| from dateutil.zoneinfo import rebuild | ||
| zi_all = (gettz, gettz_db_metadata, rebuild) | ||
| for var in zi_all: | ||
| assert var is not None |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| Tests for implementation details, not necessarily part of the user-facing | ||
| API. | ||
| The motivating case for these tests is #483, where we want to smoke-test | ||
| code that may be difficult to reach through the standard API calls. | ||
| """ | ||
| import sys | ||
| import pytest | ||
| import warnings | ||
| from dateutil.parser._parser import _ymd | ||
| from dateutil import tz | ||
| IS_PY32 = sys.version_info[0:2] == (3, 2) | ||
| @pytest.mark.smoke | ||
| def test_YMD_could_be_day(): | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(2, 'M') | ||
| assert ymd.has_month | ||
| assert not ymd.has_year | ||
| assert ymd.could_be_day(4) | ||
| assert not ymd.could_be_day(-6) | ||
| assert not ymd.could_be_day(32) | ||
| # Assumes leap year | ||
| assert ymd.could_be_day(29) | ||
| ymd.append(1999) | ||
| assert ymd.has_year | ||
| assert not ymd.could_be_day(29) | ||
| ymd.append(16, 'D') | ||
| assert ymd.has_day | ||
| assert not ymd.could_be_day(1) | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(1999) | ||
| assert ymd.could_be_day(31) | ||
| ### | ||
| # Test that private interfaces in _parser are deprecated properly | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_private_warns(): | ||
| from dateutil.parser import _timelex, _tzparser | ||
| from dateutil.parser import _parsetz | ||
| with pytest.warns(DeprecationWarning): | ||
| _tzparser() | ||
| with pytest.warns(DeprecationWarning): | ||
| _timelex('2014-03-03') | ||
| with pytest.warns(DeprecationWarning): | ||
| _parsetz('+05:00') | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_parser_private_not_warns(): | ||
| from dateutil.parser._parser import _timelex, _tzparser | ||
| from dateutil.parser._parser import _parsetz | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error") | ||
| _tzparser() | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error") | ||
| _timelex('2014-03-03') | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error") | ||
| _parsetz('+05:00') | ||
| @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 |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, timedelta, date, time | ||
| import itertools as it | ||
| from dateutil import tz | ||
| from dateutil.tz import UTC | ||
| from dateutil.parser import isoparser, isoparse | ||
| import pytest | ||
| import six | ||
| def _generate_tzoffsets(limited): | ||
| def _mkoffset(hmtuple, fmt): | ||
| h, m = hmtuple | ||
| m_td = (-1 if h < 0 else 1) * m | ||
| tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td)) | ||
| return tzo, fmt.format(h, m) | ||
| out = [] | ||
| if not limited: | ||
| # The subset that's just hours | ||
| hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)] | ||
| out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h]) | ||
| # Ones that have hours and minutes | ||
| hm_out = [] + hm_out_h | ||
| hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)] | ||
| else: | ||
| hm_out = [(-5, -0)] | ||
| fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}'] | ||
| out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts] | ||
| # Also add in UTC and naive | ||
| out.append((UTC, 'Z')) | ||
| out.append((None, '')) | ||
| return out | ||
| FULL_TZOFFSETS = _generate_tzoffsets(False) | ||
| FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]] | ||
| TZOFFSETS = _generate_tzoffsets(True) | ||
| DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_only(dt): | ||
| dtstr = dt.strftime('%Y') | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_month(dt): | ||
| fmt = '%Y-%m' | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)] | ||
| YMD_FMTS = ('%Y%m%d', '%Y-%m-%d') | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| @pytest.mark.parametrize('fmt', YMD_FMTS) | ||
| def test_year_month_day(dt, fmt): | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, | ||
| microsecond_precision=None): | ||
| tzi, offset_str = tzoffset | ||
| fmt = date_fmt + 'T' + time_fmt | ||
| dt = dt.replace(tzinfo=tzi) | ||
| dtstr = dt.strftime(fmt) | ||
| if microsecond_precision is not None: | ||
| if not fmt.endswith('%f'): # pragma: nocover | ||
| raise ValueError('Time format has no microseconds!') | ||
| if microsecond_precision != 6: | ||
| dtstr = dtstr[: -(6 - microsecond_precision)] | ||
| elif microsecond_precision > 6: # pragma: nocover | ||
| raise ValueError("Precision must be 1-6") | ||
| dtstr += offset_str | ||
| assert isoparse(dtstr) == dt | ||
| DATETIMES = [datetime(1998, 4, 16, 12), | ||
| datetime(2019, 11, 18, 23), | ||
| datetime(2014, 12, 16, 4)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_h(dt, date_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset) | ||
| DATETIMES = [datetime(2012, 1, 6, 9, 37)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2003, 9, 2, 22, 14, 2), | ||
| datetime(2003, 8, 8, 14, 9, 14), | ||
| datetime(2003, 4, 7, 6, 14, 59)] | ||
| HMS_FMTS = ('%H%M%S', '%H:%M:%S') | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', HMS_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS | ||
| for sep in '.,')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| @pytest.mark.parametrize('precision', list(range(3, 7))) | ||
| def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision): | ||
| # Truncate the microseconds to the desired precision for the representation | ||
| dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6))) | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision) | ||
| ### | ||
| # Truncation of extra digits beyond microsecond precision | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2018-07-03T14:07:00.123456000001', | ||
| '2018-07-03T14:07:00.123456999999', | ||
| ]) | ||
| def test_extra_subsecond_digits(dt_str): | ||
| assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456) | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_full_tzoffsets(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2014-04-11T00', | ||
| '2014-04-10T24', | ||
| '2014-04-11T00:00', | ||
| '2014-04-10T24:00', | ||
| '2014-04-11T00:00:00', | ||
| '2014-04-10T24:00:00', | ||
| '2014-04-11T00:00:00.000', | ||
| '2014-04-10T24:00:00.000', | ||
| '2014-04-11T00:00:00.000000', | ||
| '2014-04-10T24:00:00.000000'] | ||
| ) | ||
| def test_datetime_midnight(dt_str): | ||
| assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0) | ||
| @pytest.mark.parametrize('datestr', [ | ||
| '2014-01-01', | ||
| '20140101', | ||
| ]) | ||
| @pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-']) | ||
| def test_isoparse_sep_none(datestr, sep): | ||
| isostr = datestr + sep + '14:33:09' | ||
| assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9) | ||
| ## | ||
| # Uncommon date formats | ||
| TIME_ARGS = ('time_args', | ||
| ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz) | ||
| for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)], | ||
| TZOFFSETS))) | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2017, 10), datetime(2017, 3, 6)), | ||
| ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year | ||
| ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014 | ||
| ]) | ||
| def test_isoweek(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2016, 13, 7), datetime(2016, 4, 3)), | ||
| ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year | ||
| ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year | ||
| ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year | ||
| ]) | ||
| def test_isoweek_day(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isoord,dt_expected', [ | ||
| ((2004, 1), datetime(2004, 1, 1)), | ||
| ((2016, 60), datetime(2016, 2, 29)), | ||
| ((2017, 60), datetime(2017, 3, 1)), | ||
| ((2016, 366), datetime(2016, 12, 31)), | ||
| ((2017, 365), datetime(2017, 12, 31)) | ||
| ]) | ||
| def test_iso_ordinal(isoord, dt_expected): | ||
| for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'): | ||
| dtstr = fmt.format(*isoord) | ||
| assert isoparse(dtstr) == dt_expected | ||
| ### | ||
| # Acceptance of bytes | ||
| @pytest.mark.parametrize('isostr,dt', [ | ||
| (b'2014', datetime(2014, 1, 1)), | ||
| (b'20140204', datetime(2014, 2, 4)), | ||
| (b'2014-02-04', datetime(2014, 2, 4)), | ||
| (b'2014-02-04T12', datetime(2014, 2, 4, 12)), | ||
| (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)), | ||
| (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)), | ||
| (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| UTC)), | ||
| (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| UTC)), | ||
| (b'2014-02-04T12:30:15.224+05:00', | ||
| datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| tzinfo=tz.tzoffset(None, timedelta(hours=5))))]) | ||
| def test_bytes(isostr, dt): | ||
| assert isoparse(isostr) == dt | ||
| ### | ||
| # Invalid ISO strings | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('201', ValueError), # ISO string too short | ||
| ('2012-0425', ValueError), # Inconsistent date separators | ||
| ('201204-25', ValueError), # Inconsistent date separators | ||
| ('20120425T0120:00', ValueError), # Inconsistent time separators | ||
| ('20120425T01:2000', ValueError), # Inconsistent time separators | ||
| ('14:3015', ValueError), # Inconsistent time separator | ||
| ('20120425T012500-334', ValueError), # Wrong microsecond separator | ||
| ('2001-1', ValueError), # YYYY-M not valid | ||
| ('2012-04-9', ValueError), # YYYY-MM-D not valid | ||
| ('201204', ValueError), # YYYYMM not valid | ||
| ('20120411T03:30+', ValueError), # Time zone too short | ||
| ('20120411T03:30+1234567', ValueError), # Time zone too long | ||
| ('20120411T03:30-25:40', ValueError), # Time zone invalid | ||
| ('2012-1a', ValueError), # Invalid month | ||
| ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes | ||
| ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes | ||
| ('20120411T033030.123456012:00', # No sign in time zone | ||
| ValueError), | ||
| ('2012-W00', ValueError), # Invalid ISO week | ||
| ('2012-W55', ValueError), # Invalid ISO week | ||
| ('2012-W01-0', ValueError), # Invalid ISO week day | ||
| ('2012-W01-8', ValueError), # Invalid ISO week day | ||
| ('2013-000', ValueError), # Invalid ordinal day | ||
| ('2013-366', ValueError), # Invalid ordinal day | ||
| ('2013366', ValueError), # Invalid ordinal day | ||
| ('2014-03-12Т12:30:14', ValueError), # Cyrillic T | ||
| ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight | ||
| ('2014_W01-1', ValueError), # Invalid separator | ||
| ('2014W01-1', ValueError), # Inconsistent use of dashes | ||
| ('2014-W011', ValueError), # Inconsistent use of dashes | ||
| ]) | ||
| def test_iso_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparse(isostr) | ||
| @pytest.mark.parametrize('sep_act, valid_sep, exception', [ | ||
| ('T', 'C', ValueError), | ||
| ('C', 'T', ValueError), | ||
| ]) | ||
| def test_iso_with_sep_raises(sep_act, valid_sep, exception): | ||
| parser = isoparser(sep=valid_sep) | ||
| isostr = '2012-04-25' + sep_act + '01:25:00' | ||
| with pytest.raises(exception): | ||
| parser.isoparse(isostr) | ||
| ### | ||
| # Test ISOParser constructor | ||
| @pytest.mark.parametrize('sep', [' ', '9', '🍛']) | ||
| def test_isoparser_invalid_sep(sep): | ||
| with pytest.raises(ValueError): | ||
| isoparser(sep=sep) | ||
| # This only fails on Python 3 | ||
| @pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only") | ||
| def test_isoparser_byte_sep(): | ||
| dt = datetime(2017, 12, 6, 12, 30, 45) | ||
| dt_str = dt.isoformat(sep=str('T')) | ||
| dt_rt = isoparser(sep=b'T').isoparse(dt_str) | ||
| assert dt == dt_rt | ||
| ### | ||
| # Test parse_tzstr | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_parse_tzstr(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('tzstr', [ | ||
| '-00:00', '+00:00', '+00', '-00', '+0000', '-0000' | ||
| ]) | ||
| @pytest.mark.parametrize('zero_as_utc', [True, False]) | ||
| def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc): | ||
| tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| assert tzi == UTC | ||
| assert (type(tzi) == tz.tzutc) == zero_as_utc | ||
| @pytest.mark.parametrize('tzstr,exception', [ | ||
| ('00:00', ValueError), # No sign | ||
| ('05:00', ValueError), # No sign | ||
| ('_00:00', ValueError), # Invalid sign | ||
| ('+25:00', ValueError), # Offset too large | ||
| ('00:0000', ValueError), # String too long | ||
| ]) | ||
| def test_parse_tzstr_fails(tzstr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_tzstr(tzstr) | ||
| ### | ||
| # Test parse_isodate | ||
| def __make_date_examples(): | ||
| dates_no_day = [ | ||
| date(1999, 12, 1), | ||
| date(2016, 2, 1) | ||
| ] | ||
| if not six.PY2: | ||
| # strftime does not support dates before 1900 in Python 2 | ||
| dates_no_day.append(date(1000, 11, 1)) | ||
| # Only one supported format for dates with no day | ||
| o = zip(dates_no_day, it.repeat('%Y-%m')) | ||
| dates_w_day = [ | ||
| date(1969, 12, 31), | ||
| date(1900, 1, 1), | ||
| date(2016, 2, 29), | ||
| date(2017, 11, 14) | ||
| ] | ||
| dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d') | ||
| o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts)) | ||
| return list(o) | ||
| @pytest.mark.parametrize('d,dt_fmt', __make_date_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_parse_isodate(d, dt_fmt, as_bytes): | ||
| d_str = d.strftime(dt_fmt) | ||
| if isinstance(d_str, six.text_type) and as_bytes: | ||
| d_str = d_str.encode('ascii') | ||
| elif isinstance(d_str, bytes) and not as_bytes: | ||
| d_str = d_str.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isodate(d_str) == d | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('243', ValueError), # ISO string too short | ||
| ('2014-0423', ValueError), # Inconsistent date separators | ||
| ('201404-23', ValueError), # Inconsistent date separators | ||
| ('2014日03月14', ValueError), # Not ASCII | ||
| ('2013-02-29', ValueError), # Not a leap year | ||
| ('2014/12/03', ValueError), # Wrong separators | ||
| ('2014-04-19T', ValueError), # Unknown components | ||
| ('201202', ValueError), # Invalid format | ||
| ]) | ||
| def test_isodate_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_isodate(isostr) | ||
| def test_parse_isodate_error_text(): | ||
| with pytest.raises(ValueError) as excinfo: | ||
| isoparser().parse_isodate('2014-0423') | ||
| # ensure the error message does not contain b' prefixes | ||
| if six.PY2: | ||
| expected_error = "String contains unknown ISO components: u'2014-0423'" | ||
| else: | ||
| expected_error = "String contains unknown ISO components: '2014-0423'" | ||
| assert expected_error == str(excinfo.value) | ||
| ### | ||
| # Test parse_isotime | ||
| def __make_time_examples(): | ||
| outputs = [] | ||
| # HH | ||
| time_h = [time(0), time(8), time(22)] | ||
| time_h_fmts = ['%H'] | ||
| outputs.append(it.product(time_h, time_h_fmts)) | ||
| # HHMM / HH:MM | ||
| time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)] | ||
| time_hm_fmts = ['%H%M', '%H:%M'] | ||
| outputs.append(it.product(time_hm, time_hm_fmts)) | ||
| # HHMMSS / HH:MM:SS | ||
| time_hms = [time(0, 0, 0), time(0, 15, 30), | ||
| time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)] | ||
| time_hms_fmts = ['%H%M%S', '%H:%M:%S'] | ||
| outputs.append(it.product(time_hms, time_hms_fmts)) | ||
| # HHMMSS.ffffff / HH:MM:SS.ffffff | ||
| time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993), | ||
| time(14, 21, 59, 948730), | ||
| time(23, 59, 59, 999999)] | ||
| time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f'] | ||
| outputs.append(it.product(time_hmsu, time_hmsu_fmts)) | ||
| outputs = list(map(list, outputs)) | ||
| # Time zones | ||
| ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs)) | ||
| o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr)) | ||
| o = ((t.replace(tzinfo=tzi), fmt + off_str) | ||
| for (t, fmt), (tzi, off_str) in o) | ||
| outputs.append(o) | ||
| return list(it.chain.from_iterable(outputs)) | ||
| @pytest.mark.parametrize('time_val,time_fmt', __make_time_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_isotime(time_val, time_fmt, as_bytes): | ||
| tstr = time_val.strftime(time_fmt) | ||
| if isinstance(tstr, six.text_type) and as_bytes: | ||
| tstr = tstr.encode('ascii') | ||
| elif isinstance(tstr, bytes) and not as_bytes: | ||
| tstr = tstr.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(tstr) == time_val | ||
| @pytest.mark.parametrize('isostr', [ | ||
| '24:00', | ||
| '2400', | ||
| '24:00:00', | ||
| '240000', | ||
| '24:00:00.000', | ||
| '24:00:00,000', | ||
| '24:00:00.000000', | ||
| '24:00:00,000000', | ||
| ]) | ||
| def test_isotime_midnight(isostr): | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0) | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('3', ValueError), # ISO string too short | ||
| ('14時30分15秒', ValueError), # Not ASCII | ||
| ('14_30_15', ValueError), # Invalid separators | ||
| ('1430:15', ValueError), # Inconsistent separator use | ||
| ('25', ValueError), # Invalid hours | ||
| ('25:15', ValueError), # Invalid hours | ||
| ('14:60', ValueError), # Invalid minutes | ||
| ('14:59:61', ValueError), # Invalid seconds | ||
| ('14:30:15.34468305:00', ValueError), # No sign in time zone | ||
| ('14:30:15+', ValueError), # Time zone too short | ||
| ('14:30:15+1234567', ValueError), # Time zone invalid | ||
| ('14:59:59+25:00', ValueError), # Invalid tz hours | ||
| ('14:59:59+12:62', ValueError), # Invalid tz minutes | ||
| ('14:59:30_344583', ValueError), # Invalid microsecond separator | ||
| ('24:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.001', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.000001', ValueError), # 24 used for non-midnight time | ||
| ]) | ||
| def test_isotime_raises(isostr, exception): | ||
| iparser = isoparser() | ||
| with pytest.raises(exception): | ||
| iparser.parse_isotime(isostr) |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| import itertools | ||
| from datetime import datetime, timedelta | ||
| import unittest | ||
| import sys | ||
| from dateutil import tz | ||
| from dateutil.tz import tzoffset | ||
| from dateutil.parser import parse, parserinfo | ||
| from dateutil.parser import ParserError | ||
| from dateutil.parser import UnknownTimezoneWarning | ||
| from ._common import TZEnvContext | ||
| from six import assertRaisesRegex, PY2 | ||
| from io import StringIO | ||
| import pytest | ||
| # Platform info | ||
| IS_WIN = sys.platform.startswith('win') | ||
| PLATFORM_HAS_DASH_D = False | ||
| try: | ||
| if datetime.now().strftime('%-d'): | ||
| PLATFORM_HAS_DASH_D = True | ||
| except ValueError: | ||
| pass | ||
| @pytest.fixture(params=[True, False]) | ||
| def fuzzy(request): | ||
| """Fixture to pass fuzzy=True or fuzzy=False to parse""" | ||
| return request.param | ||
| # Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"), | ||
| ("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"), | ||
| ("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"), | ||
| ("2003-09-25", datetime(2003, 9, 25), "iso format strip"), | ||
| ("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"), | ||
| ("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"), | ||
| ("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"), | ||
| ("20030925", datetime(2003, 9, 25), "iso stripped format strip"), | ||
| ("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"), | ||
| ("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"), | ||
| ("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"), | ||
| ("09-25-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("25-09-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("10-09-2003", datetime(2003, 10, 9), "date with dash"), | ||
| ("10-09-03", datetime(2003, 10, 9), "date with dash"), | ||
| ("2003.09.25", datetime(2003, 9, 25), "date with dot"), | ||
| ("09.25.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("25.09.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("10.09.2003", datetime(2003, 10, 9), "date with dot"), | ||
| ("10.09.03", datetime(2003, 10, 9), "date with dot"), | ||
| ("2003/09/25", datetime(2003, 9, 25), "date with slash"), | ||
| ("09/25/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("25/09/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("10/09/2003", datetime(2003, 10, 9), "date with slash"), | ||
| ("10/09/03", datetime(2003, 10, 9), "date with slash"), | ||
| ("2003 09 25", datetime(2003, 9, 25), "date with space"), | ||
| ("09 25 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("25 09 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("10 09 2003", datetime(2003, 10, 9), "date with space"), | ||
| ("10 09 03", datetime(2003, 10, 9), "date with space"), | ||
| ("25 09 03", datetime(2003, 9, 25), "date with space"), | ||
| ("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"), | ||
| ("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"), | ||
| (" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"), | ||
| ("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"), | ||
| ("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"), | ||
| ("July 4, 1976", datetime(1976, 7, 4), "random format"), | ||
| ("7 4 1976", datetime(1976, 7, 4), "random format"), | ||
| ("4 jul 1976", datetime(1976, 7, 4), "random format"), | ||
| ("4 Jul 1976", datetime(1976, 7, 4), "'%-d %b %Y' format"), | ||
| ("7-4-76", datetime(1976, 7, 4), "random format"), | ||
| ("19760704", datetime(1976, 7, 4), "random format"), | ||
| ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"), | ||
| ("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"), | ||
| ("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"), | ||
| ("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"), | ||
| ("3rd of May 2001", datetime(2001, 5, 3), "random format"), | ||
| ("5th of March 2001", datetime(2001, 3, 5), "random format"), | ||
| ("1st of May 2003", datetime(2003, 5, 1), "random format"), | ||
| ('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"), | ||
| ('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"), | ||
| ("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"), | ||
| ('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"), | ||
| ('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"), | ||
| ('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)"), | ||
| # Cases with legacy h/m/s format, candidates for deprecation (GH#886) | ||
| ("2016-12-21 04.2h", datetime(2016, 12, 21, 4, 12), "Fractional Hours"), | ||
| ] | ||
| # Check that we don't have any duplicates | ||
| assert len(set([x[0] for x in PARSER_TEST_CASES])) == len(PARSER_TEST_CASES) | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES) | ||
| def test_parser(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text) == expected_datetime, assertion_message | ||
| # Parser test cases using datetime(2003, 9, 25) as a default. | ||
| # Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_DEFAULT_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"), | ||
| ("Sep 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("Sep", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"), | ||
| ("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"), | ||
| ("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10h", datetime(2003, 9, 25, 10), "hour with letters strip"), | ||
| ("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"), | ||
| ("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"), | ||
| ("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"), | ||
| ("10h am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10h pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("Wed", datetime(2003, 10, 1), "weekday alone"), | ||
| ("Wednesday", datetime(2003, 10, 1), "long weekday"), | ||
| ("October", datetime(2003, 10, 25), "long month"), | ||
| ("31-Dec-00", datetime(2000, 12, 31), "zero year"), | ||
| ("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"), | ||
| ("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"), | ||
| ("01h02", datetime(2003, 9, 25, 1, 2), "random format"), | ||
| ("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"), | ||
| ("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("01m02h", datetime(2003, 9, 25, 2, 1), "random format"), | ||
| ("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format") | ||
| ] | ||
| # Check that we don't have any duplicates | ||
| assert len(set([x[0] for x in PARSER_DEFAULT_TEST_CASES])) == len(PARSER_DEFAULT_TEST_CASES) | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES) | ||
| def test_parser_default(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message | ||
| @pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) | ||
| def test_parse_dayfirst(sep): | ||
| expected = datetime(2003, 9, 10) | ||
| fmt = sep.join(['%d', '%m', '%Y']) | ||
| dstr = expected.strftime(fmt) | ||
| result = parse(dstr, dayfirst=True) | ||
| assert result == expected | ||
| @pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) | ||
| def test_parse_yearfirst(sep): | ||
| expected = datetime(2010, 9, 3) | ||
| fmt = sep.join(['%Y', '%m', '%d']) | ||
| dstr = expected.strftime(fmt) | ||
| result = parse(dstr, yearfirst=True) | ||
| assert result == expected | ||
| @pytest.mark.parametrize('dstr,expected', [ | ||
| ("Thu Sep 25 10:36:28 BRST 2003", datetime(2003, 9, 25, 10, 36, 28)), | ||
| ("1996.07.10 AD at 15:08:56 PDT", datetime(1996, 7, 10, 15, 8, 56)), | ||
| ("Tuesday, April 12, 1952 AD 3:30:42pm PST", | ||
| datetime(1952, 4, 12, 15, 30, 42)), | ||
| ("November 5, 1994, 8:15:30 am EST", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1994-11-05T08:15:30-05:00", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1994-11-05T08:15:30Z", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1976-07-04T00:01:02Z", datetime(1976, 7, 4, 0, 1, 2)), | ||
| ("1986-07-05T08:15:30z", datetime(1986, 7, 5, 8, 15, 30)), | ||
| ("Tue Apr 4 00:22:12 PDT 1995", datetime(1995, 4, 4, 0, 22, 12)), | ||
| ]) | ||
| def test_parse_ignoretz(dstr, expected): | ||
| result = parse(dstr, ignoretz=True) | ||
| assert result == expected | ||
| _brsttz = tzoffset("BRST", -10800) | ||
| @pytest.mark.parametrize('dstr,expected', [ | ||
| ("20030925T104941-0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("Thu, 25 Sep 2003 10:49:41 -0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("2003-09-25T10:49:41.5-03:00", | ||
| datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), | ||
| ("2003-09-25T10:49:41-03:00", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("20030925T104941.5-0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), | ||
| ]) | ||
| def test_parse_with_tzoffset(dstr, expected): | ||
| # In these cases, we are _not_ passing a tzinfos arg | ||
| result = parse(dstr) | ||
| assert result == expected | ||
| class TestFormat(object): | ||
| def test_ybd(self): | ||
| # If we have a 4-digit year, a non-numeric month (abbreviated or not), | ||
| # and a day (1 or 2 digits), then there is no ambiguity as to which | ||
| # token is a year/month/day. This holds regardless of what order the | ||
| # terms are in and for each of the separators below. | ||
| seps = ['-', ' ', '/', '.'] | ||
| year_tokens = ['%Y'] | ||
| month_tokens = ['%b', '%B'] | ||
| day_tokens = ['%d'] | ||
| if PLATFORM_HAS_DASH_D: | ||
| day_tokens.append('%-d') | ||
| prods = itertools.product(year_tokens, month_tokens, day_tokens) | ||
| perms = [y for x in prods for y in itertools.permutations(x)] | ||
| unambig_fmts = [sep.join(perm) for sep in seps for perm in perms] | ||
| actual = datetime(2003, 9, 25) | ||
| for fmt in unambig_fmts: | ||
| dstr = actual.strftime(fmt) | ||
| res = parse(dstr) | ||
| assert res == actual | ||
| # TODO: some redundancy with PARSER_TEST_CASES cases | ||
| @pytest.mark.parametrize("fmt,dstr", [ | ||
| ("%a %b %d %Y", "Thu Sep 25 2003"), | ||
| ("%b %d %Y", "Sep 25 2003"), | ||
| ("%Y-%m-%d", "2003-09-25"), | ||
| ("%Y%m%d", "20030925"), | ||
| ("%Y-%b-%d", "2003-Sep-25"), | ||
| ("%d-%b-%Y", "25-Sep-2003"), | ||
| ("%b-%d-%Y", "Sep-25-2003"), | ||
| ("%m-%d-%Y", "09-25-2003"), | ||
| ("%d-%m-%Y", "25-09-2003"), | ||
| ("%Y.%m.%d", "2003.09.25"), | ||
| ("%Y.%b.%d", "2003.Sep.25"), | ||
| ("%d.%b.%Y", "25.Sep.2003"), | ||
| ("%b.%d.%Y", "Sep.25.2003"), | ||
| ("%m.%d.%Y", "09.25.2003"), | ||
| ("%d.%m.%Y", "25.09.2003"), | ||
| ("%Y/%m/%d", "2003/09/25"), | ||
| ("%Y/%b/%d", "2003/Sep/25"), | ||
| ("%d/%b/%Y", "25/Sep/2003"), | ||
| ("%b/%d/%Y", "Sep/25/2003"), | ||
| ("%m/%d/%Y", "09/25/2003"), | ||
| ("%d/%m/%Y", "25/09/2003"), | ||
| ("%Y %m %d", "2003 09 25"), | ||
| ("%Y %b %d", "2003 Sep 25"), | ||
| ("%d %b %Y", "25 Sep 2003"), | ||
| ("%m %d %Y", "09 25 2003"), | ||
| ("%d %m %Y", "25 09 2003"), | ||
| ("%y %d %b", "03 25 Sep",), | ||
| ]) | ||
| def test_strftime_formats_2003Sep25(self, fmt, dstr): | ||
| expected = datetime(2003, 9, 25) | ||
| # First check that the format strings behave as expected | ||
| # (not strictly necessary, but nice to have) | ||
| assert expected.strftime(fmt) == dstr | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| class TestInputTypes(object): | ||
| def test_empty_string_invalid(self): | ||
| with pytest.raises(ParserError): | ||
| parse('') | ||
| def test_none_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(None) | ||
| def test_int_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(13) | ||
| def test_duck_typing(self): | ||
| # We want to support arbitrary classes that implement the stream | ||
| # interface. | ||
| class StringPassThrough(object): | ||
| def __init__(self, stream): | ||
| self.stream = stream | ||
| def read(self, *args, **kwargs): | ||
| return self.stream.read(*args, **kwargs) | ||
| dstr = StringPassThrough(StringIO('2014 January 19')) | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_stream(self): | ||
| dstr = StringIO('2014 January 19') | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_str(self): | ||
| # Parser should be able to handle bytestring and unicode | ||
| uni_str = '2014-05-01 08:00:00' | ||
| bytes_str = uni_str.encode() | ||
| res = parse(bytes_str) | ||
| expected = parse(uni_str) | ||
| assert res == expected | ||
| def test_parse_bytes(self): | ||
| res = parse(b'2014 January 19') | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_bytearray(self): | ||
| # GH#417 | ||
| res = parse(bytearray(b'2014 January 19')) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| class TestTzinfoInputTypes(object): | ||
| def assert_equal_same_tz(self, dt1, dt2): | ||
| assert dt1 == dt2 | ||
| assert dt1.tzinfo is dt2.tzinfo | ||
| def test_tzinfo_dict_could_return_none(self): | ||
| dstr = "2017-02-03 12:40 BRST" | ||
| result = parse(dstr, tzinfos={"BRST": None}) | ||
| expected = datetime(2017, 2, 3, 12, 40) | ||
| self.assert_equal_same_tz(result, expected) | ||
| def test_tzinfos_callable_could_return_none(self): | ||
| dstr = "2017-02-03 12:40 BRST" | ||
| result = parse(dstr, tzinfos=lambda *args: None) | ||
| expected = datetime(2017, 2, 3, 12, 40) | ||
| self.assert_equal_same_tz(result, expected) | ||
| def test_invalid_tzinfo_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| # Pass an absurd tzinfos object | ||
| tzinfos = {"UTC": ValueError} | ||
| with pytest.raises(TypeError): | ||
| parse(dstr, tzinfos=tzinfos) | ||
| def test_valid_tzinfo_tzinfo_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {"UTC": tz.UTC} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_unicode_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {u"UTC": u"UTC+0"} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_callable_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| def tzinfos(*args, **kwargs): | ||
| return u"UTC+0" | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_int_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {u"UTC": -28800} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800)) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| class ParserTest(unittest.TestCase): | ||
| @classmethod | ||
| def setup_class(cls): | ||
| cls.tzinfos = {"BRST": -10800} | ||
| cls.brsttz = tzoffset("BRST", -10800) | ||
| cls.default = datetime(2003, 9, 25) | ||
| # Parser should be able to handle bytestring and unicode | ||
| cls.uni_str = '2014-05-01 08:00:00' | ||
| cls.str_str = cls.uni_str.encode() | ||
| def testParserParseStr(self): | ||
| from dateutil.parser import parser | ||
| assert parser().parse(self.str_str) == parser().parse(self.uni_str) | ||
| def testParseUnicodeWords(self): | ||
| class rus_parserinfo(parserinfo): | ||
| MONTHS = [("янв", "Январь"), | ||
| ("фев", "Февраль"), | ||
| ("мар", "Март"), | ||
| ("апр", "Апрель"), | ||
| ("май", "Май"), | ||
| ("июн", "Июнь"), | ||
| ("июл", "Июль"), | ||
| ("авг", "Август"), | ||
| ("сен", "Сентябрь"), | ||
| ("окт", "Октябрь"), | ||
| ("ноя", "Ноябрь"), | ||
| ("дек", "Декабрь")] | ||
| expected = datetime(2015, 9, 10, 10, 20) | ||
| res = parse('10 Сентябрь 2015 10:20', parserinfo=rus_parserinfo()) | ||
| assert res == expected | ||
| def testParseWithNulls(self): | ||
| # This relies on the from __future__ import unicode_literals, because | ||
| # explicitly specifying a unicode literal is a syntax error in Py 3.2 | ||
| # May want to switch to u'...' if we ever drop Python 3.2 support. | ||
| pstring = '\x00\x00August 29, 1924' | ||
| assert parse(pstring) == datetime(1924, 8, 29) | ||
| def testDateCommandFormat(self): | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", | ||
| tzinfos=self.tzinfos), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testDateCommandFormatReversed(self): | ||
| self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu", | ||
| tzinfos=self.tzinfos), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testDateCommandFormatWithLong(self): | ||
| if PY2: | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", | ||
| tzinfos={"BRST": long(-10800)}), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testISOFormatStrip2(self): | ||
| self.assertEqual(parse("2003-09-25T10:49:41+03:00"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testISOStrippedFormatStrip2(self): | ||
| self.assertEqual(parse("20030925T104941+0300"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testAMPMNoHour(self): | ||
| with pytest.raises(ParserError): | ||
| parse("AM") | ||
| with pytest.raises(ParserError): | ||
| parse("Jan 20, 2015 PM") | ||
| def testAMPMRange(self): | ||
| with pytest.raises(ParserError): | ||
| parse("13:44 AM") | ||
| with pytest.raises(ParserError): | ||
| parse("January 25, 1921 23:13 PM") | ||
| def testPertain(self): | ||
| self.assertEqual(parse("Sep 03", default=self.default), | ||
| datetime(2003, 9, 3)) | ||
| self.assertEqual(parse("Sep of 03", default=self.default), | ||
| datetime(2003, 9, 25)) | ||
| def testFuzzy(self): | ||
| s = "Today is 25 of September of 2003, exactly " \ | ||
| "at 10:49:41 with timezone -03:00." | ||
| self.assertEqual(parse(s, fuzzy=True), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=self.brsttz)) | ||
| def testFuzzyWithTokens(self): | ||
| s1 = "Today is 25 of September of 2003, exactly " \ | ||
| "at 10:49:41 with timezone -03:00." | ||
| self.assertEqual(parse(s1, fuzzy_with_tokens=True), | ||
| (datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=self.brsttz), | ||
| ('Today is ', 'of ', ', exactly at ', | ||
| ' with timezone ', '.'))) | ||
| s2 = "http://biz.yahoo.com/ipo/p/600221.html" | ||
| self.assertEqual(parse(s2, fuzzy_with_tokens=True), | ||
| (datetime(2060, 2, 21, 0, 0, 0), | ||
| ('http://biz.yahoo.com/ipo/p/', '.html'))) | ||
| def testFuzzyAMPMProblem(self): | ||
| # Sometimes fuzzy parsing results in AM/PM flag being set without | ||
| # hours - if it's fuzzy it should ignore that. | ||
| s1 = "I have a meeting on March 1, 1974." | ||
| s2 = "On June 8th, 2020, I am going to be the first man on Mars" | ||
| # Also don't want any erroneous AM or PMs changing the parsed time | ||
| s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003" | ||
| s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset" | ||
| self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1)) | ||
| self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8)) | ||
| self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3)) | ||
| self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3)) | ||
| def testFuzzyIgnoreAMPM(self): | ||
| s1 = "Jan 29, 1945 14:45 AM I going to see you there?" | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse(s1, fuzzy=True) | ||
| self.assertEqual(res, datetime(1945, 1, 29, 14, 45)) | ||
| def testRandomFormat24(self): | ||
| self.assertEqual(parse("0:00 PM, PST", default=self.default, | ||
| ignoretz=True), | ||
| datetime(2003, 9, 25, 12, 0)) | ||
| def testRandomFormat26(self): | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse("5:50 A.M. on June 13, 1990") | ||
| self.assertEqual(res, datetime(1990, 6, 13, 5, 50)) | ||
| def testUnspecifiedDayFallback(self): | ||
| # Test that for an unspecified day, the fallback behavior is correct. | ||
| self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)), | ||
| datetime(2009, 4, 30)) | ||
| def testUnspecifiedDayFallbackFebNoLeapYear(self): | ||
| self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)), | ||
| datetime(2007, 2, 28)) | ||
| def testUnspecifiedDayFallbackFebLeapYear(self): | ||
| self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), | ||
| datetime(2008, 2, 29)) | ||
| def testErrorType01(self): | ||
| with pytest.raises(ParserError): | ||
| parse('shouldfail') | ||
| def testCorrectErrorOnFuzzyWithTokens(self): | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/32/423', fuzzy_with_tokens=True) | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/04 +32423', fuzzy_with_tokens=True) | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/0d4', fuzzy_with_tokens=True) | ||
| def testIncreasingCTime(self): | ||
| # This test will check 200 different years, every month, every day, | ||
| # every hour, every minute, every second, and every weekday, using | ||
| # a delta of more or less 1 year, 1 month, 1 day, 1 minute and | ||
| # 1 second. | ||
| delta = timedelta(days=365+31+1, seconds=1+60+60*60) | ||
| dt = datetime(1900, 1, 1, 0, 0, 0, 0) | ||
| for i in range(200): | ||
| assert parse(dt.ctime()) == dt | ||
| dt += delta | ||
| def testIncreasingISOFormat(self): | ||
| delta = timedelta(days=365+31+1, seconds=1+60+60*60) | ||
| dt = datetime(1900, 1, 1, 0, 0, 0, 0) | ||
| for i in range(200): | ||
| assert parse(dt.isoformat()) == dt | ||
| dt += delta | ||
| def testMicrosecondsPrecisionError(self): | ||
| # Skip found out that sad precision problem. :-( | ||
| dt1 = parse("00:11:25.01") | ||
| dt2 = parse("00:12:10.01") | ||
| assert dt1.microsecond == 10000 | ||
| assert dt2.microsecond == 10000 | ||
| def testMicrosecondPrecisionErrorReturns(self): | ||
| # One more precision issue, discovered by Eric Brown. This should | ||
| # be the last one, as we're no longer using floating points. | ||
| for ms in [100001, 100000, 99999, 99998, | ||
| 10001, 10000, 9999, 9998, | ||
| 1001, 1000, 999, 998, | ||
| 101, 100, 99, 98]: | ||
| dt = datetime(2008, 2, 27, 21, 26, 1, ms) | ||
| assert parse(dt.isoformat()) == dt | ||
| def testCustomParserInfo(self): | ||
| # Custom parser info wasn't working, as Michael Elsdörfer discovered. | ||
| from dateutil.parser import parserinfo, parser | ||
| class myparserinfo(parserinfo): | ||
| MONTHS = parserinfo.MONTHS[:] | ||
| MONTHS[0] = ("Foo", "Foo") | ||
| myparser = parser(myparserinfo()) | ||
| dt = myparser.parse("01/Foo/2007") | ||
| assert dt == datetime(2007, 1, 1) | ||
| def testCustomParserShortDaynames(self): | ||
| # Horacio Hoyos discovered that day names shorter than 3 characters, | ||
| # for example two letter German day name abbreviations, don't work: | ||
| # https://github.com/dateutil/dateutil/issues/343 | ||
| from dateutil.parser import parserinfo, parser | ||
| class GermanParserInfo(parserinfo): | ||
| WEEKDAYS = [("Mo", "Montag"), | ||
| ("Di", "Dienstag"), | ||
| ("Mi", "Mittwoch"), | ||
| ("Do", "Donnerstag"), | ||
| ("Fr", "Freitag"), | ||
| ("Sa", "Samstag"), | ||
| ("So", "Sonntag")] | ||
| myparser = parser(GermanParserInfo()) | ||
| dt = myparser.parse("Sa 21. Jan 2017") | ||
| self.assertEqual(dt, datetime(2017, 1, 21)) | ||
| def testNoYearFirstNoDayFirst(self): | ||
| dtstr = '090107' | ||
| # Should be MMDDYY | ||
| self.assertEqual(parse(dtstr), | ||
| datetime(2007, 9, 1)) | ||
| self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False), | ||
| datetime(2007, 9, 1)) | ||
| def testYearFirst(self): | ||
| dtstr = '090107' | ||
| # Should be MMDDYY | ||
| self.assertEqual(parse(dtstr, yearfirst=True), | ||
| datetime(2009, 1, 7)) | ||
| self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False), | ||
| datetime(2009, 1, 7)) | ||
| def testDayFirst(self): | ||
| dtstr = '090107' | ||
| # Should be DDMMYY | ||
| self.assertEqual(parse(dtstr, dayfirst=True), | ||
| datetime(2007, 1, 9)) | ||
| self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True), | ||
| datetime(2007, 1, 9)) | ||
| def testDayFirstYearFirst(self): | ||
| dtstr = '090107' | ||
| # Should be YYDDMM | ||
| self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True), | ||
| datetime(2009, 7, 1)) | ||
| def testUnambiguousYearFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, yearfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def testUnambiguousDayFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, dayfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def testUnambiguousDayFirstYearFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def test_mstridx(self): | ||
| # See GH408 | ||
| dtstr = '2015-15-May' | ||
| self.assertEqual(parse(dtstr), | ||
| datetime(2015, 5, 15)) | ||
| def test_idx_check(self): | ||
| dtstr = '2017-07-17 06:15:' | ||
| # Pre-PR, the trailing colon will cause an IndexError at 824-825 | ||
| # when checking `i < len_l` and then accessing `l[i+1]` | ||
| res = parse(dtstr, fuzzy=True) | ||
| assert res == datetime(2017, 7, 17, 6, 15) | ||
| def test_hmBY(self): | ||
| # See GH#483 | ||
| dtstr = '02:17NOV2017' | ||
| res = parse(dtstr, default=self.default) | ||
| assert res == datetime(2017, 11, self.default.day, 2, 17) | ||
| def test_validate_hour(self): | ||
| # See GH353 | ||
| invalid = "201A-01-01T23:58:39.239769+03:00" | ||
| with pytest.raises(ParserError): | ||
| parse(invalid) | ||
| def test_era_trailing_year(self): | ||
| dstr = 'AD2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| def test_includes_timestr(self): | ||
| timestr = "2020-13-97T44:61:83" | ||
| try: | ||
| parse(timestr) | ||
| except ParserError as e: | ||
| assert e.args[1] == timestr | ||
| else: | ||
| pytest.fail("Failed to raise ParserError") | ||
| class TestOutOfBounds(object): | ||
| def test_no_year_zero(self): | ||
| with pytest.raises(ParserError): | ||
| parse("0000 Jun 20") | ||
| def test_out_of_bound_day(self): | ||
| with pytest.raises(ParserError): | ||
| parse("Feb 30, 2007") | ||
| def test_illegal_month_error(self): | ||
| with pytest.raises(ParserError): | ||
| parse("0-100") | ||
| def test_day_sanity(self, fuzzy): | ||
| dstr = "2014-15-25" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_minute_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 22:64" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_hour_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 25:16 PM" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_second_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 22:14:64" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| class TestParseUnimplementedCases(object): | ||
| @pytest.mark.xfail | ||
| def test_somewhat_ambiguous_string(self): | ||
| # Ref: github issue #487 | ||
| # The parser is choosing the wrong part for hour | ||
| # causing datetime to raise an exception. | ||
| dtstr = '1237 PM BRST Mon Oct 30 2017' | ||
| res = parse(dtstr, tzinfo=self.tzinfos) | ||
| assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos) | ||
| @pytest.mark.xfail | ||
| def test_YmdH_M_S(self): | ||
| # found in nasdaq's ftp data | ||
| dstr = '1991041310:19:24' | ||
| expected = datetime(1991, 4, 13, 10, 19, 24) | ||
| res = parse(dstr) | ||
| assert res == expected, (res, expected) | ||
| @pytest.mark.xfail | ||
| def test_first_century(self): | ||
| dstr = '0031 Nov 03' | ||
| expected = datetime(31, 11, 3) | ||
| res = parse(dstr) | ||
| assert res == expected, res | ||
| @pytest.mark.xfail | ||
| def test_era_trailing_year_with_dots(self): | ||
| dstr = 'A.D.2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| @pytest.mark.xfail | ||
| def test_ad_nospace(self): | ||
| expected = datetime(6, 5, 19) | ||
| for dstr in [' 6AD May 19', ' 06AD May 19', | ||
| ' 006AD May 19', ' 0006AD May 19']: | ||
| res = parse(dstr) | ||
| assert res == expected, (dstr, res) | ||
| @pytest.mark.xfail | ||
| def test_four_letter_day(self): | ||
| dstr = 'Frid Dec 30, 2016' | ||
| expected = datetime(2016, 12, 30) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_non_date_number(self): | ||
| dstr = '1,700' | ||
| with pytest.raises(ParserError): | ||
| parse(dstr) | ||
| @pytest.mark.xfail | ||
| def test_on_era(self): | ||
| # This could be classified as an "eras" test, but the relevant part | ||
| # about this is the ` on ` | ||
| dstr = '2:15 PM on January 2nd 1973 A.D.' | ||
| expected = datetime(1973, 1, 2, 14, 15) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(2012, 11, 7) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year_tokens(self): | ||
| # This was found in the wild at insidertrading.org | ||
| # Unlike in the case above, identifying the first "2012" as the year | ||
| # would not be a problem, but inferring that the latter 2012 is hhmm | ||
| # is a problem. | ||
| dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| expected = datetime(2012, 11, 7) | ||
| (res, tokens) = parse(dstr, fuzzy_with_tokens=True) | ||
| assert res == expected | ||
| assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",) | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year2(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust " | ||
| "u/d/t November 2, 1998 f/b/o Jennifer L Berylson") | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1998, 11, 2) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year3(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1994, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_unambiguous_YYYYMM(self): | ||
| # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed | ||
| # as instance of YYMMDD and parser could fallback to YYYYMM format. | ||
| dstr = "201712" | ||
| res = parse(dstr) | ||
| expected = datetime(2017, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_numerical_content(self): | ||
| # ref: https://github.com/dateutil/dateutil/issues/1029 | ||
| # parser interprets price and percentage as parts of the date | ||
| dstr = "£14.99 (25% off, until April 20)" | ||
| res = parse(dstr, fuzzy=True, default=datetime(2000, 1, 1)) | ||
| expected = datetime(2000, 4, 20) | ||
| assert res == expected | ||
| @pytest.mark.skipif(IS_WIN, reason="Windows does not use TZ var") | ||
| class TestTZVar(object): | ||
| def test_parse_unambiguous_nonexistent_local(self): | ||
| # When dates are specified "EST" even when they should be "EDT" in the | ||
| # local time zone, we should still assign the local time zone | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal()) | ||
| dt = parse('2011-08-01T12:30 EST') | ||
| assert dt.tzname() == 'EDT' | ||
| assert dt == dt_exp | ||
| def test_tzlocal_in_gmt(self): | ||
| # GH #318 | ||
| with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'): | ||
| # This is an imaginary datetime in tz.tzlocal() but should still | ||
| # parse using the GMT-as-alias-for-UTC rule | ||
| dt = parse('2004-05-01T12:00 GMT') | ||
| dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.UTC) | ||
| assert dt == dt_exp | ||
| def test_tzlocal_parse_fold(self): | ||
| # One manifestion of GH #318 | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal()) | ||
| dt_exp = tz.enfold(dt_exp, fold=1) | ||
| dt = parse('2011-11-06T01:30 EST') | ||
| # Because this is ambiguous, until `tz.tzlocal() is tz.tzlocal()` | ||
| # we'll just check the attributes we care about rather than | ||
| # dt == dt_exp | ||
| assert dt.tzname() == dt_exp.tzname() | ||
| assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None) | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) | ||
| def test_parse_tzinfos_fold(): | ||
| NYC = tz.gettz('America/New_York') | ||
| tzinfos = {'EST': NYC, 'EDT': NYC} | ||
| dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1) | ||
| dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos) | ||
| assert dt == dt_exp | ||
| assert dt.tzinfo is dt_exp.tzinfo | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) | ||
| @pytest.mark.parametrize('dtstr,dt', [ | ||
| ('5.6h', datetime(2003, 9, 25, 5, 36)), | ||
| ('5.6m', datetime(2003, 9, 25, 0, 5, 36)), | ||
| # '5.6s' never had a rounding problem, test added for completeness | ||
| ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000)) | ||
| ]) | ||
| def test_rounding_floatlike_strings(dtstr, dt): | ||
| assert parse(dtstr, default=datetime(2003, 9, 25)) == dt | ||
| @pytest.mark.parametrize('value', ['1: test', 'Nan']) | ||
| def test_decimal_error(value): | ||
| # GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception | ||
| # when constructed with an invalid value | ||
| with pytest.raises(ParserError): | ||
| parse(value) | ||
| def test_parsererror_repr(): | ||
| # GH 991 — the __repr__ was not properly indented and so was never defined. | ||
| # This tests the current behavior of the ParserError __repr__, but the | ||
| # precise format is not guaranteed to be stable and may change even in | ||
| # minor versions. This test exists to avoid regressions. | ||
| s = repr(ParserError("Problem with string: %s", "2019-01-01")) | ||
| assert s == "ParserError('Problem with string: %s', '2019-01-01')" |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from ._common import NotAValue | ||
| import calendar | ||
| from datetime import datetime, date, timedelta | ||
| import unittest | ||
| import pytest | ||
| from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU | ||
| class RelativeDeltaTest(unittest.TestCase): | ||
| now = datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| today = date(2003, 9, 17) | ||
| def testInheritance(self): | ||
| # Ensure that relativedelta is inheritance-friendly. | ||
| class rdChildClass(relativedelta): | ||
| pass | ||
| ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1, | ||
| hours=1, minutes=1, seconds=1, microseconds=1) | ||
| rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1, | ||
| hours=1, minutes=1, seconds=1, microseconds=1) | ||
| self.assertEqual(type(ccRD + rd), type(ccRD), | ||
| msg='Addition does not inherit type.') | ||
| self.assertEqual(type(ccRD - rd), type(ccRD), | ||
| msg='Subtraction does not inherit type.') | ||
| self.assertEqual(type(-ccRD), type(ccRD), | ||
| msg='Negation does not inherit type.') | ||
| self.assertEqual(type(ccRD * 5.0), type(ccRD), | ||
| msg='Multiplication does not inherit type.') | ||
| self.assertEqual(type(ccRD / 5.0), type(ccRD), | ||
| msg='Division does not inherit type.') | ||
| def testMonthEndMonthBeginning(self): | ||
| self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59), | ||
| datetime(2003, 3, 1, 0, 0, 0)), | ||
| relativedelta(months=-1, seconds=-1)) | ||
| self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), | ||
| datetime(2003, 1, 31, 23, 59, 59)), | ||
| relativedelta(months=1, seconds=1)) | ||
| def testMonthEndMonthBeginningLeapYear(self): | ||
| self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59), | ||
| datetime(2012, 3, 1, 0, 0, 0)), | ||
| relativedelta(months=-1, seconds=-1)) | ||
| self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), | ||
| datetime(2003, 1, 31, 23, 59, 59)), | ||
| relativedelta(months=1, seconds=1)) | ||
| def testNextMonth(self): | ||
| self.assertEqual(self.now+relativedelta(months=+1), | ||
| datetime(2003, 10, 17, 20, 54, 47, 282310)) | ||
| def testNextMonthPlusOneWeek(self): | ||
| self.assertEqual(self.now+relativedelta(months=+1, weeks=+1), | ||
| datetime(2003, 10, 24, 20, 54, 47, 282310)) | ||
| def testNextMonthPlusOneWeek10am(self): | ||
| self.assertEqual(self.today + | ||
| relativedelta(months=+1, weeks=+1, hour=10), | ||
| datetime(2003, 10, 24, 10, 0)) | ||
| def testNextMonthPlusOneWeek10amDiff(self): | ||
| self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0), | ||
| self.today), | ||
| relativedelta(months=+1, days=+7, hours=+10)) | ||
| def testOneMonthBeforeOneYear(self): | ||
| self.assertEqual(self.now+relativedelta(years=+1, months=-1), | ||
| datetime(2004, 8, 17, 20, 54, 47, 282310)) | ||
| def testMonthsOfDiffNumOfDays(self): | ||
| self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1), | ||
| date(2003, 2, 27)) | ||
| self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1), | ||
| date(2003, 2, 28)) | ||
| self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2), | ||
| date(2003, 3, 31)) | ||
| def testMonthsOfDiffNumOfDaysWithYears(self): | ||
| self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1), | ||
| date(2001, 2, 28)) | ||
| self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1), | ||
| date(2001, 2, 28)) | ||
| self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1), | ||
| date(2000, 2, 28)) | ||
| self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), | ||
| date(2000, 3, 1)) | ||
| self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), | ||
| date(2000, 3, 1)) | ||
| self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1), | ||
| date(2000, 2, 28)) | ||
| self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1), | ||
| date(2000, 3, 1)) | ||
| def testNextFriday(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=FR), | ||
| date(2003, 9, 19)) | ||
| def testNextFridayInt(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY), | ||
| date(2003, 9, 19)) | ||
| def testLastFridayInThisMonth(self): | ||
| self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)), | ||
| date(2003, 9, 26)) | ||
| def testLastDayOfFebruary(self): | ||
| self.assertEqual(date(2021, 2, 1) + relativedelta(day=31), | ||
| date(2021, 2, 28)) | ||
| def testLastDayOfFebruaryLeapYear(self): | ||
| self.assertEqual(date(2020, 2, 1) + relativedelta(day=31), | ||
| date(2020, 2, 29)) | ||
| def testNextWednesdayIsToday(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=WE), | ||
| date(2003, 9, 17)) | ||
| def testNextWednesdayNotToday(self): | ||
| self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), | ||
| date(2003, 9, 24)) | ||
| def testAddMoreThan12Months(self): | ||
| self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13), | ||
| date(2005, 1, 1)) | ||
| def testAddNegativeMonths(self): | ||
| self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2), | ||
| date(2002, 11, 1)) | ||
| def test15thISOYearWeek(self): | ||
| self.assertEqual(date(2003, 1, 1) + | ||
| relativedelta(day=4, weeks=+14, weekday=MO(-1)), | ||
| date(2003, 4, 7)) | ||
| def testMillenniumAge(self): | ||
| self.assertEqual(relativedelta(self.now, date(2001, 1, 1)), | ||
| relativedelta(years=+2, months=+8, days=+16, | ||
| hours=+20, minutes=+54, seconds=+47, | ||
| microseconds=+282310)) | ||
| def testJohnAge(self): | ||
| self.assertEqual(relativedelta(self.now, | ||
| datetime(1978, 4, 5, 12, 0)), | ||
| relativedelta(years=+25, months=+5, days=+12, | ||
| hours=+8, minutes=+54, seconds=+47, | ||
| microseconds=+282310)) | ||
| def testJohnAgeWithDate(self): | ||
| self.assertEqual(relativedelta(self.today, | ||
| datetime(1978, 4, 5, 12, 0)), | ||
| relativedelta(years=+25, months=+5, days=+11, | ||
| hours=+12)) | ||
| def testYearDay(self): | ||
| self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260), | ||
| date(2003, 9, 17)) | ||
| self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260), | ||
| date(2002, 9, 17)) | ||
| self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260), | ||
| date(2000, 9, 16)) | ||
| self.assertEqual(self.today+relativedelta(yearday=261), | ||
| date(2003, 9, 18)) | ||
| def testYearDayBug(self): | ||
| # Tests a problem reported by Adam Ryan. | ||
| self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15), | ||
| date(2010, 1, 15)) | ||
| def testNonLeapYearDay(self): | ||
| self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2003, 9, 17)) | ||
| self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2002, 9, 17)) | ||
| self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2000, 9, 17)) | ||
| self.assertEqual(self.today+relativedelta(yearday=261), | ||
| date(2003, 9, 18)) | ||
| def testAddition(self): | ||
| self.assertEqual(relativedelta(days=10) + | ||
| relativedelta(years=1, months=2, days=3, hours=4, | ||
| minutes=5, microseconds=6), | ||
| relativedelta(years=1, months=2, days=13, hours=4, | ||
| minutes=5, microseconds=6)) | ||
| def testAbsoluteAddition(self): | ||
| self.assertEqual(relativedelta() + relativedelta(day=0, hour=0), | ||
| relativedelta(day=0, hour=0)) | ||
| self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(), | ||
| relativedelta(day=0, hour=0)) | ||
| def testAdditionToDatetime(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1), | ||
| datetime(2000, 1, 2)) | ||
| def testRightAdditionToDatetime(self): | ||
| self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1), | ||
| datetime(2000, 1, 2)) | ||
| def testAdditionInvalidType(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(days=3) + 9 | ||
| def testAdditionUnsupportedType(self): | ||
| # For unsupported types that define their own comparators, etc. | ||
| self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) | ||
| def testAdditionFloatValue(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)), | ||
| datetime(2000, 1, 2)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)), | ||
| datetime(2000, 2, 1)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)), | ||
| datetime(2001, 1, 1)) | ||
| def testAdditionFloatFractionals(self): | ||
| self.assertEqual(datetime(2000, 1, 1, 0) + | ||
| relativedelta(days=float(0.5)), | ||
| datetime(2000, 1, 1, 12)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0) + | ||
| relativedelta(hours=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) + | ||
| relativedelta(minutes=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(seconds=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(microseconds=float(500000.25)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| def testSubtraction(self): | ||
| self.assertEqual(relativedelta(days=10) - | ||
| relativedelta(years=1, months=2, days=3, hours=4, | ||
| minutes=5, microseconds=6), | ||
| relativedelta(years=-1, months=-2, days=7, hours=-4, | ||
| minutes=-5, microseconds=-6)) | ||
| def testRightSubtractionFromDatetime(self): | ||
| self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1), | ||
| datetime(2000, 1, 1)) | ||
| def testSubractionWithDatetime(self): | ||
| self.assertRaises(TypeError, lambda x, y: x - y, | ||
| (relativedelta(days=1), datetime(2000, 1, 1))) | ||
| def testSubtractionInvalidType(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(hours=12) - 14 | ||
| def testSubtractionUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) | ||
| def testMultiplication(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28, | ||
| datetime(2000, 1, 29)) | ||
| self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1), | ||
| datetime(2000, 1, 29)) | ||
| def testMultiplicationUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) * NotAValue, NotAValue) | ||
| def testDivision(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28, | ||
| datetime(2000, 1, 2)) | ||
| def testDivisionUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) / NotAValue, NotAValue) | ||
| def testBoolean(self): | ||
| self.assertFalse(relativedelta(days=0)) | ||
| self.assertTrue(relativedelta(days=1)) | ||
| def testAbsoluteValueNegative(self): | ||
| rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3, | ||
| minutes=-5, seconds=-2, microseconds=-12) | ||
| rd_expected = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testAbsoluteValuePositive(self): | ||
| rd_base = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| rd_expected = rd_base | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testComparison(self): | ||
| d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=1) | ||
| d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=1) | ||
| d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=2) | ||
| self.assertEqual(d1, d2) | ||
| self.assertNotEqual(d1, d3) | ||
| def testInequalityTypeMismatch(self): | ||
| # Different type | ||
| self.assertFalse(relativedelta(year=1) == 19) | ||
| def testInequalityUnsupportedType(self): | ||
| self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue) | ||
| def testInequalityWeekdays(self): | ||
| # Different weekdays | ||
| no_wday = relativedelta(year=1997, month=4) | ||
| wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1)) | ||
| wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2)) | ||
| wday_tu = relativedelta(year=1997, month=4, weekday=TU) | ||
| self.assertTrue(wday_mo_1 == wday_mo_1) | ||
| self.assertFalse(no_wday == wday_mo_1) | ||
| self.assertFalse(wday_mo_1 == no_wday) | ||
| self.assertFalse(wday_mo_1 == wday_mo_2) | ||
| self.assertFalse(wday_mo_2 == wday_mo_1) | ||
| self.assertFalse(wday_mo_1 == wday_tu) | ||
| self.assertFalse(wday_tu == wday_mo_1) | ||
| def testMonthOverflow(self): | ||
| self.assertEqual(relativedelta(months=273), | ||
| relativedelta(years=22, months=9)) | ||
| def testWeeks(self): | ||
| # Test that the weeks property is working properly. | ||
| rd = relativedelta(years=4, months=2, weeks=8, days=6) | ||
| self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6)) | ||
| rd.weeks = 3 | ||
| self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6)) | ||
| def testRelativeDeltaRepr(self): | ||
| self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)), | ||
| 'relativedelta(years=+1, months=-1, days=+15)') | ||
| self.assertEqual(repr(relativedelta(months=14, seconds=-25)), | ||
| 'relativedelta(years=+1, months=+2, seconds=-25)') | ||
| self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))), | ||
| 'relativedelta(month=3, weekday=SU(+3), hour=3)') | ||
| def testRelativeDeltaFractionalYear(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(years=1.5) | ||
| def testRelativeDeltaFractionalMonth(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(months=1.5) | ||
| def testRelativeDeltaInvalidDatetimeObject(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2)) | ||
| def testRelativeDeltaFractionalAbsolutes(self): | ||
| # Fractional absolute values will soon be unsupported, | ||
| # check for the deprecation warning. | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(year=2.86) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(month=1.29) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(day=0.44) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(hour=23.98) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(minute=45.21) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(second=13.2) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(microsecond=157221.93) | ||
| def testRelativeDeltaFractionalRepr(self): | ||
| rd = relativedelta(years=3, months=-2, days=1.25) | ||
| self.assertEqual(repr(rd), | ||
| 'relativedelta(years=+3, months=-2, days=+1.25)') | ||
| rd = relativedelta(hours=0.5, seconds=9.22) | ||
| self.assertEqual(repr(rd), | ||
| 'relativedelta(hours=+0.5, seconds=+9.22)') | ||
| def testRelativeDeltaFractionalWeeks(self): | ||
| # Equivalent to days=8, hours=18 | ||
| rd = relativedelta(weeks=1.25) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 11, 18)) | ||
| def testRelativeDeltaFractionalDays(self): | ||
| rd1 = relativedelta(days=1.48) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 4, 11, 31, 12)) | ||
| rd2 = relativedelta(days=1.5) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 4, 12, 0, 0)) | ||
| def testRelativeDeltaFractionalHours(self): | ||
| rd = relativedelta(days=1, hours=12.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 4, 12, 30, 0)) | ||
| def testRelativeDeltaFractionalMinutes(self): | ||
| rd = relativedelta(hours=1, minutes=30.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 3, 1, 30, 30)) | ||
| def testRelativeDeltaFractionalSeconds(self): | ||
| rd = relativedelta(hours=5, minutes=30, seconds=30.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 3, 5, 30, 30, 500000)) | ||
| def testRelativeDeltaFractionalPositiveOverflow(self): | ||
| # Equivalent to (days=1, hours=14) | ||
| rd1 = relativedelta(days=1.5, hours=2) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 4, 14, 0, 0)) | ||
| # Equivalent to (days=1, hours=14, minutes=45) | ||
| rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 4, 14, 45)) | ||
| # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1) | ||
| rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31) | ||
| self.assertEqual(d1 + rd3, | ||
| datetime(2009, 9, 5, 2, 0, 1)) | ||
| def testRelativeDeltaFractionalNegativeDays(self): | ||
| # Equivalent to (days=-1, hours=-1) | ||
| rd1 = relativedelta(days=-1.5, hours=11) | ||
| d1 = datetime(2009, 9, 3, 12, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 2, 11, 0, 0)) | ||
| # Equivalent to (days=-1, hours=-9) | ||
| rd2 = relativedelta(days=-1.25, hours=-3) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 2, 3)) | ||
| def testRelativeDeltaNormalizeFractionalDays(self): | ||
| # Equivalent to (days=2, hours=18) | ||
| rd1 = relativedelta(days=2.75) | ||
| self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18)) | ||
| # Equivalent to (days=1, hours=11, minutes=31, seconds=12) | ||
| rd2 = relativedelta(days=1.48) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=1, hours=11, minutes=31, seconds=12)) | ||
| def testRelativeDeltaNormalizeFractionalDays2(self): | ||
| # Equivalent to (hours=1, minutes=30) | ||
| rd1 = relativedelta(hours=1.5) | ||
| self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30)) | ||
| # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100) | ||
| rd2 = relativedelta(hours=3.28472225) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(hours=3, minutes=17, seconds=5, microseconds=100)) | ||
| def testRelativeDeltaNormalizeFractionalMinutes(self): | ||
| # Equivalent to (minutes=15, seconds=36) | ||
| rd1 = relativedelta(minutes=15.6) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(minutes=15, seconds=36)) | ||
| # Equivalent to (minutes=25, seconds=20, microseconds=25000) | ||
| rd2 = relativedelta(minutes=25.33375) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(minutes=25, seconds=20, microseconds=25000)) | ||
| def testRelativeDeltaNormalizeFractionalSeconds(self): | ||
| # Equivalent to (seconds=45, microseconds=25000) | ||
| rd1 = relativedelta(seconds=45.025) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(seconds=45, microseconds=25000)) | ||
| def testRelativeDeltaFractionalPositiveOverflow2(self): | ||
| # Equivalent to (days=1, hours=14) | ||
| rd1 = relativedelta(days=1.5, hours=2) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(days=1, hours=14)) | ||
| # Equivalent to (days=1, hours=14, minutes=45) | ||
| rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=1, hours=14, minutes=45)) | ||
| # Carry back up - equivalent to: | ||
| # (days=2, hours=2, minutes=0, seconds=2, microseconds=3) | ||
| rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045, | ||
| seconds=31.473, microseconds=500003) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=2, hours=2, minutes=0, | ||
| seconds=2, microseconds=3)) | ||
| def testRelativeDeltaFractionalNegativeOverflow(self): | ||
| # Equivalent to (days=-1) | ||
| rd1 = relativedelta(days=-0.5, hours=-12) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(days=-1)) | ||
| # Equivalent to (days=-1) | ||
| rd2 = relativedelta(days=-1.5, hours=12) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=-1)) | ||
| # Equivalent to (days=-1, hours=-14, minutes=-45) | ||
| rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=-1, hours=-14, minutes=-45)) | ||
| # Equivalent to (days=-1, hours=-14, minutes=+15) | ||
| rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45) | ||
| self.assertEqual(rd4.normalized(), | ||
| relativedelta(days=-1, hours=-14, minutes=+15)) | ||
| # Carry back up - equivalent to: | ||
| # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3) | ||
| rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045, | ||
| seconds=-31.473, microseconds=-500003) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=-2, hours=-2, minutes=0, | ||
| seconds=-2, microseconds=-3)) | ||
| def testInvalidYearDay(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(yearday=367) | ||
| def testAddTimedeltaToUnpopulatedRelativedelta(self): | ||
| td = timedelta( | ||
| days=1, | ||
| seconds=1, | ||
| microseconds=1, | ||
| milliseconds=1, | ||
| minutes=1, | ||
| hours=1, | ||
| weeks=1 | ||
| ) | ||
| expected = relativedelta( | ||
| weeks=1, | ||
| days=1, | ||
| hours=1, | ||
| minutes=1, | ||
| seconds=1, | ||
| microseconds=1001 | ||
| ) | ||
| self.assertEqual(expected, relativedelta() + td) | ||
| def testAddTimedeltaToPopulatedRelativeDelta(self): | ||
| td = timedelta( | ||
| days=1, | ||
| seconds=1, | ||
| microseconds=1, | ||
| milliseconds=1, | ||
| minutes=1, | ||
| hours=1, | ||
| weeks=1 | ||
| ) | ||
| rd = relativedelta( | ||
| year=1, | ||
| month=1, | ||
| day=1, | ||
| hour=1, | ||
| minute=1, | ||
| second=1, | ||
| microsecond=1, | ||
| years=1, | ||
| months=1, | ||
| days=1, | ||
| weeks=1, | ||
| hours=1, | ||
| minutes=1, | ||
| seconds=1, | ||
| microseconds=1 | ||
| ) | ||
| expected = relativedelta( | ||
| year=1, | ||
| month=1, | ||
| day=1, | ||
| hour=1, | ||
| minute=1, | ||
| second=1, | ||
| microsecond=1, | ||
| years=1, | ||
| months=1, | ||
| weeks=2, | ||
| days=2, | ||
| hours=2, | ||
| minutes=2, | ||
| seconds=2, | ||
| microseconds=1002, | ||
| ) | ||
| self.assertEqual(expected, rd + td) | ||
| def testHashable(self): | ||
| try: | ||
| {relativedelta(minute=1): 'test'} | ||
| except: | ||
| self.fail("relativedelta() failed to hash!") | ||
| def testDayOfMonthPlus(self): | ||
| assert [ | ||
| date(2021, 1, 28) + relativedelta(months=1), | ||
| date(2021, 2, 27) + relativedelta(months=1), | ||
| date(2021, 4, 29) + relativedelta(months=1), | ||
| date(2021, 5, 30) + relativedelta(months=1), | ||
| ] == [ | ||
| date(2021, 2, 28), | ||
| date(2021, 3, 27), | ||
| date(2021, 5, 29), | ||
| date(2021, 6, 30), | ||
| ] | ||
| def testLastDayOfMonthPlus(self): | ||
| assert [ | ||
| date(2021, 1, 31) + relativedelta(months=1), | ||
| date(2021, 1, 30) + relativedelta(months=1), | ||
| date(2021, 1, 29) + relativedelta(months=1), | ||
| date(2021, 1, 28) + relativedelta(months=1), | ||
| date(2021, 2, 28) + relativedelta(months=1), | ||
| date(2021, 4, 30) + relativedelta(months=1), | ||
| date(2021, 5, 31) + relativedelta(months=1), | ||
| ] == [ | ||
| date(2021, 2, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 3, 28), | ||
| date(2021, 5, 30), | ||
| date(2021, 6, 30), | ||
| ] | ||
| def testDayOfMonthMinus(self): | ||
| assert [ | ||
| date(2021, 2, 27) - relativedelta(months=1), | ||
| date(2021, 3, 30) - relativedelta(months=1), | ||
| date(2021, 3, 29) - relativedelta(months=1), | ||
| date(2021, 3, 28) - relativedelta(months=1), | ||
| date(2021, 5, 30) - relativedelta(months=1), | ||
| date(2021, 6, 29) - relativedelta(months=1), | ||
| ] == [ | ||
| date(2021, 1, 27), | ||
| date(2021, 2, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 4, 30), | ||
| date(2021, 5, 29), | ||
| ] | ||
| def testLastDayOfMonthMinus(self): | ||
| assert [ | ||
| date(2021, 2, 28) - relativedelta(months=1), | ||
| date(2021, 3, 31) - relativedelta(months=1), | ||
| date(2021, 5, 31) - relativedelta(months=1), | ||
| date(2021, 6, 30) - relativedelta(months=1), | ||
| ] == [ | ||
| date(2021, 1, 28), | ||
| date(2021, 2, 28), | ||
| date(2021, 4, 30), | ||
| date(2021, 5, 30), | ||
| ] | ||
| 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 |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import timedelta, datetime | ||
| from dateutil import tz | ||
| from dateutil import utils | ||
| from dateutil.tz import UTC | ||
| from dateutil.utils import within_delta | ||
| from freezegun import freeze_time | ||
| NYC = tz.gettz("America/New_York") | ||
| @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) | ||
| def test_utils_today(): | ||
| assert utils.today() == datetime(2014, 12, 15, 0, 0, 0) | ||
| @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) | ||
| def test_utils_today_tz_info(): | ||
| assert utils.today(NYC) == datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC) | ||
| @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) | ||
| def test_utils_today_tz_info_different_day(): | ||
| assert utils.today(UTC) == datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC) | ||
| def test_utils_default_tz_info_naive(): | ||
| dt = datetime(2014, 9, 14, 9, 30) | ||
| assert utils.default_tzinfo(dt, NYC).tzinfo is NYC | ||
| def test_utils_default_tz_info_aware(): | ||
| dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) | ||
| assert utils.default_tzinfo(dt, NYC).tzinfo is UTC | ||
| def test_utils_within_delta(): | ||
| d1 = datetime(2016, 1, 1, 12, 14, 1, 9) | ||
| d2 = d1.replace(microsecond=15) | ||
| assert within_delta(d1, d2, timedelta(seconds=1)) | ||
| assert not within_delta(d1, d2, timedelta(microseconds=1)) | ||
| def test_utils_within_delta_with_negative_delta(): | ||
| d1 = datetime(2016, 1, 1) | ||
| d2 = datetime(2015, 12, 31) | ||
| assert within_delta(d2, d1, timedelta(days=-1)) |
@@ -25,7 +25,12 @@ # This workflow is triggered three ways: | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - uses: actions/checkout@v3 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v2 | ||
| uses: actions/setup-python@v3 | ||
| with: | ||
| python-version: '3.9' | ||
| - name: Run updatezinfo.py (Unix) | ||
| run: | | ||
| python -m venv venv | ||
| venv/bin/pip install -e . | ||
| ./ci_tools/retry.sh venv/bin/python updatezinfo.py | ||
| - name: Install dependencies | ||
@@ -32,0 +37,0 @@ run: | |
@@ -22,6 +22,26 @@ name: Validate | ||
| "3.9", | ||
| "pypy2", | ||
| "pypy3", | ||
| "3.10", | ||
| "3.11", | ||
| "pypy-2.7", | ||
| "pypy-3.8", | ||
| ] | ||
| os: [ubuntu-latest, windows-latest, macos-latest] | ||
| exclude: | ||
| - python-version: "2.7" | ||
| os: "ubuntu-latest" | ||
| - python-version: "2.7" | ||
| os: "windows-latest" | ||
| - python-version: "2.7" | ||
| os: "macos-latest" | ||
| - python-version: "3.5" | ||
| os: "ubuntu-latest" | ||
| - python-version: "3.6" | ||
| os: "ubuntu-latest" | ||
| include: | ||
| - python-version: "2.7" | ||
| os: "ubuntu-20.04" | ||
| - python-version: "3.5" | ||
| os: "ubuntu-20.04" | ||
| - python-version: "3.6" | ||
| os: "ubuntu-20.04" | ||
| runs-on: ${{ matrix.os }} | ||
@@ -31,9 +51,16 @@ env: | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} | ||
| uses: actions/setup-python@v1 | ||
| - uses: actions/checkout@v3 | ||
| - if: ${{ matrix.python-version == '2.7' }} | ||
| run: | | ||
| sudo apt-get install python-is-python2 | ||
| curl -sSL https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py | ||
| python get-pip.py | ||
| name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} | ||
| - if: ${{ matrix.python-version != '2.7' }} | ||
| name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} | ||
| uses: actions/setup-python@v3 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| - name: Install dependencies | ||
| run: python -m pip install -U tox | ||
| run: python -m pip install -U tox six | ||
| - name: Install zic (Windows) | ||
@@ -57,3 +84,3 @@ run: | | ||
| - name: Report coverage to Codecov | ||
| uses: codecov/codecov-action@v1 | ||
| uses: codecov/codecov-action@v3 | ||
| with: | ||
@@ -64,29 +91,38 @@ file: ./.tox/coverage.xml | ||
| docs: | ||
| runs-on: ubuntu-latest | ||
| other: | ||
| runs-on: "ubuntu-latest" | ||
| strategy: | ||
| matrix: | ||
| toxenv: ["docs", "tz", "precommit"] | ||
| env: | ||
| TOXENV: ${{ matrix.toxenv }} | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v1 | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| python-version: 3.6 | ||
| fetch-depth: 0 | ||
| - name: ${{ matrix.toxenv }} | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: "3.10" | ||
| - name: Install tox | ||
| run: python -m pip install -U tox | ||
| - name: Run tox | ||
| run: python -m tox -e docs | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| python -m pip install -U tox | ||
| - name: Run action | ||
| run: tox | ||
| latest-tz: | ||
| darker: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v1 | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| python-version: 3.6 | ||
| - name: Install tox | ||
| run: python -m pip install -U "tox<3.8.0" | ||
| - name: Run updatezinfo.py | ||
| run: ./ci_tools/retry.sh python updatezinfo.py | ||
| - name: Run tox | ||
| run: python -m tox -e tz | ||
| fetch-depth: 0 | ||
| - uses: actions/setup-python@v4 | ||
| - uses: akaihola/darker@0fb2501a3f6c1b2d64976afa57885aeec0601182 | ||
| # pinned due to unreleased fix: https://github.com/akaihola/darker/issues/489 | ||
| with: | ||
| options: "--check --diff --color --isort" | ||
| src: "." | ||
| version: "~=1.7.1" | ||
@@ -96,7 +132,7 @@ build-dist: | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - uses: actions/checkout@v3 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v2 | ||
| uses: actions/setup-python@v3 | ||
| with: | ||
| python-version: "3.9" | ||
| python-version: "3.10" | ||
| - name: Install tox | ||
@@ -103,0 +139,0 @@ run: python -m pip install -U tox |
+1
-1
@@ -20,3 +20,3 @@ # Byte-compiled / optimized / DLL files | ||
| # Autogenerated version information | ||
| dateutil/_version.py | ||
| src/dateutil/_version.py | ||
@@ -23,0 +23,0 @@ # Sphinx documentation |
+1
-1
@@ -31,3 +31,3 @@ build: false | ||
| - set DATEUTIL_MAY_CHANGE_TZ=1 | ||
| - C:\Python36\python -m pip install -U tox | ||
| - C:\Python36\python -m pip install -U tox virtualenv==20.4.7 | ||
@@ -34,0 +34,0 @@ test_script: |
+3
-0
@@ -18,2 +18,3 @@ This is a (possibly incomplete) list of all the contributors to python-dateutil, | ||
| - Alec Reiter <areiter@MASKED> | ||
| - Aleksei Strizhak <alexei.mifrill.strizhak@MASKED> (gh: @Mifrill) | ||
| - Alex Chamberlain (gh: @alexchamberlain) **D** | ||
@@ -63,2 +64,3 @@ - Alex Verdyan <verdyan@MASKED> | ||
| - Jake Chorley (gh: @jakec-github) **D** | ||
| - Jakub Kulík (gh: @kulikjak) **D** | ||
| - Jan Studený <jendas1@MASKED> | ||
@@ -113,2 +115,3 @@ - Jay Weisskopf <jay@jayschwa.net> (gh: @jayschwa) **D** | ||
| - Thomas Achtemichuk <tom@MASKED> | ||
| - Thomas Grainger <tagrain@gmail.com> (gh: @graingert) **D** | ||
| - Thomas Kluyver <takowl@MASKED> (gh: @takluyver) | ||
@@ -115,0 +118,0 @@ - Tim Gates <tim.gates@iress.com> (gh: timgates42) |
@@ -68,3 +68,3 @@ pool: | ||
| ./ci_tools/retry.sh python updatezinfo.py | ||
| python -m tox -- dateutil/test --cov-config=tox.ini --cov=dateutil --junitxml=unittests/TEST-$(Agent.JobName).xml | ||
| python -m tox -- tests --cov-config=tox.ini --cov=dateutil --junitxml=unittests/TEST-$(Agent.JobName).xml | ||
| python -m tox -e coverage,codecov || true | ||
@@ -71,0 +71,0 @@ else |
@@ -14,3 +14,3 @@ #!/usr/bin/env bash | ||
| REPO_TARBALL=${REPO_DIR}/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | ||
| REPO_TARBALL=${REPO_DIR}/src/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | ||
| TMP_TARBALL=${TMP_DIR}/dateutil-zoneinfo.tar.gz | ||
@@ -97,3 +97,3 @@ | ||
| # Run the tests | ||
| python -m pytest ${REPO_DIR}/dateutil/test $EXTRA_TEST_ARGS | ||
| python -m pytest ${REPO_DIR}/tests $EXTRA_TEST_ARGS | ||
+1
-1
@@ -13,3 +13,3 @@ # Contributing | ||
| 2. The platform and versions of everything involved, at a minimum please include operating system, `python` version and `dateutil` version. Instructions on getting your versions: | ||
| - `dateutil`: `python -c 'import dateutil; print(dateutil.__version__)'` | ||
| - `dateutil`: `python -c "import dateutil; print(dateutil.__version__)"` | ||
| - `Python`: `python --version` | ||
@@ -16,0 +16,0 @@ |
@@ -94,2 +94,11 @@ ============= | ||
| If the result falls on a day after the last one of the month, the last day of the month is used instead. | ||
| .. doctest:: | ||
| >>> date(2003,1,30)+relativedelta(months=+1) | ||
| datetime.date(2003, 2, 28) | ||
| >>> date(2003,5,31)+relativedelta(months=-1) | ||
| datetime.date(2003, 4, 30) | ||
| .. doctest:: relativedelta | ||
@@ -96,0 +105,0 @@ |
+1
-1
@@ -344,3 +344,3 @@ ===== | ||
| Yearly in June and July for 10 occurrences. | ||
| Yearly in June and July for 4 occurrences. | ||
@@ -347,0 +347,0 @@ .. doctest:: rrule |
+1
-1
| include LICENSE NEWS zonefile_metadata.json updatezinfo.py pyproject.toml | ||
| recursive-include dateutil/test * | ||
| recursive-include tests/ * | ||
| global-exclude __pycache__ | ||
| global-exclude *.py[co] |
+34
-0
@@ -0,1 +1,35 @@ | ||
| Version 2.9.0 (2024-02-29) | ||
| ========================== | ||
| Data updates | ||
| ------------ | ||
| - Updated tzdata version to 2024a. (gh pr #1342) | ||
| Features | ||
| -------- | ||
| - Made all ``dateutil`` submodules lazily imported using `PEP 562 | ||
| <https://www.python.org/dev/peps/pep-0562/>`_. On Python 3.7+, things like | ||
| ``import dateutil; dateutil.tz.gettz("America/New_York")`` will now work | ||
| without explicitly importing ``dateutil.tz``, with the import occurring behind | ||
| the scenes on first use. The old behavior remains on Python 3.6 and earlier. | ||
| Fixed by Orson Adams. (gh issue #771, gh pr #1007) | ||
| Bugfixes | ||
| -------- | ||
| - Removed a call to ``datetime.utcfromtimestamp``, which is deprecated as of Python 3.12. Reported by Hugo van Kemenade (gh pr #1284), fixed by Thomas Grainger (gh pr #1285). | ||
| Documentation changes | ||
| --------------------- | ||
| - Added note into docs and tests where relativedelta would return last day of the month | ||
| only if the same day on a different month resolves to a date that doesn't exist. | ||
| Reported by @hawkEye-01 (gh issue #1167). Fixed by @Mifrill (gh pr #1168) | ||
| Version 2.8.2 (2021-07-08) | ||
@@ -2,0 +36,0 @@ ========================== |
+6
-5
| Metadata-Version: 2.1 | ||
| Name: python-dateutil | ||
| Version: 2.8.2 | ||
| Version: 2.9.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
@@ -13,3 +13,2 @@ Home-page: https://github.com/dateutil/dateutil | ||
| Project-URL: Source, https://github.com/dateutil/dateutil | ||
| Platform: UNKNOWN | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
@@ -30,2 +29,5 @@ Classifier: Intended Audience :: Developers | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Topic :: Software Development :: Libraries | ||
@@ -35,2 +37,3 @@ Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,>=2.7 | ||
| License-File: LICENSE | ||
| Requires-Dist: six>=1.5 | ||
@@ -186,3 +189,3 @@ dateutil - powerful extensions to datetime | ||
| =========== ============================ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| =========== ============================ | ||
@@ -206,3 +209,1 @@ | ||
| https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB | ||
+18
-0
@@ -49,1 +49,19 @@ [build-system] | ||
| [tool.black] | ||
| line-length = 80 | ||
| [tool.isort] | ||
| atomic=true | ||
| force_grid_wrap=0 | ||
| include_trailing_comma=true | ||
| known_first_party = ["dateutil"] | ||
| known_third_party=[ | ||
| "pytest", | ||
| "hypothesis", | ||
| "six", | ||
| "freezegun", | ||
| "mock", | ||
| ] | ||
| multi_line_output=3 | ||
| use_parentheses=true | ||
+1
-1
@@ -150,3 +150,3 @@ dateutil - powerful extensions to datetime | ||
| =========== ============================ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| =========== ============================ | ||
@@ -153,0 +153,0 @@ |
+6
-3
@@ -34,2 +34,5 @@ [bdist_wheel] | ||
| Programming Language :: Python :: 3.9 | ||
| Programming Language :: Python :: 3.10 | ||
| Programming Language :: Python :: 3.11 | ||
| Programming Language :: Python :: 3.12 | ||
| Topic :: Software Development :: Libraries | ||
@@ -41,9 +44,9 @@ | ||
| install_requires = six >= 1.5 | ||
| package_dir = | ||
| =src | ||
| python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.* | ||
| packages = find: | ||
| test_suite = dateutil.test | ||
| [options.packages.find] | ||
| exclude = | ||
| dateutil.test | ||
| where = src | ||
@@ -50,0 +53,0 @@ [options.package_data] |
+1
-1
@@ -51,3 +51,3 @@ #!/usr/bin/python | ||
| use_scm_version={ | ||
| 'write_to': 'dateutil/_version.py', | ||
| 'write_to': 'src/dateutil/_version.py', | ||
| }, | ||
@@ -54,0 +54,0 @@ ## Needed since doctest not supported by PyPA. |
+22
-6
@@ -8,2 +8,6 @@ [tox] | ||
| py37, | ||
| py38, | ||
| py39, | ||
| py310, | ||
| py311, | ||
| pypy, | ||
@@ -16,2 +20,5 @@ pypy3, | ||
| isolated_build = true | ||
| requires = | ||
| tox<4 | ||
| virtualenv<20.22.0 | ||
@@ -27,3 +34,3 @@ [testenv:.package] | ||
| passenv = DATEUTIL_MAY_CHANGE_TZ TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* CODECOV_* SYSTEM_* AGENT_* BUILD_* TF_BUILD | ||
| commands = python -m pytest {posargs: "{toxinidir}/dateutil/test" "{toxinidir}/docs" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| commands = python -m pytest {posargs: "{toxinidir}/tests" "{toxinidir}/docs" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| deps = -rrequirements-dev.txt | ||
@@ -35,3 +42,3 @@ | ||
| passenv = DATEUTIL_MAY_CHANGE_TZ TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* CODECOV_* SYSTEM_* AGENT_* BUILD_* TF_BUILD | ||
| commands = python -m pytest {posargs: "{toxinidir}/dateutil/test" "{toxinidir}/docs" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| commands = python -m pytest {posargs: "{toxinidir}/tests" "{toxinidir}/docs" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| deps = | ||
@@ -71,2 +78,12 @@ -rrequirements/3.3/requirements-dev.txt | ||
| [testenv:precommit] | ||
| description = Run the pre-commit hooks on all files | ||
| passenv = | ||
| PRE_COMMIT* | ||
| deps = | ||
| pre-commit | ||
| commands = | ||
| pre-commit install -f --install-hooks | ||
| pre-commit run --all-files | ||
| [testenv:tz] | ||
@@ -88,4 +105,4 @@ # Warning: This will modify the repository and is only intended to be run | ||
| {[testenv]deps} | ||
| commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -bhtml} | ||
| sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck} | ||
| commands = python -m sphinx -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -bhtml} | ||
| python -m sphinx -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck} | ||
| python setup.py check -r -s | ||
@@ -96,3 +113,3 @@ | ||
| description = Invoke towncrier to update the NEWS file | ||
| basepython = python3.7 | ||
| basepython = python3.11 | ||
| passenv = * | ||
@@ -105,3 +122,2 @@ deps = towncrier | ||
| description = Build an sdist and bdist | ||
| basepython = python3.9 | ||
| skip_install = true | ||
@@ -108,0 +124,0 @@ passenv = * |
+9
-0
@@ -10,2 +10,11 @@ #!/usr/bin/env python | ||
| try: | ||
| import dateutil | ||
| except ImportError: | ||
| print("dateutil not installed locally, adding src to Python path") | ||
| import sys | ||
| here = os.path.dirname(__file__) | ||
| sys.path.append(os.path.join(here, "src")) | ||
| print(sys.path) | ||
| from dateutil.zoneinfo import rebuild | ||
@@ -12,0 +21,0 @@ |
| { | ||
| "metadata_version": 2.0, | ||
| "releases_url": [ | ||
| "https://dateutil.github.io/tzdata/tzdata/", | ||
| "ftp://ftp.iana.org/tz/releases/" | ||
| ], | ||
| "tzdata_file": "tzdata2021a.tar.gz", | ||
| "tzdata_file_sha512": "7cdd762ec90ce12a30fa36b1d66d1ea82d9fa21e514e2b9c7fcbe2541514ee0fadf30843ff352c65512fb270857b51d1517b45e1232b89c6f954ba9ff1833bb3", | ||
| "tzversion": "2021a", | ||
| "tzdata_file": "tzdata2024a.tar.gz", | ||
| "tzdata_file_sha512": "1f09f1b2327cc9e1afc7e9045e83ee3377918dafe1bee2f282b6991828d03b3c70a4d3a17f9207dfb1361bb25bc214a8922a756e84fa114e9ba476226db57236", | ||
| "tzversion": "2024a", | ||
| "zonegroups": [ | ||
@@ -11,0 +10,0 @@ "africa", |
| # -*- coding: utf-8 -*- | ||
| try: | ||
| from ._version import version as __version__ | ||
| except ImportError: | ||
| __version__ = 'unknown' | ||
| __all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', | ||
| 'utils', 'zoneinfo'] |
| """ | ||
| 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 |
| # coding: utf-8 | ||
| # file generated by setuptools_scm | ||
| # don't change, don't track in version control | ||
| version = '2.8.2' | ||
| version_tuple = (2, 8, 2) |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a generic Easter computing method for any given year, using | ||
| Western, Orthodox or Julian algorithms. | ||
| """ | ||
| import datetime | ||
| __all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] | ||
| EASTER_JULIAN = 1 | ||
| EASTER_ORTHODOX = 2 | ||
| EASTER_WESTERN = 3 | ||
| def easter(year, method=EASTER_WESTERN): | ||
| """ | ||
| This method was ported from the work done by GM Arts, | ||
| on top of the algorithm by Claus Tondering, which was | ||
| based in part on the algorithm of Ouding (1940), as | ||
| quoted in "Explanatory Supplement to the Astronomical | ||
| Almanac", P. Kenneth Seidelmann, editor. | ||
| This algorithm implements three different Easter | ||
| calculation methods: | ||
| 1. Original calculation in Julian calendar, valid in | ||
| dates after 326 AD | ||
| 2. Original method, with date converted to Gregorian | ||
| calendar, valid in years 1583 to 4099 | ||
| 3. Revised method, in Gregorian calendar, valid in | ||
| years 1583 to 4099 as well | ||
| These methods are represented by the constants: | ||
| * ``EASTER_JULIAN = 1`` | ||
| * ``EASTER_ORTHODOX = 2`` | ||
| * ``EASTER_WESTERN = 3`` | ||
| The default method is method 3. | ||
| More about the algorithm may be found at: | ||
| `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_ | ||
| and | ||
| `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_ | ||
| """ | ||
| if not (1 <= method <= 3): | ||
| raise ValueError("invalid method") | ||
| # g - Golden year - 1 | ||
| # c - Century | ||
| # h - (23 - Epact) mod 30 | ||
| # i - Number of days from March 21 to Paschal Full Moon | ||
| # j - Weekday for PFM (0=Sunday, etc) | ||
| # p - Number of days from March 21 to Sunday on or before PFM | ||
| # (-6 to 28 methods 1 & 3, to 56 for method 2) | ||
| # e - Extra days to add for method 2 (converting Julian | ||
| # date to Gregorian date) | ||
| y = year | ||
| g = y % 19 | ||
| e = 0 | ||
| if method < 3: | ||
| # Old method | ||
| i = (19*g + 15) % 30 | ||
| j = (y + y//4 + i) % 7 | ||
| if method == 2: | ||
| # Extra dates to convert Julian to Gregorian date | ||
| e = 10 | ||
| if y > 1600: | ||
| e = e + y//100 - 16 - (y//100 - 16)//4 | ||
| else: | ||
| # New method | ||
| c = y//100 | ||
| h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 | ||
| i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) | ||
| j = (y + y//4 + i + 2 - c + c//4) % 7 | ||
| # p can be from -6 to 56 corresponding to dates 22 March to 23 May | ||
| # (later dates apply to method 2, although 23 May never actually occurs) | ||
| p = i - j + e | ||
| d = 1 + (p + 27 + (p + 6)//40) % 31 | ||
| m = 3 + (p + 26)//30 | ||
| return datetime.date(int(y), int(m), int(d)) |
| # -*- coding: utf-8 -*- | ||
| from ._parser import parse, parser, parserinfo, ParserError | ||
| from ._parser import DEFAULTPARSER, DEFAULTTZPARSER | ||
| from ._parser import UnknownTimezoneWarning | ||
| from ._parser import __doc__ | ||
| from .isoparser import isoparser, isoparse | ||
| __all__ = ['parse', 'parser', 'parserinfo', | ||
| 'isoparse', 'isoparser', | ||
| 'ParserError', | ||
| 'UnknownTimezoneWarning'] | ||
| ### | ||
| # Deprecate portions of the private interface so that downstream code that | ||
| # is improperly relying on it is given *some* notice. | ||
| def __deprecated_private_func(f): | ||
| from functools import wraps | ||
| import warnings | ||
| msg = ('{name} is a private function and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=f.__name__) | ||
| @wraps(f) | ||
| def deprecated_func(*args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| return f(*args, **kwargs) | ||
| return deprecated_func | ||
| def __deprecate_private_class(c): | ||
| import warnings | ||
| msg = ('{name} is a private class and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=c.__name__) | ||
| class private_class(c): | ||
| __doc__ = c.__doc__ | ||
| def __init__(self, *args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| super(private_class, self).__init__(*args, **kwargs) | ||
| private_class.__name__ = c.__name__ | ||
| return private_class | ||
| from ._parser import _timelex, _resultbase | ||
| from ._parser import _tzparser, _parsetz | ||
| _timelex = __deprecate_private_class(_timelex) | ||
| _tzparser = __deprecate_private_class(_tzparser) | ||
| _resultbase = __deprecate_private_class(_resultbase) | ||
| _parsetz = __deprecated_private_func(_parsetz) |
| # -*- 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 | ||
| <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_ | ||
| - `W3C Date and Time Formats <https://www.w3.org/TR/NOTE-datetime>`_ | ||
| - `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_ | ||
| - `CPAN ParseDate module | ||
| <https://metacpan.org/pod/release/MUIR/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_ | ||
| - `Java SimpleDateFormat Class | ||
| <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_ | ||
| """ | ||
| 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 integer_types, text_type | ||
| from decimal import Decimal | ||
| from warnings import warn | ||
| from .. import relativedelta | ||
| from .. import tz | ||
| __all__ = ["parse", "parserinfo", "ParserError"] | ||
| # 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 isinstance(instream, (bytes, bytearray)): | ||
| 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", "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' 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 leap year | ||
| 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 precedence 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 ParserError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :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 ParserError("Unknown string format: %s", timestr) | ||
| if len(res) == 0: | ||
| raise ParserError("String does not contain a date: %s", timestr) | ||
| try: | ||
| ret = self._build_naive(res, default) | ||
| except ValueError as e: | ||
| six.raise_from(ParserError(str(e) + ": %s", timestr), e) | ||
| 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" preceding 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) | ||
| or token in self.info.UTCZONE)) | ||
| 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 *don't* 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 _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) | ||
| # ------------------------------------------------------------------ | ||
| # Handling for individual tokens. These are kept as methods instead | ||
| # of functions for the sake of customizability via subclassing. | ||
| def _parsems(self, value): | ||
| """Parse a I[.F] seconds value into (seconds, microseconds).""" | ||
| if "." not in value: | ||
| return int(value), 0 | ||
| else: | ||
| i, f = value.split(".") | ||
| return int(i), int(f.ljust(6, "0")[:6]) | ||
| def _to_decimal(self, val): | ||
| try: | ||
| decimal_value = Decimal(val) | ||
| # See GH 662, edge case, infinite value should not be converted | ||
| # via `_to_decimal` | ||
| if not decimal_value.is_finite(): | ||
| raise ValueError("Converted decimal value is infinite or NaN") | ||
| except Exception as e: | ||
| msg = "Could not convert %s to decimal" % val | ||
| six.raise_from(ValueError(msg), e) | ||
| else: | ||
| return decimal_value | ||
| # ------------------------------------------------------------------ | ||
| # Post-Parsing construction of datetime output. These are kept as | ||
| # methods instead of functions for the sake of customizability via | ||
| # subclassing. | ||
| def _build_tzinfo(self, tzinfos, tzname, tzoffset): | ||
| if callable(tzinfos): | ||
| 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) | ||
| else: | ||
| raise TypeError("Offset must be tzinfo subclass, tz string, " | ||
| "or int offset.") | ||
| 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.UTC) | ||
| elif res.tzoffset == 0: | ||
| aware = naive.replace(tzinfo=tz.UTC) | ||
| 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 _recombine_skipped(self, tokens, skipped_idxs): | ||
| """ | ||
| >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] | ||
| >>> skipped_idxs = [0, 1, 2, 5] | ||
| >>> _recombine_skipped(tokens, skipped_idxs) | ||
| ["foo bar", "baz"] | ||
| """ | ||
| skipped_tokens = [] | ||
| for i, idx in enumerate(sorted(skipped_idxs)): | ||
| if i > 0 and idx - 1 == skipped_idxs[i - 1]: | ||
| skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] | ||
| else: | ||
| skipped_tokens.append(tokens[idx]) | ||
| return skipped_tokens | ||
| DEFAULTPARSER = parser() | ||
| 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 ParserError: | ||
| Raised for invalid or unknown string formats, 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 ParserError(ValueError): | ||
| """Exception subclass used for any failure to parse a datetime string. | ||
| This is a subclass of :py:exc:`ValueError`, and should be raised any time | ||
| earlier versions of ``dateutil`` would have raised ``ValueError``. | ||
| .. versionadded:: 2.8.1 | ||
| """ | ||
| def __str__(self): | ||
| try: | ||
| return self.args[0] % self.args[1:] | ||
| except (TypeError, IndexError): | ||
| return super(ParserError, self).__str__() | ||
| def __repr__(self): | ||
| args = ", ".join("'%s'" % arg for arg in self.args) | ||
| return "%s(%s)" % (self.__class__.__name__, args) | ||
| class UnknownTimezoneWarning(RuntimeWarning): | ||
| """Raised when the parser finds a timezone it cannot parse into a tzinfo. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| # vim:ts=4:sw=4:et |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a parser for ISO-8601 strings | ||
| It is intended to support all valid date, time and datetime formats per the | ||
| ISO-8601 specification. | ||
| ..versionadded:: 2.7.0 | ||
| """ | ||
| from datetime import datetime, timedelta, time, date | ||
| import calendar | ||
| from dateutil import tz | ||
| from functools import wraps | ||
| import re | ||
| import six | ||
| __all__ = ["isoparse", "isoparser"] | ||
| def _takes_ascii(f): | ||
| @wraps(f) | ||
| def func(self, str_in, *args, **kwargs): | ||
| # If it's a stream, read the whole thing | ||
| str_in = getattr(str_in, 'read', lambda: str_in)() | ||
| # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII | ||
| if isinstance(str_in, six.text_type): | ||
| # ASCII is the same in UTF-8 | ||
| try: | ||
| str_in = str_in.encode('ascii') | ||
| except UnicodeEncodeError as e: | ||
| msg = 'ISO-8601 strings should contain only ASCII characters' | ||
| six.raise_from(ValueError(msg), e) | ||
| return f(self, str_in, *args, **kwargs) | ||
| return func | ||
| class isoparser(object): | ||
| def __init__(self, sep=None): | ||
| """ | ||
| :param sep: | ||
| A single character that separates date and time portions. If | ||
| ``None``, the parser will accept any single character. | ||
| For strict ISO-8601 adherence, pass ``'T'``. | ||
| """ | ||
| if sep is not None: | ||
| if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): | ||
| raise ValueError('Separator must be a single, non-numeric ' + | ||
| 'ASCII character') | ||
| sep = sep.encode('ascii') | ||
| self._sep = sep | ||
| @_takes_ascii | ||
| def isoparse(self, dt_str): | ||
| """ | ||
| Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. | ||
| An ISO-8601 datetime string consists of a date portion, followed | ||
| optionally by a time portion - the date and time portions are separated | ||
| by a single character separator, which is ``T`` in the official | ||
| standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be | ||
| combined with a time portion. | ||
| Supported date formats are: | ||
| Common: | ||
| - ``YYYY`` | ||
| - ``YYYY-MM`` or ``YYYYMM`` | ||
| - ``YYYY-MM-DD`` or ``YYYYMMDD`` | ||
| Uncommon: | ||
| - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) | ||
| - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day | ||
| The ISO week and day numbering follows the same logic as | ||
| :func:`datetime.date.isocalendar`. | ||
| Supported time formats are: | ||
| - ``hh`` | ||
| - ``hh:mm`` or ``hhmm`` | ||
| - ``hh:mm:ss`` or ``hhmmss`` | ||
| - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) | ||
| Midnight is a special case for `hh`, as the standard supports both | ||
| 00:00 and 24:00 as a representation. The decimal separator can be | ||
| either a dot or a comma. | ||
| .. caution:: | ||
| Support for fractional components other than seconds is part of the | ||
| ISO-8601 standard, but is not currently implemented in this parser. | ||
| Supported time zone offset formats are: | ||
| - `Z` (UTC) | ||
| - `±HH:MM` | ||
| - `±HHMM` | ||
| - `±HH` | ||
| Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, | ||
| with the exception of UTC, which will be represented as | ||
| :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such | ||
| as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. | ||
| :param dt_str: | ||
| A string or stream containing only an ISO-8601 datetime string | ||
| :return: | ||
| Returns a :class:`datetime.datetime` representing the string. | ||
| Unspecified components default to their lowest value. | ||
| .. warning:: | ||
| As of version 2.7.0, the strictness of the parser should not be | ||
| considered a stable part of the contract. Any valid ISO-8601 string | ||
| that parses correctly with the default settings will continue to | ||
| parse correctly in future versions, but invalid strings that | ||
| currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not | ||
| guaranteed to continue failing in future versions if they encode | ||
| a valid date. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| components, pos = self._parse_isodate(dt_str) | ||
| if len(dt_str) > pos: | ||
| if self._sep is None or dt_str[pos:pos + 1] == self._sep: | ||
| components += self._parse_isotime(dt_str[pos + 1:]) | ||
| else: | ||
| raise ValueError('String contains unknown ISO components') | ||
| if len(components) > 3 and components[3] == 24: | ||
| components[3] = 0 | ||
| return datetime(*components) + timedelta(days=1) | ||
| return datetime(*components) | ||
| @_takes_ascii | ||
| def parse_isodate(self, datestr): | ||
| """ | ||
| Parse the date portion of an ISO string. | ||
| :param datestr: | ||
| The string portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.date` object | ||
| """ | ||
| components, pos = self._parse_isodate(datestr) | ||
| if pos < len(datestr): | ||
| raise ValueError('String contains unknown ISO ' + | ||
| 'components: {!r}'.format(datestr.decode('ascii'))) | ||
| return date(*components) | ||
| @_takes_ascii | ||
| def parse_isotime(self, timestr): | ||
| """ | ||
| Parse the time portion of an ISO string. | ||
| :param timestr: | ||
| The time portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.time` object | ||
| """ | ||
| components = self._parse_isotime(timestr) | ||
| if components[0] == 24: | ||
| components[0] = 0 | ||
| return time(*components) | ||
| @_takes_ascii | ||
| def parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| """ | ||
| Parse a valid ISO time zone string. | ||
| See :func:`isoparser.isoparse` for details on supported formats. | ||
| :param tzstr: | ||
| A string representing an ISO time zone offset | ||
| :param zero_as_utc: | ||
| Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones | ||
| :return: | ||
| Returns :class:`dateutil.tz.tzoffset` for offsets and | ||
| :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is | ||
| specified) offsets equivalent to UTC. | ||
| """ | ||
| return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| # Constants | ||
| _DATE_SEP = b'-' | ||
| _TIME_SEP = b':' | ||
| _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') | ||
| def _parse_isodate(self, dt_str): | ||
| try: | ||
| return self._parse_isodate_common(dt_str) | ||
| except ValueError: | ||
| return self._parse_isodate_uncommon(dt_str) | ||
| def _parse_isodate_common(self, dt_str): | ||
| len_str = len(dt_str) | ||
| components = [1, 1, 1] | ||
| if len_str < 4: | ||
| raise ValueError('ISO string too short') | ||
| # Year | ||
| components[0] = int(dt_str[0:4]) | ||
| pos = 4 | ||
| if pos >= len_str: | ||
| return components, pos | ||
| has_sep = dt_str[pos:pos + 1] == self._DATE_SEP | ||
| if has_sep: | ||
| pos += 1 | ||
| # Month | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common month') | ||
| components[1] = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| if pos >= len_str: | ||
| if has_sep: | ||
| return components, pos | ||
| else: | ||
| raise ValueError('Invalid ISO format') | ||
| if has_sep: | ||
| if dt_str[pos:pos + 1] != self._DATE_SEP: | ||
| raise ValueError('Invalid separator in ISO string') | ||
| pos += 1 | ||
| # Day | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common day') | ||
| components[2] = int(dt_str[pos:pos + 2]) | ||
| return components, pos + 2 | ||
| def _parse_isodate_uncommon(self, dt_str): | ||
| if len(dt_str) < 4: | ||
| raise ValueError('ISO string too short') | ||
| # All ISO formats start with the year | ||
| year = int(dt_str[0:4]) | ||
| has_sep = dt_str[4:5] == self._DATE_SEP | ||
| pos = 4 + has_sep # Skip '-' if it's there | ||
| if dt_str[pos:pos + 1] == b'W': | ||
| # YYYY-?Www-?D? | ||
| pos += 1 | ||
| weekno = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| dayno = 1 | ||
| if len(dt_str) > pos: | ||
| if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: | ||
| raise ValueError('Inconsistent use of dash separator') | ||
| pos += has_sep | ||
| dayno = int(dt_str[pos:pos + 1]) | ||
| pos += 1 | ||
| base_date = self._calculate_weekdate(year, weekno, dayno) | ||
| else: | ||
| # YYYYDDD or YYYY-DDD | ||
| if len(dt_str) - pos < 3: | ||
| raise ValueError('Invalid ordinal day') | ||
| ordinal_day = int(dt_str[pos:pos + 3]) | ||
| pos += 3 | ||
| if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): | ||
| raise ValueError('Invalid ordinal day' + | ||
| ' {} for year {}'.format(ordinal_day, year)) | ||
| base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) | ||
| components = [base_date.year, base_date.month, base_date.day] | ||
| return components, pos | ||
| def _calculate_weekdate(self, year, week, day): | ||
| """ | ||
| Calculate the day of corresponding to the ISO year-week-day calendar. | ||
| This function is effectively the inverse of | ||
| :func:`datetime.date.isocalendar`. | ||
| :param year: | ||
| The year in the ISO calendar | ||
| :param week: | ||
| The week in the ISO calendar - range is [1, 53] | ||
| :param day: | ||
| The day in the ISO calendar - range is [1 (MON), 7 (SUN)] | ||
| :return: | ||
| Returns a :class:`datetime.date` | ||
| """ | ||
| if not 0 < week < 54: | ||
| raise ValueError('Invalid week: {}'.format(week)) | ||
| if not 0 < day < 8: # Range is 1-7 | ||
| raise ValueError('Invalid weekday: {}'.format(day)) | ||
| # Get week 1 for the specific year: | ||
| jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it | ||
| week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) | ||
| # Now add the specific number of weeks and days to get what we want | ||
| week_offset = (week - 1) * 7 + (day - 1) | ||
| return week_1 + timedelta(days=week_offset) | ||
| def _parse_isotime(self, timestr): | ||
| len_str = len(timestr) | ||
| components = [0, 0, 0, 0, None] | ||
| pos = 0 | ||
| comp = -1 | ||
| if len_str < 2: | ||
| raise ValueError('ISO time too short') | ||
| has_sep = False | ||
| while pos < len_str and comp < 5: | ||
| comp += 1 | ||
| if timestr[pos:pos + 1] in b'-+Zz': | ||
| # Detect time zone boundary | ||
| components[-1] = self._parse_tzstr(timestr[pos:]) | ||
| pos = len_str | ||
| break | ||
| if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: | ||
| has_sep = True | ||
| pos += 1 | ||
| elif comp == 2 and has_sep: | ||
| if timestr[pos:pos+1] != self._TIME_SEP: | ||
| raise ValueError('Inconsistent use of colon separator') | ||
| pos += 1 | ||
| if comp < 3: | ||
| # Hour, minute, second | ||
| components[comp] = int(timestr[pos:pos + 2]) | ||
| pos += 2 | ||
| if comp == 3: | ||
| # Fraction of a second | ||
| frac = self._FRACTION_REGEX.match(timestr[pos:]) | ||
| if not frac: | ||
| continue | ||
| us_str = frac.group(1)[:6] # Truncate to microseconds | ||
| components[comp] = int(us_str) * 10**(6 - len(us_str)) | ||
| pos += len(frac.group()) | ||
| if pos < len_str: | ||
| raise ValueError('Unused components in ISO string') | ||
| if components[0] == 24: | ||
| # Standard supports 00:00 and 24:00 as representations of midnight | ||
| if any(component != 0 for component in components[1:4]): | ||
| raise ValueError('Hour may only be 24 at 24:00:00.000') | ||
| return components | ||
| def _parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| if tzstr == b'Z' or tzstr == b'z': | ||
| return tz.UTC | ||
| if len(tzstr) not in {3, 5, 6}: | ||
| raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') | ||
| if tzstr[0:1] == b'-': | ||
| mult = -1 | ||
| elif tzstr[0:1] == b'+': | ||
| mult = 1 | ||
| else: | ||
| raise ValueError('Time zone offset requires sign') | ||
| hours = int(tzstr[1:3]) | ||
| if len(tzstr) == 3: | ||
| minutes = 0 | ||
| else: | ||
| minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) | ||
| if zero_as_utc and hours == 0 and minutes == 0: | ||
| return tz.UTC | ||
| else: | ||
| if minutes > 59: | ||
| raise ValueError('Invalid minutes in time zone offset') | ||
| if hours > 23: | ||
| raise ValueError('Invalid hours in time zone offset') | ||
| return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) | ||
| DEFAULT_ISOPARSER = isoparser() | ||
| isoparse = DEFAULT_ISOPARSER.isoparse |
| # -*- coding: utf-8 -*- | ||
| import datetime | ||
| import calendar | ||
| import operator | ||
| from math import copysign | ||
| from six import integer_types | ||
| from warnings import warn | ||
| from ._common import weekday | ||
| MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) | ||
| __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] | ||
| class relativedelta(object): | ||
| """ | ||
| The relativedelta type is designed to be applied to an existing datetime and | ||
| can replace specific components of that datetime, or represents an interval | ||
| of time. | ||
| It is based on the specification of the excellent work done by M.-A. Lemburg | ||
| in his | ||
| `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension. | ||
| However, notice that this type does *NOT* implement the same algorithm as | ||
| his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. | ||
| There are two different ways to build a relativedelta instance. The | ||
| first one is passing it two date/datetime classes:: | ||
| relativedelta(datetime1, datetime2) | ||
| The second one is passing it any number of the following keyword arguments:: | ||
| relativedelta(arg1=x,arg2=y,arg3=z...) | ||
| year, month, day, hour, minute, second, microsecond: | ||
| Absolute information (argument is singular); adding or subtracting a | ||
| relativedelta with absolute information does not perform an arithmetic | ||
| operation, but rather REPLACES the corresponding value in the | ||
| original datetime with the value(s) in relativedelta. | ||
| years, months, weeks, days, hours, minutes, seconds, microseconds: | ||
| Relative information, may be negative (argument is plural); adding | ||
| or subtracting a relativedelta with relative information performs | ||
| the corresponding arithmetic operation on the original datetime value | ||
| with the information in the relativedelta. | ||
| weekday: | ||
| One of the weekday instances (MO, TU, etc) available in the | ||
| relativedelta module. These instances may receive a parameter N, | ||
| specifying the Nth weekday, which could be positive or negative | ||
| (like MO(+1) or MO(-2)). Not specifying it is the same as specifying | ||
| +1. You can also use an integer, where 0=MO. This argument is always | ||
| relative e.g. if the calculated date is already Monday, using MO(1) | ||
| or MO(-1) won't change the day. To effectively make it absolute, use | ||
| it in combination with the day argument (e.g. day=1, MO(1) for first | ||
| Monday of the month). | ||
| leapdays: | ||
| Will add given days to the date found, if year is a leap | ||
| year, and the date found is post 28 of february. | ||
| yearday, nlyearday: | ||
| Set the yearday or the non-leap year day (jump leap days). | ||
| These are converted to day/month/leapdays information. | ||
| There are relative and absolute forms of the keyword | ||
| arguments. The plural is relative, and the singular is | ||
| absolute. For each argument in the order below, the absolute form | ||
| is applied first (by setting each attribute to that value) and | ||
| then the relative form (by adding the value to the attribute). | ||
| The order of attributes considered when this relativedelta is | ||
| added to a datetime is: | ||
| 1. Year | ||
| 2. Month | ||
| 3. Day | ||
| 4. Hours | ||
| 5. Minutes | ||
| 6. Seconds | ||
| 7. Microseconds | ||
| Finally, weekday is applied, using the rule described above. | ||
| For example | ||
| >>> from datetime import datetime | ||
| >>> from dateutil.relativedelta import relativedelta, MO | ||
| >>> dt = datetime(2018, 4, 9, 13, 37, 0) | ||
| >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) | ||
| >>> dt + delta | ||
| datetime.datetime(2018, 4, 2, 14, 37) | ||
| First, the day is set to 1 (the first of the month), then 25 hours | ||
| are added, to get to the 2nd day and 14th hour, finally the | ||
| weekday is applied, but since the 2nd is already a Monday there is | ||
| no effect. | ||
| """ | ||
| def __init__(self, dt1=None, dt2=None, | ||
| years=0, months=0, days=0, leapdays=0, weeks=0, | ||
| hours=0, minutes=0, seconds=0, microseconds=0, | ||
| year=None, month=None, day=None, weekday=None, | ||
| yearday=None, nlyearday=None, | ||
| hour=None, minute=None, second=None, microsecond=None): | ||
| if dt1 and dt2: | ||
| # datetime is a subclass of date. So both must be date | ||
| if not (isinstance(dt1, datetime.date) and | ||
| isinstance(dt2, datetime.date)): | ||
| raise TypeError("relativedelta only diffs datetime/date") | ||
| # We allow two dates, or two datetimes, so we coerce them to be | ||
| # of the same type | ||
| if (isinstance(dt1, datetime.datetime) != | ||
| isinstance(dt2, datetime.datetime)): | ||
| if not isinstance(dt1, datetime.datetime): | ||
| dt1 = datetime.datetime.fromordinal(dt1.toordinal()) | ||
| elif not isinstance(dt2, datetime.datetime): | ||
| dt2 = datetime.datetime.fromordinal(dt2.toordinal()) | ||
| self.years = 0 | ||
| self.months = 0 | ||
| self.days = 0 | ||
| self.leapdays = 0 | ||
| self.hours = 0 | ||
| self.minutes = 0 | ||
| self.seconds = 0 | ||
| self.microseconds = 0 | ||
| self.year = None | ||
| self.month = None | ||
| self.day = None | ||
| self.weekday = None | ||
| self.hour = None | ||
| self.minute = None | ||
| self.second = None | ||
| self.microsecond = None | ||
| self._has_time = 0 | ||
| # Get year / month delta between the two | ||
| months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) | ||
| self._set_months(months) | ||
| # Remove the year/month delta so the timedelta is just well-defined | ||
| # time units (seconds, days and microseconds) | ||
| dtm = self.__radd__(dt2) | ||
| # If we've overshot our target, make an adjustment | ||
| if dt1 < dt2: | ||
| compare = operator.gt | ||
| increment = 1 | ||
| else: | ||
| compare = operator.lt | ||
| increment = -1 | ||
| while compare(dt1, dtm): | ||
| months += increment | ||
| self._set_months(months) | ||
| dtm = self.__radd__(dt2) | ||
| # Get the timedelta between the "months-adjusted" date and dt1 | ||
| delta = dt1 - dtm | ||
| self.seconds = delta.seconds + delta.days * 86400 | ||
| self.microseconds = delta.microseconds | ||
| else: | ||
| # Check for non-integer values in integer-only quantities | ||
| if any(x is not None and x != int(x) for x in (years, months)): | ||
| raise ValueError("Non-integer years and months are " | ||
| "ambiguous and not currently supported.") | ||
| # Relative information | ||
| self.years = int(years) | ||
| self.months = int(months) | ||
| self.days = days + weeks * 7 | ||
| self.leapdays = leapdays | ||
| self.hours = hours | ||
| self.minutes = minutes | ||
| self.seconds = seconds | ||
| self.microseconds = microseconds | ||
| # Absolute information | ||
| self.year = year | ||
| self.month = month | ||
| self.day = day | ||
| self.hour = hour | ||
| self.minute = minute | ||
| self.second = second | ||
| self.microsecond = microsecond | ||
| if any(x is not None and int(x) != x | ||
| for x in (year, month, day, hour, | ||
| minute, second, microsecond)): | ||
| # For now we'll deprecate floats - later it'll be an error. | ||
| warn("Non-integer value passed as absolute information. " + | ||
| "This is not a well-defined condition and will raise " + | ||
| "errors in future versions.", DeprecationWarning) | ||
| if isinstance(weekday, integer_types): | ||
| self.weekday = weekdays[weekday] | ||
| else: | ||
| self.weekday = weekday | ||
| yday = 0 | ||
| if nlyearday: | ||
| yday = nlyearday | ||
| elif yearday: | ||
| yday = yearday | ||
| if yearday > 59: | ||
| self.leapdays = -1 | ||
| if yday: | ||
| ydayidx = [31, 59, 90, 120, 151, 181, 212, | ||
| 243, 273, 304, 334, 366] | ||
| for idx, ydays in enumerate(ydayidx): | ||
| if yday <= ydays: | ||
| self.month = idx+1 | ||
| if idx == 0: | ||
| self.day = yday | ||
| else: | ||
| self.day = yday-ydayidx[idx-1] | ||
| break | ||
| else: | ||
| raise ValueError("invalid year day (%d)" % yday) | ||
| self._fix() | ||
| def _fix(self): | ||
| if abs(self.microseconds) > 999999: | ||
| s = _sign(self.microseconds) | ||
| div, mod = divmod(self.microseconds * s, 1000000) | ||
| self.microseconds = mod * s | ||
| self.seconds += div * s | ||
| if abs(self.seconds) > 59: | ||
| s = _sign(self.seconds) | ||
| div, mod = divmod(self.seconds * s, 60) | ||
| self.seconds = mod * s | ||
| self.minutes += div * s | ||
| if abs(self.minutes) > 59: | ||
| s = _sign(self.minutes) | ||
| div, mod = divmod(self.minutes * s, 60) | ||
| self.minutes = mod * s | ||
| self.hours += div * s | ||
| if abs(self.hours) > 23: | ||
| s = _sign(self.hours) | ||
| div, mod = divmod(self.hours * s, 24) | ||
| self.hours = mod * s | ||
| self.days += div * s | ||
| if abs(self.months) > 11: | ||
| s = _sign(self.months) | ||
| div, mod = divmod(self.months * s, 12) | ||
| self.months = mod * s | ||
| self.years += div * s | ||
| if (self.hours or self.minutes or self.seconds or self.microseconds | ||
| or self.hour is not None or self.minute is not None or | ||
| self.second is not None or self.microsecond is not None): | ||
| self._has_time = 1 | ||
| else: | ||
| self._has_time = 0 | ||
| @property | ||
| def weeks(self): | ||
| return int(self.days / 7.0) | ||
| @weeks.setter | ||
| def weeks(self, value): | ||
| self.days = self.days - (self.weeks * 7) + value * 7 | ||
| def _set_months(self, months): | ||
| self.months = months | ||
| if abs(self.months) > 11: | ||
| s = _sign(self.months) | ||
| div, mod = divmod(self.months * s, 12) | ||
| self.months = mod * s | ||
| self.years = div * s | ||
| else: | ||
| self.years = 0 | ||
| def normalized(self): | ||
| """ | ||
| Return a version of this object represented entirely using integer | ||
| values for the relative attributes. | ||
| >>> relativedelta(days=1.5, hours=2).normalized() | ||
| relativedelta(days=+1, hours=+14) | ||
| :return: | ||
| Returns a :class:`dateutil.relativedelta.relativedelta` object. | ||
| """ | ||
| # Cascade remainders down (rounding each to roughly nearest microsecond) | ||
| days = int(self.days) | ||
| hours_f = round(self.hours + 24 * (self.days - days), 11) | ||
| hours = int(hours_f) | ||
| minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) | ||
| minutes = int(minutes_f) | ||
| seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) | ||
| seconds = int(seconds_f) | ||
| microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) | ||
| # Constructor carries overflow back up with call to _fix() | ||
| return self.__class__(years=self.years, months=self.months, | ||
| days=days, hours=hours, minutes=minutes, | ||
| seconds=seconds, microseconds=microseconds, | ||
| leapdays=self.leapdays, year=self.year, | ||
| month=self.month, day=self.day, | ||
| weekday=self.weekday, hour=self.hour, | ||
| minute=self.minute, second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __add__(self, other): | ||
| if isinstance(other, relativedelta): | ||
| return self.__class__(years=other.years + self.years, | ||
| months=other.months + self.months, | ||
| days=other.days + self.days, | ||
| hours=other.hours + self.hours, | ||
| minutes=other.minutes + self.minutes, | ||
| seconds=other.seconds + self.seconds, | ||
| microseconds=(other.microseconds + | ||
| self.microseconds), | ||
| leapdays=other.leapdays or self.leapdays, | ||
| year=(other.year if other.year is not None | ||
| else self.year), | ||
| month=(other.month if other.month is not None | ||
| else self.month), | ||
| day=(other.day if other.day is not None | ||
| else self.day), | ||
| weekday=(other.weekday if other.weekday is not None | ||
| else self.weekday), | ||
| hour=(other.hour if other.hour is not None | ||
| else self.hour), | ||
| minute=(other.minute if other.minute is not None | ||
| else self.minute), | ||
| second=(other.second if other.second is not None | ||
| else self.second), | ||
| microsecond=(other.microsecond if other.microsecond | ||
| is not None else | ||
| self.microsecond)) | ||
| if isinstance(other, datetime.timedelta): | ||
| return self.__class__(years=self.years, | ||
| months=self.months, | ||
| days=self.days + other.days, | ||
| hours=self.hours, | ||
| minutes=self.minutes, | ||
| seconds=self.seconds + other.seconds, | ||
| microseconds=self.microseconds + other.microseconds, | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| if not isinstance(other, datetime.date): | ||
| return NotImplemented | ||
| elif self._has_time and not isinstance(other, datetime.datetime): | ||
| other = datetime.datetime.fromordinal(other.toordinal()) | ||
| year = (self.year or other.year)+self.years | ||
| month = self.month or other.month | ||
| if self.months: | ||
| assert 1 <= abs(self.months) <= 12 | ||
| month += self.months | ||
| if month > 12: | ||
| year += 1 | ||
| month -= 12 | ||
| elif month < 1: | ||
| year -= 1 | ||
| month += 12 | ||
| day = min(calendar.monthrange(year, month)[1], | ||
| self.day or other.day) | ||
| repl = {"year": year, "month": month, "day": day} | ||
| for attr in ["hour", "minute", "second", "microsecond"]: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| repl[attr] = value | ||
| days = self.days | ||
| if self.leapdays and month > 2 and calendar.isleap(year): | ||
| days += self.leapdays | ||
| ret = (other.replace(**repl) | ||
| + datetime.timedelta(days=days, | ||
| hours=self.hours, | ||
| minutes=self.minutes, | ||
| seconds=self.seconds, | ||
| microseconds=self.microseconds)) | ||
| if self.weekday: | ||
| weekday, nth = self.weekday.weekday, self.weekday.n or 1 | ||
| jumpdays = (abs(nth) - 1) * 7 | ||
| if nth > 0: | ||
| jumpdays += (7 - ret.weekday() + weekday) % 7 | ||
| else: | ||
| jumpdays += (ret.weekday() - weekday) % 7 | ||
| jumpdays *= -1 | ||
| ret += datetime.timedelta(days=jumpdays) | ||
| return ret | ||
| def __radd__(self, other): | ||
| return self.__add__(other) | ||
| def __rsub__(self, other): | ||
| return self.__neg__().__radd__(other) | ||
| def __sub__(self, other): | ||
| if not isinstance(other, relativedelta): | ||
| return NotImplemented # In case the other object defines __rsub__ | ||
| return self.__class__(years=self.years - other.years, | ||
| months=self.months - other.months, | ||
| days=self.days - other.days, | ||
| hours=self.hours - other.hours, | ||
| minutes=self.minutes - other.minutes, | ||
| seconds=self.seconds - other.seconds, | ||
| microseconds=self.microseconds - other.microseconds, | ||
| leapdays=self.leapdays or other.leapdays, | ||
| year=(self.year if self.year is not None | ||
| else other.year), | ||
| month=(self.month if self.month is not None else | ||
| other.month), | ||
| day=(self.day if self.day is not None else | ||
| other.day), | ||
| weekday=(self.weekday if self.weekday is not None else | ||
| other.weekday), | ||
| hour=(self.hour if self.hour is not None else | ||
| other.hour), | ||
| minute=(self.minute if self.minute is not None else | ||
| other.minute), | ||
| second=(self.second if self.second is not None else | ||
| other.second), | ||
| microsecond=(self.microsecond if self.microsecond | ||
| is not None else | ||
| other.microsecond)) | ||
| def __abs__(self): | ||
| return self.__class__(years=abs(self.years), | ||
| months=abs(self.months), | ||
| days=abs(self.days), | ||
| hours=abs(self.hours), | ||
| minutes=abs(self.minutes), | ||
| seconds=abs(self.seconds), | ||
| microseconds=abs(self.microseconds), | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __neg__(self): | ||
| return self.__class__(years=-self.years, | ||
| months=-self.months, | ||
| days=-self.days, | ||
| hours=-self.hours, | ||
| minutes=-self.minutes, | ||
| seconds=-self.seconds, | ||
| microseconds=-self.microseconds, | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __bool__(self): | ||
| return not (not self.years and | ||
| not self.months and | ||
| not self.days and | ||
| not self.hours and | ||
| not self.minutes and | ||
| not self.seconds and | ||
| not self.microseconds and | ||
| not self.leapdays and | ||
| self.year is None and | ||
| self.month is None and | ||
| self.day is None and | ||
| self.weekday is None and | ||
| self.hour is None and | ||
| self.minute is None and | ||
| self.second is None and | ||
| self.microsecond is None) | ||
| # Compatibility with Python 2.x | ||
| __nonzero__ = __bool__ | ||
| def __mul__(self, other): | ||
| try: | ||
| f = float(other) | ||
| except TypeError: | ||
| return NotImplemented | ||
| return self.__class__(years=int(self.years * f), | ||
| months=int(self.months * f), | ||
| days=int(self.days * f), | ||
| hours=int(self.hours * f), | ||
| minutes=int(self.minutes * f), | ||
| seconds=int(self.seconds * f), | ||
| microseconds=int(self.microseconds * f), | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| __rmul__ = __mul__ | ||
| def __eq__(self, other): | ||
| if not isinstance(other, relativedelta): | ||
| return NotImplemented | ||
| if self.weekday or other.weekday: | ||
| if not self.weekday or not other.weekday: | ||
| return False | ||
| if self.weekday.weekday != other.weekday.weekday: | ||
| return False | ||
| n1, n2 = self.weekday.n, other.weekday.n | ||
| if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): | ||
| return False | ||
| return (self.years == other.years and | ||
| self.months == other.months and | ||
| self.days == other.days and | ||
| self.hours == other.hours and | ||
| self.minutes == other.minutes and | ||
| self.seconds == other.seconds and | ||
| self.microseconds == other.microseconds and | ||
| self.leapdays == other.leapdays and | ||
| self.year == other.year and | ||
| self.month == other.month and | ||
| self.day == other.day and | ||
| self.hour == other.hour and | ||
| self.minute == other.minute and | ||
| self.second == other.second and | ||
| self.microsecond == other.microsecond) | ||
| def __hash__(self): | ||
| return hash(( | ||
| self.weekday, | ||
| self.years, | ||
| self.months, | ||
| self.days, | ||
| self.hours, | ||
| self.minutes, | ||
| self.seconds, | ||
| self.microseconds, | ||
| self.leapdays, | ||
| self.year, | ||
| self.month, | ||
| self.day, | ||
| self.hour, | ||
| self.minute, | ||
| self.second, | ||
| self.microsecond, | ||
| )) | ||
| def __ne__(self, other): | ||
| return not self.__eq__(other) | ||
| def __div__(self, other): | ||
| try: | ||
| reciprocal = 1 / float(other) | ||
| except TypeError: | ||
| return NotImplemented | ||
| return self.__mul__(reciprocal) | ||
| __truediv__ = __div__ | ||
| def __repr__(self): | ||
| l = [] | ||
| for attr in ["years", "months", "days", "leapdays", | ||
| "hours", "minutes", "seconds", "microseconds"]: | ||
| value = getattr(self, attr) | ||
| if value: | ||
| l.append("{attr}={value:+g}".format(attr=attr, value=value)) | ||
| for attr in ["year", "month", "day", "weekday", | ||
| "hour", "minute", "second", "microsecond"]: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| l.append("{attr}={value}".format(attr=attr, value=repr(value))) | ||
| return "{classname}({attrs})".format(classname=self.__class__.__name__, | ||
| attrs=", ".join(l)) | ||
| def _sign(x): | ||
| return int(copysign(1, x)) | ||
| # vim:ts=4:sw=4:et |
Sorry, the diff of this file is too big to display
| from __future__ import unicode_literals | ||
| import os | ||
| import time | ||
| import subprocess | ||
| import warnings | ||
| import tempfile | ||
| import pickle | ||
| import pytest | ||
| class PicklableMixin(object): | ||
| def _get_nobj_bytes(self, obj, dump_kwargs, load_kwargs): | ||
| """ | ||
| Pickle and unpickle an object using ``pickle.dumps`` / ``pickle.loads`` | ||
| """ | ||
| pkl = pickle.dumps(obj, **dump_kwargs) | ||
| return pickle.loads(pkl, **load_kwargs) | ||
| def _get_nobj_file(self, obj, dump_kwargs, load_kwargs): | ||
| """ | ||
| Pickle and unpickle an object using ``pickle.dump`` / ``pickle.load`` on | ||
| a temporary file. | ||
| """ | ||
| with tempfile.TemporaryFile('w+b') as pkl: | ||
| pickle.dump(obj, pkl, **dump_kwargs) | ||
| pkl.seek(0) # Reset the file to the beginning to read it | ||
| nobj = pickle.load(pkl, **load_kwargs) | ||
| return nobj | ||
| def assertPicklable(self, obj, singleton=False, asfile=False, | ||
| dump_kwargs=None, load_kwargs=None): | ||
| """ | ||
| Assert that an object can be pickled and unpickled. This assertion | ||
| assumes that the desired behavior is that the unpickled object compares | ||
| equal to the original object, but is not the same object. | ||
| """ | ||
| get_nobj = self._get_nobj_file if asfile else self._get_nobj_bytes | ||
| dump_kwargs = dump_kwargs or {} | ||
| load_kwargs = load_kwargs or {} | ||
| nobj = get_nobj(obj, dump_kwargs, load_kwargs) | ||
| if not singleton: | ||
| self.assertIsNot(obj, nobj) | ||
| self.assertEqual(obj, nobj) | ||
| class TZContextBase(object): | ||
| """ | ||
| Base class for a context manager which allows changing of time zones. | ||
| Subclasses may define a guard variable to either block or or allow time | ||
| zone changes by redefining ``_guard_var_name`` and ``_guard_allows_change``. | ||
| The default is that the guard variable must be affirmatively set. | ||
| Subclasses must define ``get_current_tz`` and ``set_current_tz``. | ||
| """ | ||
| _guard_var_name = "DATEUTIL_MAY_CHANGE_TZ" | ||
| _guard_allows_change = True | ||
| def __init__(self, tzval): | ||
| self.tzval = tzval | ||
| self._old_tz = None | ||
| @classmethod | ||
| def tz_change_allowed(cls): | ||
| """ | ||
| Class method used to query whether or not this class allows time zone | ||
| changes. | ||
| """ | ||
| guard = bool(os.environ.get(cls._guard_var_name, False)) | ||
| # _guard_allows_change gives the "default" behavior - if True, the | ||
| # guard is overcoming a block. If false, the guard is causing a block. | ||
| # Whether tz_change is allowed is therefore the XNOR of the two. | ||
| return guard == cls._guard_allows_change | ||
| @classmethod | ||
| def tz_change_disallowed_message(cls): | ||
| """ Generate instructions on how to allow tz changes """ | ||
| msg = ('Changing time zone not allowed. Set {envar} to {gval} ' | ||
| 'if you would like to allow this behavior') | ||
| return msg.format(envar=cls._guard_var_name, | ||
| gval=cls._guard_allows_change) | ||
| def __enter__(self): | ||
| if not self.tz_change_allowed(): | ||
| msg = self.tz_change_disallowed_message() | ||
| pytest.skip(msg) | ||
| # If this is used outside of a test suite, we still want an error. | ||
| raise ValueError(msg) # pragma: no cover | ||
| self._old_tz = self.get_current_tz() | ||
| self.set_current_tz(self.tzval) | ||
| def __exit__(self, type, value, traceback): | ||
| if self._old_tz is not None: | ||
| self.set_current_tz(self._old_tz) | ||
| self._old_tz = None | ||
| def get_current_tz(self): | ||
| raise NotImplementedError | ||
| def set_current_tz(self): | ||
| raise NotImplementedError | ||
| class TZEnvContext(TZContextBase): | ||
| """ | ||
| Context manager that temporarily sets the `TZ` variable (for use on | ||
| *nix-like systems). Because the effect is local to the shell anyway, this | ||
| will apply *unless* a guard is set. | ||
| If you do not want the TZ environment variable set, you may set the | ||
| ``DATEUTIL_MAY_NOT_CHANGE_TZ_VAR`` variable to a truthy value. | ||
| """ | ||
| _guard_var_name = "DATEUTIL_MAY_NOT_CHANGE_TZ_VAR" | ||
| _guard_allows_change = False | ||
| def get_current_tz(self): | ||
| return os.environ.get('TZ', UnsetTz) | ||
| def set_current_tz(self, tzval): | ||
| if tzval is UnsetTz and 'TZ' in os.environ: | ||
| del os.environ['TZ'] | ||
| else: | ||
| os.environ['TZ'] = tzval | ||
| time.tzset() | ||
| class TZWinContext(TZContextBase): | ||
| """ | ||
| Context manager for changing local time zone on Windows. | ||
| Because the effect of this is system-wide and global, it may have | ||
| unintended side effect. Set the ``DATEUTIL_MAY_CHANGE_TZ`` environment | ||
| variable to a truthy value before using this context manager. | ||
| """ | ||
| def get_current_tz(self): | ||
| p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE) | ||
| ctzname, err = p.communicate() | ||
| ctzname = ctzname.decode() # Popen returns | ||
| if p.returncode: | ||
| raise OSError('Failed to get current time zone: ' + err) | ||
| return ctzname | ||
| def set_current_tz(self, tzname): | ||
| p = subprocess.Popen('tzutil /s "' + tzname + '"') | ||
| out, err = p.communicate() | ||
| if p.returncode: | ||
| raise OSError('Failed to set current time zone: ' + | ||
| (err or 'Unknown error.')) | ||
| ### | ||
| # Utility classes | ||
| class NotAValueClass(object): | ||
| """ | ||
| A class analogous to NaN that has operations defined for any type. | ||
| """ | ||
| def _op(self, other): | ||
| return self # Operation with NotAValue returns NotAValue | ||
| def _cmp(self, other): | ||
| return False | ||
| __add__ = __radd__ = _op | ||
| __sub__ = __rsub__ = _op | ||
| __mul__ = __rmul__ = _op | ||
| __div__ = __rdiv__ = _op | ||
| __truediv__ = __rtruediv__ = _op | ||
| __floordiv__ = __rfloordiv__ = _op | ||
| __lt__ = __rlt__ = _op | ||
| __gt__ = __rgt__ = _op | ||
| __eq__ = __req__ = _op | ||
| __le__ = __rle__ = _op | ||
| __ge__ = __rge__ = _op | ||
| NotAValue = NotAValueClass() | ||
| class ComparesEqualClass(object): | ||
| """ | ||
| A class that is always equal to whatever you compare it to. | ||
| """ | ||
| def __eq__(self, other): | ||
| return True | ||
| def __ne__(self, other): | ||
| return False | ||
| def __le__(self, other): | ||
| return True | ||
| def __ge__(self, other): | ||
| return True | ||
| def __lt__(self, other): | ||
| return False | ||
| def __gt__(self, other): | ||
| return False | ||
| __req__ = __eq__ | ||
| __rne__ = __ne__ | ||
| __rle__ = __le__ | ||
| __rge__ = __ge__ | ||
| __rlt__ = __lt__ | ||
| __rgt__ = __gt__ | ||
| ComparesEqual = ComparesEqualClass() | ||
| class UnsetTzClass(object): | ||
| """ Sentinel class for unset time zone variable """ | ||
| pass | ||
| UnsetTz = UnsetTzClass() |
| import os | ||
| import pytest | ||
| # Configure pytest to ignore xfailing tests | ||
| # See: https://stackoverflow.com/a/53198349/467366 | ||
| def pytest_collection_modifyitems(items): | ||
| for item in items: | ||
| marker_getter = getattr(item, 'get_closest_marker', None) | ||
| # Python 3.3 support | ||
| if marker_getter is None: | ||
| marker_getter = item.get_marker | ||
| marker = marker_getter('xfail') | ||
| # Need to query the args because conditional xfail tests still have | ||
| # the xfail mark even if they are not expected to fail | ||
| if marker and (not marker.args or marker.args[0]): | ||
| item.add_marker(pytest.mark.no_cover) | ||
| def set_tzpath(): | ||
| """ | ||
| Sets the TZPATH variable if it's specified in an environment variable. | ||
| """ | ||
| tzpath = os.environ.get('DATEUTIL_TZPATH', None) | ||
| if tzpath is None: | ||
| return | ||
| path_components = tzpath.split(':') | ||
| print("Setting TZPATH to {}".format(path_components)) | ||
| from dateutil import tz | ||
| tz.TZPATHS.clear() | ||
| tz.TZPATHS.extend(path_components) | ||
| set_tzpath() |
| from hypothesis import given, assume | ||
| from hypothesis import strategies as st | ||
| from dateutil import tz | ||
| from dateutil.parser import isoparse | ||
| import pytest | ||
| # Strategies | ||
| TIME_ZONE_STRATEGY = st.sampled_from([None, tz.UTC] + | ||
| [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific', | ||
| 'Australia/Sydney', 'Europe/London')]) | ||
| ASCII_STRATEGY = st.characters(max_codepoint=127) | ||
| @pytest.mark.isoparser | ||
| @given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY) | ||
| def test_timespec_auto(dt, sep): | ||
| if dt.tzinfo is not None: | ||
| # Assume offset has no sub-second components | ||
| assume(dt.utcoffset().total_seconds() % 60 == 0) | ||
| sep = str(sep) # Python 2.7 requires bytes | ||
| dtstr = dt.isoformat(sep=sep) | ||
| dt_rt = isoparse(dtstr) | ||
| assert dt_rt == dt |
| 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 |
| from datetime import datetime, timedelta | ||
| import pytest | ||
| import six | ||
| from hypothesis import assume, given | ||
| from hypothesis import strategies as st | ||
| from dateutil import tz as tz | ||
| EPOCHALYPSE = datetime.fromtimestamp(2147483647) | ||
| NEGATIVE_EPOCHALYPSE = datetime.fromtimestamp(0) - timedelta(seconds=2147483648) | ||
| @pytest.mark.gettz | ||
| @pytest.mark.parametrize("gettz_arg", [None, ""]) | ||
| # TODO: Remove bounds when GH #590 is resolved | ||
| @given( | ||
| dt=st.datetimes( | ||
| min_value=NEGATIVE_EPOCHALYPSE, max_value=EPOCHALYPSE, timezones=st.just(tz.UTC), | ||
| ) | ||
| ) | ||
| def test_gettz_returns_local(gettz_arg, dt): | ||
| act_tz = tz.gettz(gettz_arg) | ||
| if isinstance(act_tz, tz.tzlocal): | ||
| return | ||
| dt_act = dt.astimezone(tz.gettz(gettz_arg)) | ||
| if six.PY2: | ||
| dt_exp = dt.astimezone(tz.tzlocal()) | ||
| else: | ||
| dt_exp = dt.astimezone() | ||
| assert dt_act == dt_exp | ||
| assert dt_act.tzname() == dt_exp.tzname() | ||
| assert dt_act.utcoffset() == dt_exp.utcoffset() |
| from dateutil.easter import easter | ||
| from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN | ||
| from datetime import date | ||
| import pytest | ||
| # List of easters between 1990 and 2050 | ||
| western_easter_dates = [ | ||
| date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11), | ||
| date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30), | ||
| date(1998, 4, 12), date(1999, 4, 4), | ||
| date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20), | ||
| date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8), | ||
| date(2008, 3, 23), date(2009, 4, 12), | ||
| date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31), | ||
| date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16), | ||
| date(2018, 4, 1), date(2019, 4, 21), | ||
| date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9), | ||
| date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28), | ||
| date(2028, 4, 16), date(2029, 4, 1), | ||
| date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17), | ||
| date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5), | ||
| date(2038, 4, 25), date(2039, 4, 10), | ||
| date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29), | ||
| date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14), | ||
| date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10) | ||
| ] | ||
| orthodox_easter_dates = [ | ||
| date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18), | ||
| date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27), | ||
| date(1998, 4, 19), date(1999, 4, 11), | ||
| date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27), | ||
| date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8), | ||
| date(2008, 4, 27), date(2009, 4, 19), | ||
| date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5), | ||
| date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16), | ||
| date(2018, 4, 8), date(2019, 4, 28), | ||
| date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16), | ||
| date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2), | ||
| date(2028, 4, 16), date(2029, 4, 8), | ||
| date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24), | ||
| date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5), | ||
| date(2038, 4, 25), date(2039, 4, 17), | ||
| date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3), | ||
| date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21), | ||
| date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17) | ||
| ] | ||
| # A random smattering of Julian dates. | ||
| # Pulled values from http://www.kevinlaughery.com/east4099.html | ||
| julian_easter_dates = [ | ||
| date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31), | ||
| date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19), | ||
| date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8), | ||
| date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14), | ||
| date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6), | ||
| date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30), | ||
| date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24), | ||
| date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17), | ||
| date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3), | ||
| date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14) | ||
| ] | ||
| @pytest.mark.parametrize("easter_date", western_easter_dates) | ||
| def test_easter_western(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_WESTERN) | ||
| @pytest.mark.parametrize("easter_date", orthodox_easter_dates) | ||
| def test_easter_orthodox(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_ORTHODOX) | ||
| @pytest.mark.parametrize("easter_date", julian_easter_dates) | ||
| def test_easter_julian(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_JULIAN) | ||
| def test_easter_bad_method(): | ||
| with pytest.raises(ValueError): | ||
| easter(1975, 4) |
| """Test for the "import *" functionality. | ||
| As import * can be only done at module level, it has been added in a separate file | ||
| """ | ||
| import pytest | ||
| prev_locals = list(locals()) | ||
| from dateutil import * | ||
| new_locals = {name:value for name,value in locals().items() | ||
| if name not in prev_locals} | ||
| new_locals.pop('prev_locals') | ||
| @pytest.mark.import_star | ||
| def test_imported_modules(): | ||
| """ Test that `from dateutil import *` adds modules in __all__ locally """ | ||
| import dateutil.easter | ||
| import dateutil.parser | ||
| import dateutil.relativedelta | ||
| import dateutil.rrule | ||
| import dateutil.tz | ||
| import dateutil.utils | ||
| import dateutil.zoneinfo | ||
| assert dateutil.easter == new_locals.pop("easter") | ||
| assert dateutil.parser == new_locals.pop("parser") | ||
| assert dateutil.relativedelta == new_locals.pop("relativedelta") | ||
| assert dateutil.rrule == new_locals.pop("rrule") | ||
| assert dateutil.tz == new_locals.pop("tz") | ||
| assert dateutil.utils == new_locals.pop("utils") | ||
| assert dateutil.zoneinfo == new_locals.pop("zoneinfo") | ||
| assert not new_locals |
| import sys | ||
| import pytest | ||
| HOST_IS_WINDOWS = sys.platform.startswith('win') | ||
| def test_import_version_str(): | ||
| """ Test that dateutil.__version__ can be imported""" | ||
| from dateutil import __version__ | ||
| def test_import_version_root(): | ||
| import dateutil | ||
| assert hasattr(dateutil, '__version__') | ||
| # Test that dateutil.easter-related imports work properly | ||
| def test_import_easter_direct(): | ||
| import dateutil.easter | ||
| def test_import_easter_from(): | ||
| from dateutil import easter | ||
| def test_import_easter_start(): | ||
| from dateutil.easter import easter | ||
| # Test that dateutil.parser-related imports work properly | ||
| def test_import_parser_direct(): | ||
| import dateutil.parser | ||
| def test_import_parser_from(): | ||
| from dateutil import parser | ||
| def test_import_parser_all(): | ||
| # All interface | ||
| from dateutil.parser import parse | ||
| from dateutil.parser import parserinfo | ||
| # Other public classes | ||
| from dateutil.parser import parser | ||
| for var in (parse, parserinfo, parser): | ||
| assert var is not None | ||
| # Test that dateutil.relativedelta-related imports work properly | ||
| def test_import_relative_delta_direct(): | ||
| import dateutil.relativedelta | ||
| def test_import_relative_delta_from(): | ||
| from dateutil import relativedelta | ||
| def test_import_relative_delta_all(): | ||
| from dateutil.relativedelta import relativedelta | ||
| from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU | ||
| for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): | ||
| assert var is not None | ||
| # In the public interface but not in all | ||
| from dateutil.relativedelta import weekday | ||
| assert weekday is not None | ||
| # Test that dateutil.rrule related imports work properly | ||
| def test_import_rrule_direct(): | ||
| import dateutil.rrule | ||
| def test_import_rrule_from(): | ||
| from dateutil import rrule | ||
| def test_import_rrule_all(): | ||
| from dateutil.rrule import rrule | ||
| from dateutil.rrule import rruleset | ||
| from dateutil.rrule import rrulestr | ||
| from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY | ||
| from dateutil.rrule import HOURLY, MINUTELY, SECONDLY | ||
| from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU | ||
| rr_all = (rrule, rruleset, rrulestr, | ||
| YEARLY, MONTHLY, WEEKLY, DAILY, | ||
| HOURLY, MINUTELY, SECONDLY, | ||
| MO, TU, WE, TH, FR, SA, SU) | ||
| for var in rr_all: | ||
| assert var is not None | ||
| # In the public interface but not in all | ||
| from dateutil.rrule import weekday | ||
| assert weekday is not None | ||
| # Test that dateutil.tz related imports work properly | ||
| def test_import_tztest_direct(): | ||
| import dateutil.tz | ||
| def test_import_tz_from(): | ||
| from dateutil import tz | ||
| def test_import_tz_all(): | ||
| from dateutil.tz import tzutc | ||
| from dateutil.tz import tzoffset | ||
| from dateutil.tz import tzlocal | ||
| from dateutil.tz import tzfile | ||
| from dateutil.tz import tzrange | ||
| from dateutil.tz import tzstr | ||
| from dateutil.tz import tzical | ||
| from dateutil.tz import gettz | ||
| from dateutil.tz import tzwin | ||
| from dateutil.tz import tzwinlocal | ||
| from dateutil.tz import UTC | ||
| from dateutil.tz import datetime_ambiguous | ||
| from dateutil.tz import datetime_exists | ||
| from dateutil.tz import resolve_imaginary | ||
| tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "gettz", "datetime_ambiguous", | ||
| "datetime_exists", "resolve_imaginary", "UTC"] | ||
| tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] | ||
| lvars = locals() | ||
| for var in tz_all: | ||
| assert lvars[var] is not None | ||
| # Test that dateutil.tzwin related imports work properly | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_direct(): | ||
| import dateutil.tzwin | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_from(): | ||
| from dateutil import tzwin | ||
| @pytest.mark.skipif(not HOST_IS_WINDOWS, reason="Requires Windows") | ||
| def test_import_tz_windows_star(): | ||
| from dateutil.tzwin import tzwin | ||
| from dateutil.tzwin import tzwinlocal | ||
| tzwin_all = [tzwin, tzwinlocal] | ||
| for var in tzwin_all: | ||
| assert var is not None | ||
| # Test imports of Zone Info | ||
| def test_import_zone_info_direct(): | ||
| import dateutil.zoneinfo | ||
| def test_import_zone_info_from(): | ||
| from dateutil import zoneinfo | ||
| def test_import_zone_info_star(): | ||
| from dateutil.zoneinfo import gettz | ||
| from dateutil.zoneinfo import gettz_db_metadata | ||
| from dateutil.zoneinfo import rebuild | ||
| zi_all = (gettz, gettz_db_metadata, rebuild) | ||
| for var in zi_all: | ||
| assert var is not None |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| Tests for implementation details, not necessarily part of the user-facing | ||
| API. | ||
| The motivating case for these tests is #483, where we want to smoke-test | ||
| code that may be difficult to reach through the standard API calls. | ||
| """ | ||
| import sys | ||
| import pytest | ||
| from dateutil.parser._parser import _ymd | ||
| from dateutil import tz | ||
| IS_PY32 = sys.version_info[0:2] == (3, 2) | ||
| @pytest.mark.smoke | ||
| def test_YMD_could_be_day(): | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(2, 'M') | ||
| assert ymd.has_month | ||
| assert not ymd.has_year | ||
| assert ymd.could_be_day(4) | ||
| assert not ymd.could_be_day(-6) | ||
| assert not ymd.could_be_day(32) | ||
| # Assumes leap year | ||
| assert ymd.could_be_day(29) | ||
| ymd.append(1999) | ||
| assert ymd.has_year | ||
| assert not ymd.could_be_day(29) | ||
| ymd.append(16, 'D') | ||
| assert ymd.has_day | ||
| assert not ymd.could_be_day(1) | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(1999) | ||
| assert ymd.could_be_day(31) | ||
| ### | ||
| # Test that private interfaces in _parser are deprecated properly | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_private_warns(): | ||
| from dateutil.parser import _timelex, _tzparser | ||
| from dateutil.parser import _parsetz | ||
| with pytest.warns(DeprecationWarning): | ||
| _tzparser() | ||
| with pytest.warns(DeprecationWarning): | ||
| _timelex('2014-03-03') | ||
| with pytest.warns(DeprecationWarning): | ||
| _parsetz('+05:00') | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_parser_private_not_warns(): | ||
| from dateutil.parser._parser import _timelex, _tzparser | ||
| from dateutil.parser._parser import _parsetz | ||
| with pytest.warns(None) as recorder: | ||
| _tzparser() | ||
| assert len(recorder) == 0 | ||
| with pytest.warns(None) as recorder: | ||
| _timelex('2014-03-03') | ||
| assert len(recorder) == 0 | ||
| with pytest.warns(None) as recorder: | ||
| _parsetz('+05:00') | ||
| assert len(recorder) == 0 | ||
| @pytest.mark.tzstr | ||
| def test_tzstr_internal_timedeltas(): | ||
| with pytest.warns(tz.DeprecatedTzFormatWarning): | ||
| tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200") | ||
| with pytest.warns(tz.DeprecatedTzFormatWarning): | ||
| tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200") | ||
| assert tz1._start_delta != tz2._start_delta | ||
| assert tz1._end_delta != tz2._end_delta |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, timedelta, date, time | ||
| import itertools as it | ||
| from dateutil import tz | ||
| from dateutil.tz import UTC | ||
| from dateutil.parser import isoparser, isoparse | ||
| import pytest | ||
| import six | ||
| def _generate_tzoffsets(limited): | ||
| def _mkoffset(hmtuple, fmt): | ||
| h, m = hmtuple | ||
| m_td = (-1 if h < 0 else 1) * m | ||
| tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td)) | ||
| return tzo, fmt.format(h, m) | ||
| out = [] | ||
| if not limited: | ||
| # The subset that's just hours | ||
| hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)] | ||
| out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h]) | ||
| # Ones that have hours and minutes | ||
| hm_out = [] + hm_out_h | ||
| hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)] | ||
| else: | ||
| hm_out = [(-5, -0)] | ||
| fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}'] | ||
| out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts] | ||
| # Also add in UTC and naive | ||
| out.append((UTC, 'Z')) | ||
| out.append((None, '')) | ||
| return out | ||
| FULL_TZOFFSETS = _generate_tzoffsets(False) | ||
| FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]] | ||
| TZOFFSETS = _generate_tzoffsets(True) | ||
| DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_only(dt): | ||
| dtstr = dt.strftime('%Y') | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_month(dt): | ||
| fmt = '%Y-%m' | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)] | ||
| YMD_FMTS = ('%Y%m%d', '%Y-%m-%d') | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| @pytest.mark.parametrize('fmt', YMD_FMTS) | ||
| def test_year_month_day(dt, fmt): | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, | ||
| microsecond_precision=None): | ||
| tzi, offset_str = tzoffset | ||
| fmt = date_fmt + 'T' + time_fmt | ||
| dt = dt.replace(tzinfo=tzi) | ||
| dtstr = dt.strftime(fmt) | ||
| if microsecond_precision is not None: | ||
| if not fmt.endswith('%f'): # pragma: nocover | ||
| raise ValueError('Time format has no microseconds!') | ||
| if microsecond_precision != 6: | ||
| dtstr = dtstr[:-(6 - microsecond_precision)] | ||
| elif microsecond_precision > 6: # pragma: nocover | ||
| raise ValueError('Precision must be 1-6') | ||
| dtstr += offset_str | ||
| assert isoparse(dtstr) == dt | ||
| DATETIMES = [datetime(1998, 4, 16, 12), | ||
| datetime(2019, 11, 18, 23), | ||
| datetime(2014, 12, 16, 4)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_h(dt, date_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset) | ||
| DATETIMES = [datetime(2012, 1, 6, 9, 37)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2003, 9, 2, 22, 14, 2), | ||
| datetime(2003, 8, 8, 14, 9, 14), | ||
| datetime(2003, 4, 7, 6, 14, 59)] | ||
| HMS_FMTS = ('%H%M%S', '%H:%M:%S') | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', HMS_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS | ||
| for sep in '.,')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| @pytest.mark.parametrize('precision', list(range(3, 7))) | ||
| def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision): | ||
| # Truncate the microseconds to the desired precision for the representation | ||
| dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6))) | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision) | ||
| ### | ||
| # Truncation of extra digits beyond microsecond precision | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2018-07-03T14:07:00.123456000001', | ||
| '2018-07-03T14:07:00.123456999999', | ||
| ]) | ||
| def test_extra_subsecond_digits(dt_str): | ||
| assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456) | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_full_tzoffsets(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2014-04-11T00', | ||
| '2014-04-10T24', | ||
| '2014-04-11T00:00', | ||
| '2014-04-10T24:00', | ||
| '2014-04-11T00:00:00', | ||
| '2014-04-10T24:00:00', | ||
| '2014-04-11T00:00:00.000', | ||
| '2014-04-10T24:00:00.000', | ||
| '2014-04-11T00:00:00.000000', | ||
| '2014-04-10T24:00:00.000000'] | ||
| ) | ||
| def test_datetime_midnight(dt_str): | ||
| assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0) | ||
| @pytest.mark.parametrize('datestr', [ | ||
| '2014-01-01', | ||
| '20140101', | ||
| ]) | ||
| @pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-']) | ||
| def test_isoparse_sep_none(datestr, sep): | ||
| isostr = datestr + sep + '14:33:09' | ||
| assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9) | ||
| ## | ||
| # Uncommon date formats | ||
| TIME_ARGS = ('time_args', | ||
| ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz) | ||
| for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)], | ||
| TZOFFSETS))) | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2017, 10), datetime(2017, 3, 6)), | ||
| ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year | ||
| ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014 | ||
| ]) | ||
| def test_isoweek(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2016, 13, 7), datetime(2016, 4, 3)), | ||
| ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year | ||
| ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year | ||
| ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year | ||
| ]) | ||
| def test_isoweek_day(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isoord,dt_expected', [ | ||
| ((2004, 1), datetime(2004, 1, 1)), | ||
| ((2016, 60), datetime(2016, 2, 29)), | ||
| ((2017, 60), datetime(2017, 3, 1)), | ||
| ((2016, 366), datetime(2016, 12, 31)), | ||
| ((2017, 365), datetime(2017, 12, 31)) | ||
| ]) | ||
| def test_iso_ordinal(isoord, dt_expected): | ||
| for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'): | ||
| dtstr = fmt.format(*isoord) | ||
| assert isoparse(dtstr) == dt_expected | ||
| ### | ||
| # Acceptance of bytes | ||
| @pytest.mark.parametrize('isostr,dt', [ | ||
| (b'2014', datetime(2014, 1, 1)), | ||
| (b'20140204', datetime(2014, 2, 4)), | ||
| (b'2014-02-04', datetime(2014, 2, 4)), | ||
| (b'2014-02-04T12', datetime(2014, 2, 4, 12)), | ||
| (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)), | ||
| (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)), | ||
| (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| UTC)), | ||
| (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| UTC)), | ||
| (b'2014-02-04T12:30:15.224+05:00', | ||
| datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| tzinfo=tz.tzoffset(None, timedelta(hours=5))))]) | ||
| def test_bytes(isostr, dt): | ||
| assert isoparse(isostr) == dt | ||
| ### | ||
| # Invalid ISO strings | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('201', ValueError), # ISO string too short | ||
| ('2012-0425', ValueError), # Inconsistent date separators | ||
| ('201204-25', ValueError), # Inconsistent date separators | ||
| ('20120425T0120:00', ValueError), # Inconsistent time separators | ||
| ('20120425T01:2000', ValueError), # Inconsistent time separators | ||
| ('14:3015', ValueError), # Inconsistent time separator | ||
| ('20120425T012500-334', ValueError), # Wrong microsecond separator | ||
| ('2001-1', ValueError), # YYYY-M not valid | ||
| ('2012-04-9', ValueError), # YYYY-MM-D not valid | ||
| ('201204', ValueError), # YYYYMM not valid | ||
| ('20120411T03:30+', ValueError), # Time zone too short | ||
| ('20120411T03:30+1234567', ValueError), # Time zone too long | ||
| ('20120411T03:30-25:40', ValueError), # Time zone invalid | ||
| ('2012-1a', ValueError), # Invalid month | ||
| ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes | ||
| ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes | ||
| ('20120411T033030.123456012:00', # No sign in time zone | ||
| ValueError), | ||
| ('2012-W00', ValueError), # Invalid ISO week | ||
| ('2012-W55', ValueError), # Invalid ISO week | ||
| ('2012-W01-0', ValueError), # Invalid ISO week day | ||
| ('2012-W01-8', ValueError), # Invalid ISO week day | ||
| ('2013-000', ValueError), # Invalid ordinal day | ||
| ('2013-366', ValueError), # Invalid ordinal day | ||
| ('2013366', ValueError), # Invalid ordinal day | ||
| ('2014-03-12Т12:30:14', ValueError), # Cyrillic T | ||
| ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight | ||
| ('2014_W01-1', ValueError), # Invalid separator | ||
| ('2014W01-1', ValueError), # Inconsistent use of dashes | ||
| ('2014-W011', ValueError), # Inconsistent use of dashes | ||
| ]) | ||
| def test_iso_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparse(isostr) | ||
| @pytest.mark.parametrize('sep_act, valid_sep, exception', [ | ||
| ('T', 'C', ValueError), | ||
| ('C', 'T', ValueError), | ||
| ]) | ||
| def test_iso_with_sep_raises(sep_act, valid_sep, exception): | ||
| parser = isoparser(sep=valid_sep) | ||
| isostr = '2012-04-25' + sep_act + '01:25:00' | ||
| with pytest.raises(exception): | ||
| parser.isoparse(isostr) | ||
| ### | ||
| # Test ISOParser constructor | ||
| @pytest.mark.parametrize('sep', [' ', '9', '🍛']) | ||
| def test_isoparser_invalid_sep(sep): | ||
| with pytest.raises(ValueError): | ||
| isoparser(sep=sep) | ||
| # This only fails on Python 3 | ||
| @pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only") | ||
| def test_isoparser_byte_sep(): | ||
| dt = datetime(2017, 12, 6, 12, 30, 45) | ||
| dt_str = dt.isoformat(sep=str('T')) | ||
| dt_rt = isoparser(sep=b'T').isoparse(dt_str) | ||
| assert dt == dt_rt | ||
| ### | ||
| # Test parse_tzstr | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_parse_tzstr(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('tzstr', [ | ||
| '-00:00', '+00:00', '+00', '-00', '+0000', '-0000' | ||
| ]) | ||
| @pytest.mark.parametrize('zero_as_utc', [True, False]) | ||
| def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc): | ||
| tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| assert tzi == UTC | ||
| assert (type(tzi) == tz.tzutc) == zero_as_utc | ||
| @pytest.mark.parametrize('tzstr,exception', [ | ||
| ('00:00', ValueError), # No sign | ||
| ('05:00', ValueError), # No sign | ||
| ('_00:00', ValueError), # Invalid sign | ||
| ('+25:00', ValueError), # Offset too large | ||
| ('00:0000', ValueError), # String too long | ||
| ]) | ||
| def test_parse_tzstr_fails(tzstr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_tzstr(tzstr) | ||
| ### | ||
| # Test parse_isodate | ||
| def __make_date_examples(): | ||
| dates_no_day = [ | ||
| date(1999, 12, 1), | ||
| date(2016, 2, 1) | ||
| ] | ||
| if not six.PY2: | ||
| # strftime does not support dates before 1900 in Python 2 | ||
| dates_no_day.append(date(1000, 11, 1)) | ||
| # Only one supported format for dates with no day | ||
| o = zip(dates_no_day, it.repeat('%Y-%m')) | ||
| dates_w_day = [ | ||
| date(1969, 12, 31), | ||
| date(1900, 1, 1), | ||
| date(2016, 2, 29), | ||
| date(2017, 11, 14) | ||
| ] | ||
| dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d') | ||
| o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts)) | ||
| return list(o) | ||
| @pytest.mark.parametrize('d,dt_fmt', __make_date_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_parse_isodate(d, dt_fmt, as_bytes): | ||
| d_str = d.strftime(dt_fmt) | ||
| if isinstance(d_str, six.text_type) and as_bytes: | ||
| d_str = d_str.encode('ascii') | ||
| elif isinstance(d_str, bytes) and not as_bytes: | ||
| d_str = d_str.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isodate(d_str) == d | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('243', ValueError), # ISO string too short | ||
| ('2014-0423', ValueError), # Inconsistent date separators | ||
| ('201404-23', ValueError), # Inconsistent date separators | ||
| ('2014日03月14', ValueError), # Not ASCII | ||
| ('2013-02-29', ValueError), # Not a leap year | ||
| ('2014/12/03', ValueError), # Wrong separators | ||
| ('2014-04-19T', ValueError), # Unknown components | ||
| ('201202', ValueError), # Invalid format | ||
| ]) | ||
| def test_isodate_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_isodate(isostr) | ||
| def test_parse_isodate_error_text(): | ||
| with pytest.raises(ValueError) as excinfo: | ||
| isoparser().parse_isodate('2014-0423') | ||
| # ensure the error message does not contain b' prefixes | ||
| if six.PY2: | ||
| expected_error = "String contains unknown ISO components: u'2014-0423'" | ||
| else: | ||
| expected_error = "String contains unknown ISO components: '2014-0423'" | ||
| assert expected_error == str(excinfo.value) | ||
| ### | ||
| # Test parse_isotime | ||
| def __make_time_examples(): | ||
| outputs = [] | ||
| # HH | ||
| time_h = [time(0), time(8), time(22)] | ||
| time_h_fmts = ['%H'] | ||
| outputs.append(it.product(time_h, time_h_fmts)) | ||
| # HHMM / HH:MM | ||
| time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)] | ||
| time_hm_fmts = ['%H%M', '%H:%M'] | ||
| outputs.append(it.product(time_hm, time_hm_fmts)) | ||
| # HHMMSS / HH:MM:SS | ||
| time_hms = [time(0, 0, 0), time(0, 15, 30), | ||
| time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)] | ||
| time_hms_fmts = ['%H%M%S', '%H:%M:%S'] | ||
| outputs.append(it.product(time_hms, time_hms_fmts)) | ||
| # HHMMSS.ffffff / HH:MM:SS.ffffff | ||
| time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993), | ||
| time(14, 21, 59, 948730), | ||
| time(23, 59, 59, 999999)] | ||
| time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f'] | ||
| outputs.append(it.product(time_hmsu, time_hmsu_fmts)) | ||
| outputs = list(map(list, outputs)) | ||
| # Time zones | ||
| ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs)) | ||
| o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr)) | ||
| o = ((t.replace(tzinfo=tzi), fmt + off_str) | ||
| for (t, fmt), (tzi, off_str) in o) | ||
| outputs.append(o) | ||
| return list(it.chain.from_iterable(outputs)) | ||
| @pytest.mark.parametrize('time_val,time_fmt', __make_time_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_isotime(time_val, time_fmt, as_bytes): | ||
| tstr = time_val.strftime(time_fmt) | ||
| if isinstance(tstr, six.text_type) and as_bytes: | ||
| tstr = tstr.encode('ascii') | ||
| elif isinstance(tstr, bytes) and not as_bytes: | ||
| tstr = tstr.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(tstr) == time_val | ||
| @pytest.mark.parametrize('isostr', [ | ||
| '24:00', | ||
| '2400', | ||
| '24:00:00', | ||
| '240000', | ||
| '24:00:00.000', | ||
| '24:00:00,000', | ||
| '24:00:00.000000', | ||
| '24:00:00,000000', | ||
| ]) | ||
| def test_isotime_midnight(isostr): | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0) | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('3', ValueError), # ISO string too short | ||
| ('14時30分15秒', ValueError), # Not ASCII | ||
| ('14_30_15', ValueError), # Invalid separators | ||
| ('1430:15', ValueError), # Inconsistent separator use | ||
| ('25', ValueError), # Invalid hours | ||
| ('25:15', ValueError), # Invalid hours | ||
| ('14:60', ValueError), # Invalid minutes | ||
| ('14:59:61', ValueError), # Invalid seconds | ||
| ('14:30:15.34468305:00', ValueError), # No sign in time zone | ||
| ('14:30:15+', ValueError), # Time zone too short | ||
| ('14:30:15+1234567', ValueError), # Time zone invalid | ||
| ('14:59:59+25:00', ValueError), # Invalid tz hours | ||
| ('14:59:59+12:62', ValueError), # Invalid tz minutes | ||
| ('14:59:30_344583', ValueError), # Invalid microsecond separator | ||
| ('24:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.001', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.000001', ValueError), # 24 used for non-midnight time | ||
| ]) | ||
| def test_isotime_raises(isostr, exception): | ||
| iparser = isoparser() | ||
| with pytest.raises(exception): | ||
| iparser.parse_isotime(isostr) |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| import itertools | ||
| from datetime import datetime, timedelta | ||
| import unittest | ||
| import sys | ||
| from dateutil import tz | ||
| from dateutil.tz import tzoffset | ||
| from dateutil.parser import parse, parserinfo | ||
| from dateutil.parser import ParserError | ||
| from dateutil.parser import UnknownTimezoneWarning | ||
| from ._common import TZEnvContext | ||
| from six import assertRaisesRegex, PY2 | ||
| from io import StringIO | ||
| import pytest | ||
| # Platform info | ||
| IS_WIN = sys.platform.startswith('win') | ||
| PLATFORM_HAS_DASH_D = False | ||
| try: | ||
| if datetime.now().strftime('%-d'): | ||
| PLATFORM_HAS_DASH_D = True | ||
| except ValueError: | ||
| pass | ||
| @pytest.fixture(params=[True, False]) | ||
| def fuzzy(request): | ||
| """Fixture to pass fuzzy=True or fuzzy=False to parse""" | ||
| return request.param | ||
| # Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"), | ||
| ("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"), | ||
| ("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"), | ||
| ("2003-09-25", datetime(2003, 9, 25), "iso format strip"), | ||
| ("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"), | ||
| ("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"), | ||
| ("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"), | ||
| ("20030925", datetime(2003, 9, 25), "iso stripped format strip"), | ||
| ("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"), | ||
| ("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"), | ||
| ("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"), | ||
| ("09-25-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("25-09-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("10-09-2003", datetime(2003, 10, 9), "date with dash"), | ||
| ("10-09-03", datetime(2003, 10, 9), "date with dash"), | ||
| ("2003.09.25", datetime(2003, 9, 25), "date with dot"), | ||
| ("09.25.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("25.09.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("10.09.2003", datetime(2003, 10, 9), "date with dot"), | ||
| ("10.09.03", datetime(2003, 10, 9), "date with dot"), | ||
| ("2003/09/25", datetime(2003, 9, 25), "date with slash"), | ||
| ("09/25/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("25/09/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("10/09/2003", datetime(2003, 10, 9), "date with slash"), | ||
| ("10/09/03", datetime(2003, 10, 9), "date with slash"), | ||
| ("2003 09 25", datetime(2003, 9, 25), "date with space"), | ||
| ("09 25 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("25 09 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("10 09 2003", datetime(2003, 10, 9), "date with space"), | ||
| ("10 09 03", datetime(2003, 10, 9), "date with space"), | ||
| ("25 09 03", datetime(2003, 9, 25), "date with space"), | ||
| ("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"), | ||
| ("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"), | ||
| (" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"), | ||
| ("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"), | ||
| ("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"), | ||
| ("July 4, 1976", datetime(1976, 7, 4), "random format"), | ||
| ("7 4 1976", datetime(1976, 7, 4), "random format"), | ||
| ("4 jul 1976", datetime(1976, 7, 4), "random format"), | ||
| ("4 Jul 1976", datetime(1976, 7, 4), "'%-d %b %Y' format"), | ||
| ("7-4-76", datetime(1976, 7, 4), "random format"), | ||
| ("19760704", datetime(1976, 7, 4), "random format"), | ||
| ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"), | ||
| ("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"), | ||
| ("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"), | ||
| ("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"), | ||
| ("3rd of May 2001", datetime(2001, 5, 3), "random format"), | ||
| ("5th of March 2001", datetime(2001, 3, 5), "random format"), | ||
| ("1st of May 2003", datetime(2003, 5, 1), "random format"), | ||
| ('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"), | ||
| ('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"), | ||
| ("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"), | ||
| ('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"), | ||
| ('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"), | ||
| ('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)"), | ||
| # Cases with legacy h/m/s format, candidates for deprecation (GH#886) | ||
| ("2016-12-21 04.2h", datetime(2016, 12, 21, 4, 12), "Fractional Hours"), | ||
| ] | ||
| # Check that we don't have any duplicates | ||
| assert len(set([x[0] for x in PARSER_TEST_CASES])) == len(PARSER_TEST_CASES) | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES) | ||
| def test_parser(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text) == expected_datetime, assertion_message | ||
| # Parser test cases using datetime(2003, 9, 25) as a default. | ||
| # Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_DEFAULT_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"), | ||
| ("Sep 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("Sep", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"), | ||
| ("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"), | ||
| ("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10h", datetime(2003, 9, 25, 10), "hour with letters strip"), | ||
| ("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"), | ||
| ("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"), | ||
| ("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"), | ||
| ("10h am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10h pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("Wed", datetime(2003, 10, 1), "weekday alone"), | ||
| ("Wednesday", datetime(2003, 10, 1), "long weekday"), | ||
| ("October", datetime(2003, 10, 25), "long month"), | ||
| ("31-Dec-00", datetime(2000, 12, 31), "zero year"), | ||
| ("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"), | ||
| ("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"), | ||
| ("01h02", datetime(2003, 9, 25, 1, 2), "random format"), | ||
| ("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"), | ||
| ("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("01m02h", datetime(2003, 9, 25, 2, 1), "random format"), | ||
| ("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format") | ||
| ] | ||
| # Check that we don't have any duplicates | ||
| assert len(set([x[0] for x in PARSER_DEFAULT_TEST_CASES])) == len(PARSER_DEFAULT_TEST_CASES) | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES) | ||
| def test_parser_default(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message | ||
| @pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) | ||
| def test_parse_dayfirst(sep): | ||
| expected = datetime(2003, 9, 10) | ||
| fmt = sep.join(['%d', '%m', '%Y']) | ||
| dstr = expected.strftime(fmt) | ||
| result = parse(dstr, dayfirst=True) | ||
| assert result == expected | ||
| @pytest.mark.parametrize('sep', ['-', '.', '/', ' ']) | ||
| def test_parse_yearfirst(sep): | ||
| expected = datetime(2010, 9, 3) | ||
| fmt = sep.join(['%Y', '%m', '%d']) | ||
| dstr = expected.strftime(fmt) | ||
| result = parse(dstr, yearfirst=True) | ||
| assert result == expected | ||
| @pytest.mark.parametrize('dstr,expected', [ | ||
| ("Thu Sep 25 10:36:28 BRST 2003", datetime(2003, 9, 25, 10, 36, 28)), | ||
| ("1996.07.10 AD at 15:08:56 PDT", datetime(1996, 7, 10, 15, 8, 56)), | ||
| ("Tuesday, April 12, 1952 AD 3:30:42pm PST", | ||
| datetime(1952, 4, 12, 15, 30, 42)), | ||
| ("November 5, 1994, 8:15:30 am EST", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1994-11-05T08:15:30-05:00", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1994-11-05T08:15:30Z", datetime(1994, 11, 5, 8, 15, 30)), | ||
| ("1976-07-04T00:01:02Z", datetime(1976, 7, 4, 0, 1, 2)), | ||
| ("1986-07-05T08:15:30z", datetime(1986, 7, 5, 8, 15, 30)), | ||
| ("Tue Apr 4 00:22:12 PDT 1995", datetime(1995, 4, 4, 0, 22, 12)), | ||
| ]) | ||
| def test_parse_ignoretz(dstr, expected): | ||
| result = parse(dstr, ignoretz=True) | ||
| assert result == expected | ||
| _brsttz = tzoffset("BRST", -10800) | ||
| @pytest.mark.parametrize('dstr,expected', [ | ||
| ("20030925T104941-0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("Thu, 25 Sep 2003 10:49:41 -0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("2003-09-25T10:49:41.5-03:00", | ||
| datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), | ||
| ("2003-09-25T10:49:41-03:00", | ||
| datetime(2003, 9, 25, 10, 49, 41, tzinfo=_brsttz)), | ||
| ("20030925T104941.5-0300", | ||
| datetime(2003, 9, 25, 10, 49, 41, 500000, tzinfo=_brsttz)), | ||
| ]) | ||
| def test_parse_with_tzoffset(dstr, expected): | ||
| # In these cases, we are _not_ passing a tzinfos arg | ||
| result = parse(dstr) | ||
| assert result == expected | ||
| class TestFormat(object): | ||
| def test_ybd(self): | ||
| # If we have a 4-digit year, a non-numeric month (abbreviated or not), | ||
| # and a day (1 or 2 digits), then there is no ambiguity as to which | ||
| # token is a year/month/day. This holds regardless of what order the | ||
| # terms are in and for each of the separators below. | ||
| seps = ['-', ' ', '/', '.'] | ||
| year_tokens = ['%Y'] | ||
| month_tokens = ['%b', '%B'] | ||
| day_tokens = ['%d'] | ||
| if PLATFORM_HAS_DASH_D: | ||
| day_tokens.append('%-d') | ||
| prods = itertools.product(year_tokens, month_tokens, day_tokens) | ||
| perms = [y for x in prods for y in itertools.permutations(x)] | ||
| unambig_fmts = [sep.join(perm) for sep in seps for perm in perms] | ||
| actual = datetime(2003, 9, 25) | ||
| for fmt in unambig_fmts: | ||
| dstr = actual.strftime(fmt) | ||
| res = parse(dstr) | ||
| assert res == actual | ||
| # TODO: some redundancy with PARSER_TEST_CASES cases | ||
| @pytest.mark.parametrize("fmt,dstr", [ | ||
| ("%a %b %d %Y", "Thu Sep 25 2003"), | ||
| ("%b %d %Y", "Sep 25 2003"), | ||
| ("%Y-%m-%d", "2003-09-25"), | ||
| ("%Y%m%d", "20030925"), | ||
| ("%Y-%b-%d", "2003-Sep-25"), | ||
| ("%d-%b-%Y", "25-Sep-2003"), | ||
| ("%b-%d-%Y", "Sep-25-2003"), | ||
| ("%m-%d-%Y", "09-25-2003"), | ||
| ("%d-%m-%Y", "25-09-2003"), | ||
| ("%Y.%m.%d", "2003.09.25"), | ||
| ("%Y.%b.%d", "2003.Sep.25"), | ||
| ("%d.%b.%Y", "25.Sep.2003"), | ||
| ("%b.%d.%Y", "Sep.25.2003"), | ||
| ("%m.%d.%Y", "09.25.2003"), | ||
| ("%d.%m.%Y", "25.09.2003"), | ||
| ("%Y/%m/%d", "2003/09/25"), | ||
| ("%Y/%b/%d", "2003/Sep/25"), | ||
| ("%d/%b/%Y", "25/Sep/2003"), | ||
| ("%b/%d/%Y", "Sep/25/2003"), | ||
| ("%m/%d/%Y", "09/25/2003"), | ||
| ("%d/%m/%Y", "25/09/2003"), | ||
| ("%Y %m %d", "2003 09 25"), | ||
| ("%Y %b %d", "2003 Sep 25"), | ||
| ("%d %b %Y", "25 Sep 2003"), | ||
| ("%m %d %Y", "09 25 2003"), | ||
| ("%d %m %Y", "25 09 2003"), | ||
| ("%y %d %b", "03 25 Sep",), | ||
| ]) | ||
| def test_strftime_formats_2003Sep25(self, fmt, dstr): | ||
| expected = datetime(2003, 9, 25) | ||
| # First check that the format strings behave as expected | ||
| # (not strictly necessary, but nice to have) | ||
| assert expected.strftime(fmt) == dstr | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| class TestInputTypes(object): | ||
| def test_empty_string_invalid(self): | ||
| with pytest.raises(ParserError): | ||
| parse('') | ||
| def test_none_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(None) | ||
| def test_int_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(13) | ||
| def test_duck_typing(self): | ||
| # We want to support arbitrary classes that implement the stream | ||
| # interface. | ||
| class StringPassThrough(object): | ||
| def __init__(self, stream): | ||
| self.stream = stream | ||
| def read(self, *args, **kwargs): | ||
| return self.stream.read(*args, **kwargs) | ||
| dstr = StringPassThrough(StringIO('2014 January 19')) | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_stream(self): | ||
| dstr = StringIO('2014 January 19') | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_str(self): | ||
| # Parser should be able to handle bytestring and unicode | ||
| uni_str = '2014-05-01 08:00:00' | ||
| bytes_str = uni_str.encode() | ||
| res = parse(bytes_str) | ||
| expected = parse(uni_str) | ||
| assert res == expected | ||
| def test_parse_bytes(self): | ||
| res = parse(b'2014 January 19') | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_bytearray(self): | ||
| # GH#417 | ||
| res = parse(bytearray(b'2014 January 19')) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| class TestTzinfoInputTypes(object): | ||
| def assert_equal_same_tz(self, dt1, dt2): | ||
| assert dt1 == dt2 | ||
| assert dt1.tzinfo is dt2.tzinfo | ||
| def test_tzinfo_dict_could_return_none(self): | ||
| dstr = "2017-02-03 12:40 BRST" | ||
| result = parse(dstr, tzinfos={"BRST": None}) | ||
| expected = datetime(2017, 2, 3, 12, 40) | ||
| self.assert_equal_same_tz(result, expected) | ||
| def test_tzinfos_callable_could_return_none(self): | ||
| dstr = "2017-02-03 12:40 BRST" | ||
| result = parse(dstr, tzinfos=lambda *args: None) | ||
| expected = datetime(2017, 2, 3, 12, 40) | ||
| self.assert_equal_same_tz(result, expected) | ||
| def test_invalid_tzinfo_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| # Pass an absurd tzinfos object | ||
| tzinfos = {"UTC": ValueError} | ||
| with pytest.raises(TypeError): | ||
| parse(dstr, tzinfos=tzinfos) | ||
| def test_valid_tzinfo_tzinfo_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {"UTC": tz.UTC} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_unicode_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {u"UTC": u"UTC+0"} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_callable_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| def tzinfos(*args, **kwargs): | ||
| return u"UTC+0" | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| def test_valid_tzinfo_int_input(self): | ||
| dstr = "2014 January 19 09:00 UTC" | ||
| tzinfos = {u"UTC": -28800} | ||
| expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800)) | ||
| res = parse(dstr, tzinfos=tzinfos) | ||
| self.assert_equal_same_tz(res, expected) | ||
| class ParserTest(unittest.TestCase): | ||
| @classmethod | ||
| def setup_class(cls): | ||
| cls.tzinfos = {"BRST": -10800} | ||
| cls.brsttz = tzoffset("BRST", -10800) | ||
| cls.default = datetime(2003, 9, 25) | ||
| # Parser should be able to handle bytestring and unicode | ||
| cls.uni_str = '2014-05-01 08:00:00' | ||
| cls.str_str = cls.uni_str.encode() | ||
| def testParserParseStr(self): | ||
| from dateutil.parser import parser | ||
| assert parser().parse(self.str_str) == parser().parse(self.uni_str) | ||
| def testParseUnicodeWords(self): | ||
| class rus_parserinfo(parserinfo): | ||
| MONTHS = [("янв", "Январь"), | ||
| ("фев", "Февраль"), | ||
| ("мар", "Март"), | ||
| ("апр", "Апрель"), | ||
| ("май", "Май"), | ||
| ("июн", "Июнь"), | ||
| ("июл", "Июль"), | ||
| ("авг", "Август"), | ||
| ("сен", "Сентябрь"), | ||
| ("окт", "Октябрь"), | ||
| ("ноя", "Ноябрь"), | ||
| ("дек", "Декабрь")] | ||
| expected = datetime(2015, 9, 10, 10, 20) | ||
| res = parse('10 Сентябрь 2015 10:20', parserinfo=rus_parserinfo()) | ||
| assert res == expected | ||
| def testParseWithNulls(self): | ||
| # This relies on the from __future__ import unicode_literals, because | ||
| # explicitly specifying a unicode literal is a syntax error in Py 3.2 | ||
| # May want to switch to u'...' if we ever drop Python 3.2 support. | ||
| pstring = '\x00\x00August 29, 1924' | ||
| assert parse(pstring) == datetime(1924, 8, 29) | ||
| def testDateCommandFormat(self): | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", | ||
| tzinfos=self.tzinfos), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testDateCommandFormatReversed(self): | ||
| self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu", | ||
| tzinfos=self.tzinfos), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testDateCommandFormatWithLong(self): | ||
| if PY2: | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", | ||
| tzinfos={"BRST": long(-10800)}), | ||
| datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=self.brsttz)) | ||
| def testISOFormatStrip2(self): | ||
| self.assertEqual(parse("2003-09-25T10:49:41+03:00"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testISOStrippedFormatStrip2(self): | ||
| self.assertEqual(parse("20030925T104941+0300"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testAMPMNoHour(self): | ||
| with pytest.raises(ParserError): | ||
| parse("AM") | ||
| with pytest.raises(ParserError): | ||
| parse("Jan 20, 2015 PM") | ||
| def testAMPMRange(self): | ||
| with pytest.raises(ParserError): | ||
| parse("13:44 AM") | ||
| with pytest.raises(ParserError): | ||
| parse("January 25, 1921 23:13 PM") | ||
| def testPertain(self): | ||
| self.assertEqual(parse("Sep 03", default=self.default), | ||
| datetime(2003, 9, 3)) | ||
| self.assertEqual(parse("Sep of 03", default=self.default), | ||
| datetime(2003, 9, 25)) | ||
| def testFuzzy(self): | ||
| s = "Today is 25 of September of 2003, exactly " \ | ||
| "at 10:49:41 with timezone -03:00." | ||
| self.assertEqual(parse(s, fuzzy=True), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=self.brsttz)) | ||
| def testFuzzyWithTokens(self): | ||
| s1 = "Today is 25 of September of 2003, exactly " \ | ||
| "at 10:49:41 with timezone -03:00." | ||
| self.assertEqual(parse(s1, fuzzy_with_tokens=True), | ||
| (datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=self.brsttz), | ||
| ('Today is ', 'of ', ', exactly at ', | ||
| ' with timezone ', '.'))) | ||
| s2 = "http://biz.yahoo.com/ipo/p/600221.html" | ||
| self.assertEqual(parse(s2, fuzzy_with_tokens=True), | ||
| (datetime(2060, 2, 21, 0, 0, 0), | ||
| ('http://biz.yahoo.com/ipo/p/', '.html'))) | ||
| def testFuzzyAMPMProblem(self): | ||
| # Sometimes fuzzy parsing results in AM/PM flag being set without | ||
| # hours - if it's fuzzy it should ignore that. | ||
| s1 = "I have a meeting on March 1, 1974." | ||
| s2 = "On June 8th, 2020, I am going to be the first man on Mars" | ||
| # Also don't want any erroneous AM or PMs changing the parsed time | ||
| s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003" | ||
| s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset" | ||
| self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1)) | ||
| self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8)) | ||
| self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3)) | ||
| self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3)) | ||
| def testFuzzyIgnoreAMPM(self): | ||
| s1 = "Jan 29, 1945 14:45 AM I going to see you there?" | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse(s1, fuzzy=True) | ||
| self.assertEqual(res, datetime(1945, 1, 29, 14, 45)) | ||
| def testRandomFormat24(self): | ||
| self.assertEqual(parse("0:00 PM, PST", default=self.default, | ||
| ignoretz=True), | ||
| datetime(2003, 9, 25, 12, 0)) | ||
| def testRandomFormat26(self): | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse("5:50 A.M. on June 13, 1990") | ||
| self.assertEqual(res, datetime(1990, 6, 13, 5, 50)) | ||
| def testUnspecifiedDayFallback(self): | ||
| # Test that for an unspecified day, the fallback behavior is correct. | ||
| self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)), | ||
| datetime(2009, 4, 30)) | ||
| def testUnspecifiedDayFallbackFebNoLeapYear(self): | ||
| self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)), | ||
| datetime(2007, 2, 28)) | ||
| def testUnspecifiedDayFallbackFebLeapYear(self): | ||
| self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), | ||
| datetime(2008, 2, 29)) | ||
| def testErrorType01(self): | ||
| with pytest.raises(ParserError): | ||
| parse('shouldfail') | ||
| def testCorrectErrorOnFuzzyWithTokens(self): | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/32/423', fuzzy_with_tokens=True) | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/04 +32423', fuzzy_with_tokens=True) | ||
| assertRaisesRegex(self, ParserError, 'Unknown string format', | ||
| parse, '04/04/0d4', fuzzy_with_tokens=True) | ||
| def testIncreasingCTime(self): | ||
| # This test will check 200 different years, every month, every day, | ||
| # every hour, every minute, every second, and every weekday, using | ||
| # a delta of more or less 1 year, 1 month, 1 day, 1 minute and | ||
| # 1 second. | ||
| delta = timedelta(days=365+31+1, seconds=1+60+60*60) | ||
| dt = datetime(1900, 1, 1, 0, 0, 0, 0) | ||
| for i in range(200): | ||
| assert parse(dt.ctime()) == dt | ||
| dt += delta | ||
| def testIncreasingISOFormat(self): | ||
| delta = timedelta(days=365+31+1, seconds=1+60+60*60) | ||
| dt = datetime(1900, 1, 1, 0, 0, 0, 0) | ||
| for i in range(200): | ||
| assert parse(dt.isoformat()) == dt | ||
| dt += delta | ||
| def testMicrosecondsPrecisionError(self): | ||
| # Skip found out that sad precision problem. :-( | ||
| dt1 = parse("00:11:25.01") | ||
| dt2 = parse("00:12:10.01") | ||
| assert dt1.microsecond == 10000 | ||
| assert dt2.microsecond == 10000 | ||
| def testMicrosecondPrecisionErrorReturns(self): | ||
| # One more precision issue, discovered by Eric Brown. This should | ||
| # be the last one, as we're no longer using floating points. | ||
| for ms in [100001, 100000, 99999, 99998, | ||
| 10001, 10000, 9999, 9998, | ||
| 1001, 1000, 999, 998, | ||
| 101, 100, 99, 98]: | ||
| dt = datetime(2008, 2, 27, 21, 26, 1, ms) | ||
| assert parse(dt.isoformat()) == dt | ||
| def testCustomParserInfo(self): | ||
| # Custom parser info wasn't working, as Michael Elsdörfer discovered. | ||
| from dateutil.parser import parserinfo, parser | ||
| class myparserinfo(parserinfo): | ||
| MONTHS = parserinfo.MONTHS[:] | ||
| MONTHS[0] = ("Foo", "Foo") | ||
| myparser = parser(myparserinfo()) | ||
| dt = myparser.parse("01/Foo/2007") | ||
| assert dt == datetime(2007, 1, 1) | ||
| def testCustomParserShortDaynames(self): | ||
| # Horacio Hoyos discovered that day names shorter than 3 characters, | ||
| # for example two letter German day name abbreviations, don't work: | ||
| # https://github.com/dateutil/dateutil/issues/343 | ||
| from dateutil.parser import parserinfo, parser | ||
| class GermanParserInfo(parserinfo): | ||
| WEEKDAYS = [("Mo", "Montag"), | ||
| ("Di", "Dienstag"), | ||
| ("Mi", "Mittwoch"), | ||
| ("Do", "Donnerstag"), | ||
| ("Fr", "Freitag"), | ||
| ("Sa", "Samstag"), | ||
| ("So", "Sonntag")] | ||
| myparser = parser(GermanParserInfo()) | ||
| dt = myparser.parse("Sa 21. Jan 2017") | ||
| self.assertEqual(dt, datetime(2017, 1, 21)) | ||
| def testNoYearFirstNoDayFirst(self): | ||
| dtstr = '090107' | ||
| # Should be MMDDYY | ||
| self.assertEqual(parse(dtstr), | ||
| datetime(2007, 9, 1)) | ||
| self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=False), | ||
| datetime(2007, 9, 1)) | ||
| def testYearFirst(self): | ||
| dtstr = '090107' | ||
| # Should be MMDDYY | ||
| self.assertEqual(parse(dtstr, yearfirst=True), | ||
| datetime(2009, 1, 7)) | ||
| self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=False), | ||
| datetime(2009, 1, 7)) | ||
| def testDayFirst(self): | ||
| dtstr = '090107' | ||
| # Should be DDMMYY | ||
| self.assertEqual(parse(dtstr, dayfirst=True), | ||
| datetime(2007, 1, 9)) | ||
| self.assertEqual(parse(dtstr, yearfirst=False, dayfirst=True), | ||
| datetime(2007, 1, 9)) | ||
| def testDayFirstYearFirst(self): | ||
| dtstr = '090107' | ||
| # Should be YYDDMM | ||
| self.assertEqual(parse(dtstr, yearfirst=True, dayfirst=True), | ||
| datetime(2009, 7, 1)) | ||
| def testUnambiguousYearFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, yearfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def testUnambiguousDayFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, dayfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def testUnambiguousDayFirstYearFirst(self): | ||
| dtstr = '2015 09 25' | ||
| self.assertEqual(parse(dtstr, dayfirst=True, yearfirst=True), | ||
| datetime(2015, 9, 25)) | ||
| def test_mstridx(self): | ||
| # See GH408 | ||
| dtstr = '2015-15-May' | ||
| self.assertEqual(parse(dtstr), | ||
| datetime(2015, 5, 15)) | ||
| def test_idx_check(self): | ||
| dtstr = '2017-07-17 06:15:' | ||
| # Pre-PR, the trailing colon will cause an IndexError at 824-825 | ||
| # when checking `i < len_l` and then accessing `l[i+1]` | ||
| res = parse(dtstr, fuzzy=True) | ||
| assert res == datetime(2017, 7, 17, 6, 15) | ||
| def test_hmBY(self): | ||
| # See GH#483 | ||
| dtstr = '02:17NOV2017' | ||
| res = parse(dtstr, default=self.default) | ||
| assert res == datetime(2017, 11, self.default.day, 2, 17) | ||
| def test_validate_hour(self): | ||
| # See GH353 | ||
| invalid = "201A-01-01T23:58:39.239769+03:00" | ||
| with pytest.raises(ParserError): | ||
| parse(invalid) | ||
| def test_era_trailing_year(self): | ||
| dstr = 'AD2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| def test_includes_timestr(self): | ||
| timestr = "2020-13-97T44:61:83" | ||
| try: | ||
| parse(timestr) | ||
| except ParserError as e: | ||
| assert e.args[1] == timestr | ||
| else: | ||
| pytest.fail("Failed to raise ParserError") | ||
| class TestOutOfBounds(object): | ||
| def test_no_year_zero(self): | ||
| with pytest.raises(ParserError): | ||
| parse("0000 Jun 20") | ||
| def test_out_of_bound_day(self): | ||
| with pytest.raises(ParserError): | ||
| parse("Feb 30, 2007") | ||
| def test_illegal_month_error(self): | ||
| with pytest.raises(ParserError): | ||
| parse("0-100") | ||
| def test_day_sanity(self, fuzzy): | ||
| dstr = "2014-15-25" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_minute_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 22:64" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_hour_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 25:16 PM" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| def test_second_sanity(self, fuzzy): | ||
| dstr = "2014-02-28 22:14:64" | ||
| with pytest.raises(ParserError): | ||
| parse(dstr, fuzzy=fuzzy) | ||
| class TestParseUnimplementedCases(object): | ||
| @pytest.mark.xfail | ||
| def test_somewhat_ambiguous_string(self): | ||
| # Ref: github issue #487 | ||
| # The parser is choosing the wrong part for hour | ||
| # causing datetime to raise an exception. | ||
| dtstr = '1237 PM BRST Mon Oct 30 2017' | ||
| res = parse(dtstr, tzinfo=self.tzinfos) | ||
| assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos) | ||
| @pytest.mark.xfail | ||
| def test_YmdH_M_S(self): | ||
| # found in nasdaq's ftp data | ||
| dstr = '1991041310:19:24' | ||
| expected = datetime(1991, 4, 13, 10, 19, 24) | ||
| res = parse(dstr) | ||
| assert res == expected, (res, expected) | ||
| @pytest.mark.xfail | ||
| def test_first_century(self): | ||
| dstr = '0031 Nov 03' | ||
| expected = datetime(31, 11, 3) | ||
| res = parse(dstr) | ||
| assert res == expected, res | ||
| @pytest.mark.xfail | ||
| def test_era_trailing_year_with_dots(self): | ||
| dstr = 'A.D.2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| @pytest.mark.xfail | ||
| def test_ad_nospace(self): | ||
| expected = datetime(6, 5, 19) | ||
| for dstr in [' 6AD May 19', ' 06AD May 19', | ||
| ' 006AD May 19', ' 0006AD May 19']: | ||
| res = parse(dstr) | ||
| assert res == expected, (dstr, res) | ||
| @pytest.mark.xfail | ||
| def test_four_letter_day(self): | ||
| dstr = 'Frid Dec 30, 2016' | ||
| expected = datetime(2016, 12, 30) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_non_date_number(self): | ||
| dstr = '1,700' | ||
| with pytest.raises(ParserError): | ||
| parse(dstr) | ||
| @pytest.mark.xfail | ||
| def test_on_era(self): | ||
| # This could be classified as an "eras" test, but the relevant part | ||
| # about this is the ` on ` | ||
| dstr = '2:15 PM on January 2nd 1973 A.D.' | ||
| expected = datetime(1973, 1, 2, 14, 15) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(2012, 11, 7) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year_tokens(self): | ||
| # This was found in the wild at insidertrading.org | ||
| # Unlike in the case above, identifying the first "2012" as the year | ||
| # would not be a problem, but inferring that the latter 2012 is hhmm | ||
| # is a problem. | ||
| dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| expected = datetime(2012, 11, 7) | ||
| (res, tokens) = parse(dstr, fuzzy_with_tokens=True) | ||
| assert res == expected | ||
| assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",) | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year2(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust " | ||
| "u/d/t November 2, 1998 f/b/o Jennifer L Berylson") | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1998, 11, 2) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year3(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1994, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_unambiguous_YYYYMM(self): | ||
| # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed | ||
| # as instance of YYMMDD and parser could fallback to YYYYMM format. | ||
| dstr = "201712" | ||
| res = parse(dstr) | ||
| expected = datetime(2017, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_numerical_content(self): | ||
| # ref: https://github.com/dateutil/dateutil/issues/1029 | ||
| # parser interprets price and percentage as parts of the date | ||
| dstr = "£14.99 (25% off, until April 20)" | ||
| res = parse(dstr, fuzzy=True, default=datetime(2000, 1, 1)) | ||
| expected = datetime(2000, 4, 20) | ||
| assert res == expected | ||
| @pytest.mark.skipif(IS_WIN, reason="Windows does not use TZ var") | ||
| class TestTZVar(object): | ||
| def test_parse_unambiguous_nonexistent_local(self): | ||
| # When dates are specified "EST" even when they should be "EDT" in the | ||
| # local time zone, we should still assign the local time zone | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal()) | ||
| dt = parse('2011-08-01T12:30 EST') | ||
| assert dt.tzname() == 'EDT' | ||
| assert dt == dt_exp | ||
| def test_tzlocal_in_gmt(self): | ||
| # GH #318 | ||
| with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'): | ||
| # This is an imaginary datetime in tz.tzlocal() but should still | ||
| # parse using the GMT-as-alias-for-UTC rule | ||
| dt = parse('2004-05-01T12:00 GMT') | ||
| dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.UTC) | ||
| assert dt == dt_exp | ||
| def test_tzlocal_parse_fold(self): | ||
| # One manifestion of GH #318 | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal()) | ||
| dt_exp = tz.enfold(dt_exp, fold=1) | ||
| dt = parse('2011-11-06T01:30 EST') | ||
| # Because this is ambiguous, until `tz.tzlocal() is tz.tzlocal()` | ||
| # we'll just check the attributes we care about rather than | ||
| # dt == dt_exp | ||
| assert dt.tzname() == dt_exp.tzname() | ||
| assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None) | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) | ||
| def test_parse_tzinfos_fold(): | ||
| NYC = tz.gettz('America/New_York') | ||
| tzinfos = {'EST': NYC, 'EDT': NYC} | ||
| dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1) | ||
| dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos) | ||
| assert dt == dt_exp | ||
| assert dt.tzinfo is dt_exp.tzinfo | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.UTC) == dt_exp.astimezone(tz.UTC) | ||
| @pytest.mark.parametrize('dtstr,dt', [ | ||
| ('5.6h', datetime(2003, 9, 25, 5, 36)), | ||
| ('5.6m', datetime(2003, 9, 25, 0, 5, 36)), | ||
| # '5.6s' never had a rounding problem, test added for completeness | ||
| ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000)) | ||
| ]) | ||
| def test_rounding_floatlike_strings(dtstr, dt): | ||
| assert parse(dtstr, default=datetime(2003, 9, 25)) == dt | ||
| @pytest.mark.parametrize('value', ['1: test', 'Nan']) | ||
| def test_decimal_error(value): | ||
| # GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception | ||
| # when constructed with an invalid value | ||
| with pytest.raises(ParserError): | ||
| parse(value) | ||
| def test_parsererror_repr(): | ||
| # GH 991 — the __repr__ was not properly indented and so was never defined. | ||
| # This tests the current behavior of the ParserError __repr__, but the | ||
| # precise format is not guaranteed to be stable and may change even in | ||
| # minor versions. This test exists to avoid regressions. | ||
| s = repr(ParserError("Problem with string: %s", "2019-01-01")) | ||
| assert s == "ParserError('Problem with string: %s', '2019-01-01')" |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from ._common import NotAValue | ||
| import calendar | ||
| from datetime import datetime, date, timedelta | ||
| import unittest | ||
| import pytest | ||
| from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU | ||
| class RelativeDeltaTest(unittest.TestCase): | ||
| now = datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| today = date(2003, 9, 17) | ||
| def testInheritance(self): | ||
| # Ensure that relativedelta is inheritance-friendly. | ||
| class rdChildClass(relativedelta): | ||
| pass | ||
| ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1, | ||
| hours=1, minutes=1, seconds=1, microseconds=1) | ||
| rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1, | ||
| hours=1, minutes=1, seconds=1, microseconds=1) | ||
| self.assertEqual(type(ccRD + rd), type(ccRD), | ||
| msg='Addition does not inherit type.') | ||
| self.assertEqual(type(ccRD - rd), type(ccRD), | ||
| msg='Subtraction does not inherit type.') | ||
| self.assertEqual(type(-ccRD), type(ccRD), | ||
| msg='Negation does not inherit type.') | ||
| self.assertEqual(type(ccRD * 5.0), type(ccRD), | ||
| msg='Multiplication does not inherit type.') | ||
| self.assertEqual(type(ccRD / 5.0), type(ccRD), | ||
| msg='Division does not inherit type.') | ||
| def testMonthEndMonthBeginning(self): | ||
| self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59), | ||
| datetime(2003, 3, 1, 0, 0, 0)), | ||
| relativedelta(months=-1, seconds=-1)) | ||
| self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), | ||
| datetime(2003, 1, 31, 23, 59, 59)), | ||
| relativedelta(months=1, seconds=1)) | ||
| def testMonthEndMonthBeginningLeapYear(self): | ||
| self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59), | ||
| datetime(2012, 3, 1, 0, 0, 0)), | ||
| relativedelta(months=-1, seconds=-1)) | ||
| self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), | ||
| datetime(2003, 1, 31, 23, 59, 59)), | ||
| relativedelta(months=1, seconds=1)) | ||
| def testNextMonth(self): | ||
| self.assertEqual(self.now+relativedelta(months=+1), | ||
| datetime(2003, 10, 17, 20, 54, 47, 282310)) | ||
| def testNextMonthPlusOneWeek(self): | ||
| self.assertEqual(self.now+relativedelta(months=+1, weeks=+1), | ||
| datetime(2003, 10, 24, 20, 54, 47, 282310)) | ||
| def testNextMonthPlusOneWeek10am(self): | ||
| self.assertEqual(self.today + | ||
| relativedelta(months=+1, weeks=+1, hour=10), | ||
| datetime(2003, 10, 24, 10, 0)) | ||
| def testNextMonthPlusOneWeek10amDiff(self): | ||
| self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0), | ||
| self.today), | ||
| relativedelta(months=+1, days=+7, hours=+10)) | ||
| def testOneMonthBeforeOneYear(self): | ||
| self.assertEqual(self.now+relativedelta(years=+1, months=-1), | ||
| datetime(2004, 8, 17, 20, 54, 47, 282310)) | ||
| def testMonthsOfDiffNumOfDays(self): | ||
| self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1), | ||
| date(2003, 2, 27)) | ||
| self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1), | ||
| date(2003, 2, 28)) | ||
| self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2), | ||
| date(2003, 3, 31)) | ||
| def testMonthsOfDiffNumOfDaysWithYears(self): | ||
| self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1), | ||
| date(2001, 2, 28)) | ||
| self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1), | ||
| date(2001, 2, 28)) | ||
| self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1), | ||
| date(2000, 2, 28)) | ||
| self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), | ||
| date(2000, 3, 1)) | ||
| self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), | ||
| date(2000, 3, 1)) | ||
| self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1), | ||
| date(2000, 2, 28)) | ||
| self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1), | ||
| date(2000, 3, 1)) | ||
| def testNextFriday(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=FR), | ||
| date(2003, 9, 19)) | ||
| def testNextFridayInt(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY), | ||
| date(2003, 9, 19)) | ||
| def testLastFridayInThisMonth(self): | ||
| self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)), | ||
| date(2003, 9, 26)) | ||
| def testLastDayOfFebruary(self): | ||
| self.assertEqual(date(2021, 2, 1) + relativedelta(day=31), | ||
| date(2021, 2, 28)) | ||
| def testLastDayOfFebruaryLeapYear(self): | ||
| self.assertEqual(date(2020, 2, 1) + relativedelta(day=31), | ||
| date(2020, 2, 29)) | ||
| def testNextWednesdayIsToday(self): | ||
| self.assertEqual(self.today+relativedelta(weekday=WE), | ||
| date(2003, 9, 17)) | ||
| def testNextWednesdayNotToday(self): | ||
| self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), | ||
| date(2003, 9, 24)) | ||
| def testAddMoreThan12Months(self): | ||
| self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13), | ||
| date(2005, 1, 1)) | ||
| def testAddNegativeMonths(self): | ||
| self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2), | ||
| date(2002, 11, 1)) | ||
| def test15thISOYearWeek(self): | ||
| self.assertEqual(date(2003, 1, 1) + | ||
| relativedelta(day=4, weeks=+14, weekday=MO(-1)), | ||
| date(2003, 4, 7)) | ||
| def testMillenniumAge(self): | ||
| self.assertEqual(relativedelta(self.now, date(2001, 1, 1)), | ||
| relativedelta(years=+2, months=+8, days=+16, | ||
| hours=+20, minutes=+54, seconds=+47, | ||
| microseconds=+282310)) | ||
| def testJohnAge(self): | ||
| self.assertEqual(relativedelta(self.now, | ||
| datetime(1978, 4, 5, 12, 0)), | ||
| relativedelta(years=+25, months=+5, days=+12, | ||
| hours=+8, minutes=+54, seconds=+47, | ||
| microseconds=+282310)) | ||
| def testJohnAgeWithDate(self): | ||
| self.assertEqual(relativedelta(self.today, | ||
| datetime(1978, 4, 5, 12, 0)), | ||
| relativedelta(years=+25, months=+5, days=+11, | ||
| hours=+12)) | ||
| def testYearDay(self): | ||
| self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260), | ||
| date(2003, 9, 17)) | ||
| self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260), | ||
| date(2002, 9, 17)) | ||
| self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260), | ||
| date(2000, 9, 16)) | ||
| self.assertEqual(self.today+relativedelta(yearday=261), | ||
| date(2003, 9, 18)) | ||
| def testYearDayBug(self): | ||
| # Tests a problem reported by Adam Ryan. | ||
| self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15), | ||
| date(2010, 1, 15)) | ||
| def testNonLeapYearDay(self): | ||
| self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2003, 9, 17)) | ||
| self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2002, 9, 17)) | ||
| self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260), | ||
| date(2000, 9, 17)) | ||
| self.assertEqual(self.today+relativedelta(yearday=261), | ||
| date(2003, 9, 18)) | ||
| def testAddition(self): | ||
| self.assertEqual(relativedelta(days=10) + | ||
| relativedelta(years=1, months=2, days=3, hours=4, | ||
| minutes=5, microseconds=6), | ||
| relativedelta(years=1, months=2, days=13, hours=4, | ||
| minutes=5, microseconds=6)) | ||
| def testAbsoluteAddition(self): | ||
| self.assertEqual(relativedelta() + relativedelta(day=0, hour=0), | ||
| relativedelta(day=0, hour=0)) | ||
| self.assertEqual(relativedelta(day=0, hour=0) + relativedelta(), | ||
| relativedelta(day=0, hour=0)) | ||
| def testAdditionToDatetime(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1), | ||
| datetime(2000, 1, 2)) | ||
| def testRightAdditionToDatetime(self): | ||
| self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1), | ||
| datetime(2000, 1, 2)) | ||
| def testAdditionInvalidType(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(days=3) + 9 | ||
| def testAdditionUnsupportedType(self): | ||
| # For unsupported types that define their own comparators, etc. | ||
| self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) | ||
| def testAdditionFloatValue(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)), | ||
| datetime(2000, 1, 2)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)), | ||
| datetime(2000, 2, 1)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)), | ||
| datetime(2001, 1, 1)) | ||
| def testAdditionFloatFractionals(self): | ||
| self.assertEqual(datetime(2000, 1, 1, 0) + | ||
| relativedelta(days=float(0.5)), | ||
| datetime(2000, 1, 1, 12)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0) + | ||
| relativedelta(hours=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) + | ||
| relativedelta(minutes=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(seconds=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(microseconds=float(500000.25)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| def testSubtraction(self): | ||
| self.assertEqual(relativedelta(days=10) - | ||
| relativedelta(years=1, months=2, days=3, hours=4, | ||
| minutes=5, microseconds=6), | ||
| relativedelta(years=-1, months=-2, days=7, hours=-4, | ||
| minutes=-5, microseconds=-6)) | ||
| def testRightSubtractionFromDatetime(self): | ||
| self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1), | ||
| datetime(2000, 1, 1)) | ||
| def testSubractionWithDatetime(self): | ||
| self.assertRaises(TypeError, lambda x, y: x - y, | ||
| (relativedelta(days=1), datetime(2000, 1, 1))) | ||
| def testSubtractionInvalidType(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(hours=12) - 14 | ||
| def testSubtractionUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) + NotAValue, NotAValue) | ||
| def testMultiplication(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28, | ||
| datetime(2000, 1, 29)) | ||
| self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1), | ||
| datetime(2000, 1, 29)) | ||
| def testMultiplicationUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) * NotAValue, NotAValue) | ||
| def testDivision(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28, | ||
| datetime(2000, 1, 2)) | ||
| def testDivisionUnsupportedType(self): | ||
| self.assertIs(relativedelta(days=1) / NotAValue, NotAValue) | ||
| def testBoolean(self): | ||
| self.assertFalse(relativedelta(days=0)) | ||
| self.assertTrue(relativedelta(days=1)) | ||
| def testAbsoluteValueNegative(self): | ||
| rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3, | ||
| minutes=-5, seconds=-2, microseconds=-12) | ||
| rd_expected = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testAbsoluteValuePositive(self): | ||
| rd_base = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| rd_expected = rd_base | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testComparison(self): | ||
| d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=1) | ||
| d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=1) | ||
| d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| minutes=1, seconds=1, microseconds=2) | ||
| self.assertEqual(d1, d2) | ||
| self.assertNotEqual(d1, d3) | ||
| def testInequalityTypeMismatch(self): | ||
| # Different type | ||
| self.assertFalse(relativedelta(year=1) == 19) | ||
| def testInequalityUnsupportedType(self): | ||
| self.assertIs(relativedelta(hours=3) == NotAValue, NotAValue) | ||
| def testInequalityWeekdays(self): | ||
| # Different weekdays | ||
| no_wday = relativedelta(year=1997, month=4) | ||
| wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1)) | ||
| wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2)) | ||
| wday_tu = relativedelta(year=1997, month=4, weekday=TU) | ||
| self.assertTrue(wday_mo_1 == wday_mo_1) | ||
| self.assertFalse(no_wday == wday_mo_1) | ||
| self.assertFalse(wday_mo_1 == no_wday) | ||
| self.assertFalse(wday_mo_1 == wday_mo_2) | ||
| self.assertFalse(wday_mo_2 == wday_mo_1) | ||
| self.assertFalse(wday_mo_1 == wday_tu) | ||
| self.assertFalse(wday_tu == wday_mo_1) | ||
| def testMonthOverflow(self): | ||
| self.assertEqual(relativedelta(months=273), | ||
| relativedelta(years=22, months=9)) | ||
| def testWeeks(self): | ||
| # Test that the weeks property is working properly. | ||
| rd = relativedelta(years=4, months=2, weeks=8, days=6) | ||
| self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6)) | ||
| rd.weeks = 3 | ||
| self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6)) | ||
| def testRelativeDeltaRepr(self): | ||
| self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)), | ||
| 'relativedelta(years=+1, months=-1, days=+15)') | ||
| self.assertEqual(repr(relativedelta(months=14, seconds=-25)), | ||
| 'relativedelta(years=+1, months=+2, seconds=-25)') | ||
| self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))), | ||
| 'relativedelta(month=3, weekday=SU(+3), hour=3)') | ||
| def testRelativeDeltaFractionalYear(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(years=1.5) | ||
| def testRelativeDeltaFractionalMonth(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(months=1.5) | ||
| def testRelativeDeltaInvalidDatetimeObject(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2)) | ||
| def testRelativeDeltaFractionalAbsolutes(self): | ||
| # Fractional absolute values will soon be unsupported, | ||
| # check for the deprecation warning. | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(year=2.86) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(month=1.29) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(day=0.44) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(hour=23.98) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(minute=45.21) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(second=13.2) | ||
| with pytest.warns(DeprecationWarning): | ||
| relativedelta(microsecond=157221.93) | ||
| def testRelativeDeltaFractionalRepr(self): | ||
| rd = relativedelta(years=3, months=-2, days=1.25) | ||
| self.assertEqual(repr(rd), | ||
| 'relativedelta(years=+3, months=-2, days=+1.25)') | ||
| rd = relativedelta(hours=0.5, seconds=9.22) | ||
| self.assertEqual(repr(rd), | ||
| 'relativedelta(hours=+0.5, seconds=+9.22)') | ||
| def testRelativeDeltaFractionalWeeks(self): | ||
| # Equivalent to days=8, hours=18 | ||
| rd = relativedelta(weeks=1.25) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 11, 18)) | ||
| def testRelativeDeltaFractionalDays(self): | ||
| rd1 = relativedelta(days=1.48) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 4, 11, 31, 12)) | ||
| rd2 = relativedelta(days=1.5) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 4, 12, 0, 0)) | ||
| def testRelativeDeltaFractionalHours(self): | ||
| rd = relativedelta(days=1, hours=12.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 4, 12, 30, 0)) | ||
| def testRelativeDeltaFractionalMinutes(self): | ||
| rd = relativedelta(hours=1, minutes=30.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 3, 1, 30, 30)) | ||
| def testRelativeDeltaFractionalSeconds(self): | ||
| rd = relativedelta(hours=5, minutes=30, seconds=30.5) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd, | ||
| datetime(2009, 9, 3, 5, 30, 30, 500000)) | ||
| def testRelativeDeltaFractionalPositiveOverflow(self): | ||
| # Equivalent to (days=1, hours=14) | ||
| rd1 = relativedelta(days=1.5, hours=2) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 4, 14, 0, 0)) | ||
| # Equivalent to (days=1, hours=14, minutes=45) | ||
| rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) | ||
| d1 = datetime(2009, 9, 3, 0, 0) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 4, 14, 45)) | ||
| # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1) | ||
| rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31) | ||
| self.assertEqual(d1 + rd3, | ||
| datetime(2009, 9, 5, 2, 0, 1)) | ||
| def testRelativeDeltaFractionalNegativeDays(self): | ||
| # Equivalent to (days=-1, hours=-1) | ||
| rd1 = relativedelta(days=-1.5, hours=11) | ||
| d1 = datetime(2009, 9, 3, 12, 0) | ||
| self.assertEqual(d1 + rd1, | ||
| datetime(2009, 9, 2, 11, 0, 0)) | ||
| # Equivalent to (days=-1, hours=-9) | ||
| rd2 = relativedelta(days=-1.25, hours=-3) | ||
| self.assertEqual(d1 + rd2, | ||
| datetime(2009, 9, 2, 3)) | ||
| def testRelativeDeltaNormalizeFractionalDays(self): | ||
| # Equivalent to (days=2, hours=18) | ||
| rd1 = relativedelta(days=2.75) | ||
| self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18)) | ||
| # Equivalent to (days=1, hours=11, minutes=31, seconds=12) | ||
| rd2 = relativedelta(days=1.48) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=1, hours=11, minutes=31, seconds=12)) | ||
| def testRelativeDeltaNormalizeFractionalDays2(self): | ||
| # Equivalent to (hours=1, minutes=30) | ||
| rd1 = relativedelta(hours=1.5) | ||
| self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30)) | ||
| # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100) | ||
| rd2 = relativedelta(hours=3.28472225) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(hours=3, minutes=17, seconds=5, microseconds=100)) | ||
| def testRelativeDeltaNormalizeFractionalMinutes(self): | ||
| # Equivalent to (minutes=15, seconds=36) | ||
| rd1 = relativedelta(minutes=15.6) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(minutes=15, seconds=36)) | ||
| # Equivalent to (minutes=25, seconds=20, microseconds=25000) | ||
| rd2 = relativedelta(minutes=25.33375) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(minutes=25, seconds=20, microseconds=25000)) | ||
| def testRelativeDeltaNormalizeFractionalSeconds(self): | ||
| # Equivalent to (seconds=45, microseconds=25000) | ||
| rd1 = relativedelta(seconds=45.025) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(seconds=45, microseconds=25000)) | ||
| def testRelativeDeltaFractionalPositiveOverflow2(self): | ||
| # Equivalent to (days=1, hours=14) | ||
| rd1 = relativedelta(days=1.5, hours=2) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(days=1, hours=14)) | ||
| # Equivalent to (days=1, hours=14, minutes=45) | ||
| rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=1, hours=14, minutes=45)) | ||
| # Carry back up - equivalent to: | ||
| # (days=2, hours=2, minutes=0, seconds=2, microseconds=3) | ||
| rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045, | ||
| seconds=31.473, microseconds=500003) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=2, hours=2, minutes=0, | ||
| seconds=2, microseconds=3)) | ||
| def testRelativeDeltaFractionalNegativeOverflow(self): | ||
| # Equivalent to (days=-1) | ||
| rd1 = relativedelta(days=-0.5, hours=-12) | ||
| self.assertEqual(rd1.normalized(), | ||
| relativedelta(days=-1)) | ||
| # Equivalent to (days=-1) | ||
| rd2 = relativedelta(days=-1.5, hours=12) | ||
| self.assertEqual(rd2.normalized(), | ||
| relativedelta(days=-1)) | ||
| # Equivalent to (days=-1, hours=-14, minutes=-45) | ||
| rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=-1, hours=-14, minutes=-45)) | ||
| # Equivalent to (days=-1, hours=-14, minutes=+15) | ||
| rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45) | ||
| self.assertEqual(rd4.normalized(), | ||
| relativedelta(days=-1, hours=-14, minutes=+15)) | ||
| # Carry back up - equivalent to: | ||
| # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3) | ||
| rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045, | ||
| seconds=-31.473, microseconds=-500003) | ||
| self.assertEqual(rd3.normalized(), | ||
| relativedelta(days=-2, hours=-2, minutes=0, | ||
| seconds=-2, microseconds=-3)) | ||
| def testInvalidYearDay(self): | ||
| with self.assertRaises(ValueError): | ||
| relativedelta(yearday=367) | ||
| def testAddTimedeltaToUnpopulatedRelativedelta(self): | ||
| td = timedelta( | ||
| days=1, | ||
| seconds=1, | ||
| microseconds=1, | ||
| milliseconds=1, | ||
| minutes=1, | ||
| hours=1, | ||
| weeks=1 | ||
| ) | ||
| expected = relativedelta( | ||
| weeks=1, | ||
| days=1, | ||
| hours=1, | ||
| minutes=1, | ||
| seconds=1, | ||
| microseconds=1001 | ||
| ) | ||
| self.assertEqual(expected, relativedelta() + td) | ||
| def testAddTimedeltaToPopulatedRelativeDelta(self): | ||
| td = timedelta( | ||
| days=1, | ||
| seconds=1, | ||
| microseconds=1, | ||
| milliseconds=1, | ||
| minutes=1, | ||
| hours=1, | ||
| weeks=1 | ||
| ) | ||
| rd = relativedelta( | ||
| year=1, | ||
| month=1, | ||
| day=1, | ||
| hour=1, | ||
| minute=1, | ||
| second=1, | ||
| microsecond=1, | ||
| years=1, | ||
| months=1, | ||
| days=1, | ||
| weeks=1, | ||
| hours=1, | ||
| minutes=1, | ||
| seconds=1, | ||
| microseconds=1 | ||
| ) | ||
| expected = relativedelta( | ||
| year=1, | ||
| month=1, | ||
| day=1, | ||
| hour=1, | ||
| minute=1, | ||
| second=1, | ||
| microsecond=1, | ||
| years=1, | ||
| months=1, | ||
| weeks=2, | ||
| days=2, | ||
| hours=2, | ||
| minutes=2, | ||
| seconds=2, | ||
| microseconds=1002, | ||
| ) | ||
| self.assertEqual(expected, rd + td) | ||
| def testHashable(self): | ||
| try: | ||
| {relativedelta(minute=1): 'test'} | ||
| except: | ||
| self.fail("relativedelta() failed to hash!") | ||
| class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase): | ||
| """Test the weeks property getter""" | ||
| def test_one_day(self): | ||
| rd = relativedelta(days=1) | ||
| self.assertEqual(rd.days, 1) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_minus_one_day(self): | ||
| rd = relativedelta(days=-1) | ||
| self.assertEqual(rd.days, -1) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_height_days(self): | ||
| rd = relativedelta(days=8) | ||
| self.assertEqual(rd.days, 8) | ||
| self.assertEqual(rd.weeks, 1) | ||
| def test_minus_height_days(self): | ||
| rd = relativedelta(days=-8) | ||
| self.assertEqual(rd.days, -8) | ||
| self.assertEqual(rd.weeks, -1) | ||
| class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase): | ||
| """Test the weeks setter which makes a "smart" update of the days attribute""" | ||
| def test_one_day_set_one_week(self): | ||
| rd = relativedelta(days=1) | ||
| rd.weeks = 1 # add 7 days | ||
| self.assertEqual(rd.days, 8) | ||
| self.assertEqual(rd.weeks, 1) | ||
| def test_minus_one_day_set_one_week(self): | ||
| rd = relativedelta(days=-1) | ||
| rd.weeks = 1 # add 7 days | ||
| self.assertEqual(rd.days, 6) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_height_days_set_minus_one_week(self): | ||
| rd = relativedelta(days=8) | ||
| rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day | ||
| self.assertEqual(rd.days, -6) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_minus_height_days_set_minus_one_week(self): | ||
| rd = relativedelta(days=-8) | ||
| rd.weeks = -1 # does not change anything | ||
| self.assertEqual(rd.days, -8) | ||
| self.assertEqual(rd.weeks, -1) | ||
| # vim:ts=4:sw=4:et |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import timedelta, datetime | ||
| from dateutil import tz | ||
| from dateutil import utils | ||
| from dateutil.tz import UTC | ||
| from dateutil.utils import within_delta | ||
| from freezegun import freeze_time | ||
| NYC = tz.gettz("America/New_York") | ||
| @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) | ||
| def test_utils_today(): | ||
| assert utils.today() == datetime(2014, 12, 15, 0, 0, 0) | ||
| @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) | ||
| def test_utils_today_tz_info(): | ||
| assert utils.today(NYC) == datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC) | ||
| @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) | ||
| def test_utils_today_tz_info_different_day(): | ||
| assert utils.today(UTC) == datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC) | ||
| def test_utils_default_tz_info_naive(): | ||
| dt = datetime(2014, 9, 14, 9, 30) | ||
| assert utils.default_tzinfo(dt, NYC).tzinfo is NYC | ||
| def test_utils_default_tz_info_aware(): | ||
| dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) | ||
| assert utils.default_tzinfo(dt, NYC).tzinfo is UTC | ||
| def test_utils_within_delta(): | ||
| d1 = datetime(2016, 1, 1, 12, 14, 1, 9) | ||
| d2 = d1.replace(microsecond=15) | ||
| assert within_delta(d1, d2, timedelta(seconds=1)) | ||
| assert not within_delta(d1, d2, timedelta(microseconds=1)) | ||
| def test_utils_within_delta_with_negative_delta(): | ||
| d1 = datetime(2016, 1, 1) | ||
| d2 = datetime(2015, 12, 31) | ||
| assert within_delta(d2, d1, timedelta(days=-1)) |
| # -*- coding: utf-8 -*- | ||
| from .tz import * | ||
| from .tz import __doc__ | ||
| __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", | ||
| "enfold", "datetime_ambiguous", "datetime_exists", | ||
| "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] | ||
| class DeprecatedTzFormatWarning(Warning): | ||
| """Warning raised when time zones are parsed from deprecated formats.""" |
| from six import PY2 | ||
| from functools import wraps | ||
| from datetime import datetime, timedelta, tzinfo | ||
| ZERO = timedelta(0) | ||
| __all__ = ['tzname_in_python2', 'enfold'] | ||
| def tzname_in_python2(namefunc): | ||
| """Change unicode output into bytestrings in Python 2 | ||
| tzname() API changed in Python 3. It used to return bytes, but was changed | ||
| to unicode strings | ||
| """ | ||
| if PY2: | ||
| @wraps(namefunc) | ||
| def adjust_encoding(*args, **kwargs): | ||
| name = namefunc(*args, **kwargs) | ||
| if name is not None: | ||
| name = name.encode() | ||
| return name | ||
| return adjust_encoding | ||
| else: | ||
| return namefunc | ||
| # The following is adapted from Alexander Belopolsky's tz library | ||
| # https://github.com/abalkin/tz | ||
| if hasattr(datetime, 'fold'): | ||
| # This is the pre-python 3.6 fold situation | ||
| def enfold(dt, fold=1): | ||
| """ | ||
| Provides a unified interface for assigning the ``fold`` attribute to | ||
| datetimes both before and after the implementation of PEP-495. | ||
| :param fold: | ||
| The value for the ``fold`` attribute in the returned datetime. This | ||
| should be either 0 or 1. | ||
| :return: | ||
| Returns an object for which ``getattr(dt, 'fold', 0)`` returns | ||
| ``fold`` for all versions of Python. In versions prior to | ||
| Python 3.6, this is a ``_DatetimeWithFold`` object, which is a | ||
| subclass of :py:class:`datetime.datetime` with the ``fold`` | ||
| attribute added, if ``fold`` is 1. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| return dt.replace(fold=fold) | ||
| else: | ||
| class _DatetimeWithFold(datetime): | ||
| """ | ||
| This is a class designed to provide a PEP 495-compliant interface for | ||
| Python versions before 3.6. It is used only for dates in a fold, so | ||
| the ``fold`` attribute is fixed at ``1``. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| __slots__ = () | ||
| def replace(self, *args, **kwargs): | ||
| """ | ||
| Return a datetime with the same attributes, except for those | ||
| attributes given new values by whichever keyword arguments are | ||
| specified. Note that tzinfo=None can be specified to create a naive | ||
| datetime from an aware datetime with no conversion of date and time | ||
| data. | ||
| This is reimplemented in ``_DatetimeWithFold`` because pypy3 will | ||
| return a ``datetime.datetime`` even if ``fold`` is unchanged. | ||
| """ | ||
| argnames = ( | ||
| 'year', 'month', 'day', 'hour', 'minute', 'second', | ||
| 'microsecond', 'tzinfo' | ||
| ) | ||
| for arg, argname in zip(args, argnames): | ||
| if argname in kwargs: | ||
| raise TypeError('Duplicate argument: {}'.format(argname)) | ||
| kwargs[argname] = arg | ||
| for argname in argnames: | ||
| if argname not in kwargs: | ||
| kwargs[argname] = getattr(self, argname) | ||
| dt_class = self.__class__ if kwargs.get('fold', 1) else datetime | ||
| return dt_class(**kwargs) | ||
| @property | ||
| def fold(self): | ||
| return 1 | ||
| def enfold(dt, fold=1): | ||
| """ | ||
| Provides a unified interface for assigning the ``fold`` attribute to | ||
| datetimes both before and after the implementation of PEP-495. | ||
| :param fold: | ||
| The value for the ``fold`` attribute in the returned datetime. This | ||
| should be either 0 or 1. | ||
| :return: | ||
| Returns an object for which ``getattr(dt, 'fold', 0)`` returns | ||
| ``fold`` for all versions of Python. In versions prior to | ||
| Python 3.6, this is a ``_DatetimeWithFold`` object, which is a | ||
| subclass of :py:class:`datetime.datetime` with the ``fold`` | ||
| attribute added, if ``fold`` is 1. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| if getattr(dt, 'fold', 0) == fold: | ||
| return dt | ||
| args = dt.timetuple()[:6] | ||
| args += (dt.microsecond, dt.tzinfo) | ||
| if fold: | ||
| return _DatetimeWithFold(*args) | ||
| else: | ||
| return datetime(*args) | ||
| def _validate_fromutc_inputs(f): | ||
| """ | ||
| The CPython version of ``fromutc`` checks that the input is a ``datetime`` | ||
| object and that ``self`` is attached as its ``tzinfo``. | ||
| """ | ||
| @wraps(f) | ||
| def fromutc(self, dt): | ||
| if not isinstance(dt, datetime): | ||
| raise TypeError("fromutc() requires a datetime argument") | ||
| if dt.tzinfo is not self: | ||
| raise ValueError("dt.tzinfo is not self") | ||
| return f(self, dt) | ||
| return fromutc | ||
| class _tzinfo(tzinfo): | ||
| """ | ||
| Base class for all ``dateutil`` ``tzinfo`` objects. | ||
| """ | ||
| def is_ambiguous(self, dt): | ||
| """ | ||
| Whether or not the "wall time" of a given datetime is ambiguous in this | ||
| zone. | ||
| :param dt: | ||
| A :py:class:`datetime.datetime`, naive or time zone aware. | ||
| :return: | ||
| Returns ``True`` if ambiguous, ``False`` otherwise. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| dt = dt.replace(tzinfo=self) | ||
| wall_0 = enfold(dt, fold=0) | ||
| wall_1 = enfold(dt, fold=1) | ||
| same_offset = wall_0.utcoffset() == wall_1.utcoffset() | ||
| same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) | ||
| return same_dt and not same_offset | ||
| def _fold_status(self, dt_utc, dt_wall): | ||
| """ | ||
| Determine the fold status of a "wall" datetime, given a representation | ||
| of the same datetime as a (naive) UTC datetime. This is calculated based | ||
| on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all | ||
| datetimes, and that this offset is the actual number of hours separating | ||
| ``dt_utc`` and ``dt_wall``. | ||
| :param dt_utc: | ||
| Representation of the datetime as UTC | ||
| :param dt_wall: | ||
| Representation of the datetime as "wall time". This parameter must | ||
| either have a `fold` attribute or have a fold-naive | ||
| :class:`datetime.tzinfo` attached, otherwise the calculation may | ||
| fail. | ||
| """ | ||
| if self.is_ambiguous(dt_wall): | ||
| delta_wall = dt_wall - dt_utc | ||
| _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) | ||
| else: | ||
| _fold = 0 | ||
| return _fold | ||
| def _fold(self, dt): | ||
| return getattr(dt, 'fold', 0) | ||
| def _fromutc(self, dt): | ||
| """ | ||
| Given a timezone-aware datetime in a given timezone, calculates a | ||
| timezone-aware datetime in a new timezone. | ||
| Since this is the one time that we *know* we have an unambiguous | ||
| datetime object, we take this opportunity to determine whether the | ||
| datetime is ambiguous and in a "fold" state (e.g. if it's the first | ||
| occurrence, chronologically, of the ambiguous datetime). | ||
| :param dt: | ||
| A timezone-aware :class:`datetime.datetime` object. | ||
| """ | ||
| # Re-implement the algorithm from Python's datetime.py | ||
| dtoff = dt.utcoffset() | ||
| if dtoff is None: | ||
| raise ValueError("fromutc() requires a non-None utcoffset() " | ||
| "result") | ||
| # The original datetime.py code assumes that `dst()` defaults to | ||
| # zero during ambiguous times. PEP 495 inverts this presumption, so | ||
| # for pre-PEP 495 versions of python, we need to tweak the algorithm. | ||
| dtdst = dt.dst() | ||
| if dtdst is None: | ||
| raise ValueError("fromutc() requires a non-None dst() result") | ||
| delta = dtoff - dtdst | ||
| dt += delta | ||
| # Set fold=1 so we can default to being in the fold for | ||
| # ambiguous dates. | ||
| dtdst = enfold(dt, fold=1).dst() | ||
| if dtdst is None: | ||
| raise ValueError("fromutc(): dt.dst gave inconsistent " | ||
| "results; cannot convert") | ||
| return dt + dtdst | ||
| @_validate_fromutc_inputs | ||
| def fromutc(self, dt): | ||
| """ | ||
| Given a timezone-aware datetime in a given timezone, calculates a | ||
| timezone-aware datetime in a new timezone. | ||
| Since this is the one time that we *know* we have an unambiguous | ||
| datetime object, we take this opportunity to determine whether the | ||
| datetime is ambiguous and in a "fold" state (e.g. if it's the first | ||
| occurrence, chronologically, of the ambiguous datetime). | ||
| :param dt: | ||
| A timezone-aware :class:`datetime.datetime` object. | ||
| """ | ||
| dt_wall = self._fromutc(dt) | ||
| # Calculate the fold status given the two datetimes. | ||
| _fold = self._fold_status(dt, dt_wall) | ||
| # Set the default fold value for ambiguous dates | ||
| return enfold(dt_wall, fold=_fold) | ||
| class tzrangebase(_tzinfo): | ||
| """ | ||
| This is an abstract base class for time zones represented by an annual | ||
| transition into and out of DST. Child classes should implement the following | ||
| methods: | ||
| * ``__init__(self, *args, **kwargs)`` | ||
| * ``transitions(self, year)`` - this is expected to return a tuple of | ||
| datetimes representing the DST on and off transitions in standard | ||
| time. | ||
| A fully initialized ``tzrangebase`` subclass should also provide the | ||
| following attributes: | ||
| * ``hasdst``: Boolean whether or not the zone uses DST. | ||
| * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects | ||
| representing the respective UTC offsets. | ||
| * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short | ||
| abbreviations in DST and STD, respectively. | ||
| * ``_hasdst``: Whether or not the zone has DST. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| def __init__(self): | ||
| raise NotImplementedError('tzrangebase is an abstract base class') | ||
| def utcoffset(self, dt): | ||
| isdst = self._isdst(dt) | ||
| if isdst is None: | ||
| return None | ||
| elif isdst: | ||
| return self._dst_offset | ||
| else: | ||
| return self._std_offset | ||
| def dst(self, dt): | ||
| isdst = self._isdst(dt) | ||
| if isdst is None: | ||
| return None | ||
| elif isdst: | ||
| return self._dst_base_offset | ||
| else: | ||
| return ZERO | ||
| @tzname_in_python2 | ||
| def tzname(self, dt): | ||
| if self._isdst(dt): | ||
| return self._dst_abbr | ||
| else: | ||
| return self._std_abbr | ||
| def fromutc(self, dt): | ||
| """ Given a datetime in UTC, return local time """ | ||
| if not isinstance(dt, datetime): | ||
| raise TypeError("fromutc() requires a datetime argument") | ||
| if dt.tzinfo is not self: | ||
| raise ValueError("dt.tzinfo is not self") | ||
| # Get transitions - if there are none, fixed offset | ||
| transitions = self.transitions(dt.year) | ||
| if transitions is None: | ||
| return dt + self.utcoffset(dt) | ||
| # Get the transition times in UTC | ||
| dston, dstoff = transitions | ||
| dston -= self._std_offset | ||
| dstoff -= self._std_offset | ||
| utc_transitions = (dston, dstoff) | ||
| dt_utc = dt.replace(tzinfo=None) | ||
| isdst = self._naive_isdst(dt_utc, utc_transitions) | ||
| if isdst: | ||
| dt_wall = dt + self._dst_offset | ||
| else: | ||
| dt_wall = dt + self._std_offset | ||
| _fold = int(not isdst and self.is_ambiguous(dt_wall)) | ||
| return enfold(dt_wall, fold=_fold) | ||
| def is_ambiguous(self, dt): | ||
| """ | ||
| Whether or not the "wall time" of a given datetime is ambiguous in this | ||
| zone. | ||
| :param dt: | ||
| A :py:class:`datetime.datetime`, naive or time zone aware. | ||
| :return: | ||
| Returns ``True`` if ambiguous, ``False`` otherwise. | ||
| .. versionadded:: 2.6.0 | ||
| """ | ||
| if not self.hasdst: | ||
| return False | ||
| start, end = self.transitions(dt.year) | ||
| dt = dt.replace(tzinfo=None) | ||
| return (end <= dt < end + self._dst_base_offset) | ||
| def _isdst(self, dt): | ||
| if not self.hasdst: | ||
| return False | ||
| elif dt is None: | ||
| return None | ||
| transitions = self.transitions(dt.year) | ||
| if transitions is None: | ||
| return False | ||
| dt = dt.replace(tzinfo=None) | ||
| isdst = self._naive_isdst(dt, transitions) | ||
| # Handle ambiguous dates | ||
| if not isdst and self.is_ambiguous(dt): | ||
| return not self._fold(dt) | ||
| else: | ||
| return isdst | ||
| def _naive_isdst(self, dt, transitions): | ||
| dston, dstoff = transitions | ||
| dt = dt.replace(tzinfo=None) | ||
| if dston < dstoff: | ||
| isdst = dston <= dt < dstoff | ||
| else: | ||
| isdst = not dstoff <= dt < dston | ||
| return isdst | ||
| @property | ||
| def _dst_base_offset(self): | ||
| return self._dst_offset - self._std_offset | ||
| __hash__ = None | ||
| def __ne__(self, other): | ||
| return not (self == other) | ||
| def __repr__(self): | ||
| return "%s(...)" % self.__class__.__name__ | ||
| __reduce__ = object.__reduce__ |
| from datetime import timedelta | ||
| import weakref | ||
| from collections import OrderedDict | ||
| from six.moves import _thread | ||
| class _TzSingleton(type): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instance = None | ||
| super(_TzSingleton, cls).__init__(*args, **kwargs) | ||
| def __call__(cls): | ||
| if cls.__instance is None: | ||
| cls.__instance = super(_TzSingleton, cls).__call__() | ||
| return cls.__instance | ||
| class _TzFactory(type): | ||
| def instance(cls, *args, **kwargs): | ||
| """Alternate constructor that returns a fresh instance""" | ||
| return type.__call__(cls, *args, **kwargs) | ||
| class _TzOffsetFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
| cls._cache_lock = _thread.allocate_lock() | ||
| def __call__(cls, name, offset): | ||
| if isinstance(offset, timedelta): | ||
| key = (name, offset.total_seconds()) | ||
| else: | ||
| key = (name, offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(name, offset)) | ||
| # This lock may not be necessary in Python 3. See GH issue #901 | ||
| with cls._cache_lock: | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
| class _TzStrFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
| cls.__cache_lock = _thread.allocate_lock() | ||
| def __call__(cls, s, posix_offset=False): | ||
| key = (s, posix_offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(s, posix_offset)) | ||
| # This lock may not be necessary in Python 3. See GH issue #901 | ||
| with cls.__cache_lock: | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
-1849
| # -*- 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 weakref | ||
| from collections import OrderedDict | ||
| 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 | ||
| # For warning about rounding tzinfo | ||
| from warnings import warn | ||
| 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__ | ||
| #: Convenience constant providing a :class:`tzutc()` instance | ||
| #: | ||
| #: .. versionadded:: 2.7.0 | ||
| UTC = tzutc() | ||
| @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=_get_supported_offset(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 that 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 | ||
| <https://data.iana.org/time-zones/tz-link.html>`_ for more information. | ||
| Time zone files can be compiled from the `IANA Time Zone database files | ||
| <https://www.iana.org/time-zones>`_ with the `zic time zone compiler | ||
| <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ | ||
| .. 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 = _nullcontext(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] | ||
| gmtoff = _get_supported_offset(gmtoff) | ||
| 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. | ||
| lastdst = None | ||
| lastoffset = None | ||
| lastdstoffset = None | ||
| lastbaseoffset = None | ||
| out.trans_list = [] | ||
| for i, tti in enumerate(out.trans_idx): | ||
| offset = tti.offset | ||
| dstoffset = 0 | ||
| if lastdst is not None: | ||
| if tti.isdst: | ||
| if not lastdst: | ||
| dstoffset = offset - lastoffset | ||
| if not dstoffset and lastdstoffset: | ||
| dstoffset = lastdstoffset | ||
| tti.dstoffset = datetime.timedelta(seconds=dstoffset) | ||
| lastdstoffset = dstoffset | ||
| # If a time zone changes its base offset during a DST transition, | ||
| # then you need to adjust by the previous base offset to get the | ||
| # transition time in local time. Otherwise you use the current | ||
| # base offset. Ideally, I would have some mathematical proof of | ||
| # why this is true, but I haven't really thought about it enough. | ||
| baseoffset = offset - dstoffset | ||
| adjustment = baseoffset | ||
| if (lastbaseoffset is not None and baseoffset != lastbaseoffset | ||
| and tti.isdst != lastdst): | ||
| # The base DST has changed | ||
| adjustment = lastbaseoffset | ||
| lastdst = tti.isdst | ||
| lastoffset = offset | ||
| lastbaseoffset = baseoffset | ||
| out.trans_list.append(out.trans_list_utc[i] + adjustment) | ||
| out.trans_idx = tuple(out.trans_idx) | ||
| out.trans_list = tuple(out.trans_list) | ||
| 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 "<tzicalvtz %s>" % 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 = _nullcontext(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 = weakref.WeakValueDictionary() | ||
| self.__strong_cache_size = 8 | ||
| self.__strong_cache = OrderedDict() | ||
| 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) | ||
| or rv is None): | ||
| # tzlocal is slightly more complicated than the other | ||
| # time zone providers because it depends on environment | ||
| # at construction time, so don't cache that. | ||
| # | ||
| # We also cannot store weak references to None, so we | ||
| # will also not store that. | ||
| self.__instances[name] = rv | ||
| else: | ||
| # No need for strong caching, return immediately | ||
| return rv | ||
| self.__strong_cache[name] = self.__strong_cache.pop(name, rv) | ||
| if len(self.__strong_cache) > self.__strong_cache_size: | ||
| self.__strong_cache.popitem(last=False) | ||
| return rv | ||
| def set_cache_size(self, size): | ||
| with self._cache_lock: | ||
| self.__strong_cache_size = size | ||
| while len(self.__strong_cache) > size: | ||
| self.__strong_cache.popitem(last=False) | ||
| def cache_clear(self): | ||
| with self._cache_lock: | ||
| self.__instances = weakref.WeakValueDictionary() | ||
| self.__strong_cache.clear() | ||
| @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 in ("", ":"): | ||
| 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: | ||
| try: | ||
| if name.startswith(":"): | ||
| name = name[1:] | ||
| except TypeError as e: | ||
| if isinstance(name, bytes): | ||
| new_msg = "gettz argument should be str, not bytes" | ||
| six.raise_from(TypeError(new_msg), e) | ||
| else: | ||
| raise | ||
| 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, UnicodeEncodeError): | ||
| # UnicodeEncodeError is for Python 2.7 compat | ||
| 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 = UTC | ||
| 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(UTC).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() | ||
| if sys.version_info >= (3, 6): | ||
| def _get_supported_offset(second_offset): | ||
| return second_offset | ||
| else: | ||
| def _get_supported_offset(second_offset): | ||
| # For python pre-3.6, round to full-minutes if that's not the case. | ||
| # Python's datetime doesn't accept sub-minute timezones. Check | ||
| # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 | ||
| # for some information. | ||
| old_offset = second_offset | ||
| calculated_offset = 60 * ((second_offset + 30) // 60) | ||
| return calculated_offset | ||
| try: | ||
| # Python 3.7 feature | ||
| from contextlib import nullcontext as _nullcontext | ||
| except ImportError: | ||
| class _nullcontext(object): | ||
| """ | ||
| Class for wrapping contexts so that they are passed through in a | ||
| with statement. | ||
| """ | ||
| def __init__(self, context): | ||
| self.context = context | ||
| def __enter__(self): | ||
| return self.context | ||
| def __exit__(*args, **kwargs): | ||
| pass | ||
| # vim:ts=4:sw=4:et |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module provides an interface to the native time zone data on Windows, | ||
| including :py:class:`datetime.tzinfo` implementations. | ||
| Attempting to import this module on a non-Windows platform will raise an | ||
| :py:obj:`ImportError`. | ||
| """ | ||
| # This code was originally contributed by Jeffrey Harris. | ||
| import datetime | ||
| import struct | ||
| from six.moves import winreg | ||
| from six import text_type | ||
| try: | ||
| import ctypes | ||
| from ctypes import wintypes | ||
| except ValueError: | ||
| # ValueError is raised on non-Windows systems for some horrible reason. | ||
| raise ImportError("Running tzwin on non-Windows system") | ||
| from ._common import tzrangebase | ||
| __all__ = ["tzwin", "tzwinlocal", "tzres"] | ||
| ONEWEEK = datetime.timedelta(7) | ||
| TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" | ||
| TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" | ||
| TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" | ||
| def _settzkeyname(): | ||
| handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) | ||
| try: | ||
| winreg.OpenKey(handle, TZKEYNAMENT).Close() | ||
| TZKEYNAME = TZKEYNAMENT | ||
| except WindowsError: | ||
| TZKEYNAME = TZKEYNAME9X | ||
| handle.Close() | ||
| return TZKEYNAME | ||
| TZKEYNAME = _settzkeyname() | ||
| class tzres(object): | ||
| """ | ||
| Class for accessing ``tzres.dll``, which contains timezone name related | ||
| resources. | ||
| .. versionadded:: 2.5.0 | ||
| """ | ||
| p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char | ||
| def __init__(self, tzres_loc='tzres.dll'): | ||
| # Load the user32 DLL so we can load strings from tzres | ||
| user32 = ctypes.WinDLL('user32') | ||
| # Specify the LoadStringW function | ||
| user32.LoadStringW.argtypes = (wintypes.HINSTANCE, | ||
| wintypes.UINT, | ||
| wintypes.LPWSTR, | ||
| ctypes.c_int) | ||
| self.LoadStringW = user32.LoadStringW | ||
| self._tzres = ctypes.WinDLL(tzres_loc) | ||
| self.tzres_loc = tzres_loc | ||
| def load_name(self, offset): | ||
| """ | ||
| Load a timezone name from a DLL offset (integer). | ||
| >>> from dateutil.tzwin import tzres | ||
| >>> tzr = tzres() | ||
| >>> print(tzr.load_name(112)) | ||
| 'Eastern Standard Time' | ||
| :param offset: | ||
| A positive integer value referring to a string from the tzres dll. | ||
| .. note:: | ||
| Offsets found in the registry are generally of the form | ||
| ``@tzres.dll,-114``. The offset in this case is 114, not -114. | ||
| """ | ||
| resource = self.p_wchar() | ||
| lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) | ||
| nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) | ||
| return resource[:nchar] | ||
| def name_from_string(self, tzname_str): | ||
| """ | ||
| Parse strings as returned from the Windows registry into the time zone | ||
| name as defined in the registry. | ||
| >>> from dateutil.tzwin import tzres | ||
| >>> tzr = tzres() | ||
| >>> print(tzr.name_from_string('@tzres.dll,-251')) | ||
| 'Dateline Daylight Time' | ||
| >>> print(tzr.name_from_string('Eastern Standard Time')) | ||
| 'Eastern Standard Time' | ||
| :param tzname_str: | ||
| A timezone name string as returned from a Windows registry key. | ||
| :return: | ||
| Returns the localized timezone string from tzres.dll if the string | ||
| is of the form `@tzres.dll,-offset`, else returns the input string. | ||
| """ | ||
| if not tzname_str.startswith('@'): | ||
| return tzname_str | ||
| name_splt = tzname_str.split(',-') | ||
| try: | ||
| offset = int(name_splt[1]) | ||
| except: | ||
| raise ValueError("Malformed timezone string.") | ||
| return self.load_name(offset) | ||
| class tzwinbase(tzrangebase): | ||
| """tzinfo class based on win32's timezones available in the registry.""" | ||
| def __init__(self): | ||
| raise NotImplementedError('tzwinbase is an abstract base class') | ||
| def __eq__(self, other): | ||
| # Compare on all relevant dimensions, including name. | ||
| if not isinstance(other, tzwinbase): | ||
| return NotImplemented | ||
| return (self._std_offset == other._std_offset and | ||
| self._dst_offset == other._dst_offset and | ||
| self._stddayofweek == other._stddayofweek and | ||
| self._dstdayofweek == other._dstdayofweek and | ||
| self._stdweeknumber == other._stdweeknumber and | ||
| self._dstweeknumber == other._dstweeknumber and | ||
| self._stdhour == other._stdhour and | ||
| self._dsthour == other._dsthour and | ||
| self._stdminute == other._stdminute and | ||
| self._dstminute == other._dstminute and | ||
| self._std_abbr == other._std_abbr and | ||
| self._dst_abbr == other._dst_abbr) | ||
| @staticmethod | ||
| def list(): | ||
| """Return a list of all time zones known to the system.""" | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| with winreg.OpenKey(handle, TZKEYNAME) as tzkey: | ||
| result = [winreg.EnumKey(tzkey, i) | ||
| for i in range(winreg.QueryInfoKey(tzkey)[0])] | ||
| return result | ||
| def display(self): | ||
| """ | ||
| Return the display name of the time zone. | ||
| """ | ||
| return self._display | ||
| def transitions(self, year): | ||
| """ | ||
| For a given year, get the DST on and off transition times, expressed | ||
| always on the standard time side. For zones with no transitions, this | ||
| function returns ``None``. | ||
| :param year: | ||
| The year whose transitions you would like to query. | ||
| :return: | ||
| Returns a :class:`tuple` of :class:`datetime.datetime` objects, | ||
| ``(dston, dstoff)`` for zones with an annual DST transition, or | ||
| ``None`` for fixed offset zones. | ||
| """ | ||
| if not self.hasdst: | ||
| return None | ||
| dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, | ||
| self._dsthour, self._dstminute, | ||
| self._dstweeknumber) | ||
| dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, | ||
| self._stdhour, self._stdminute, | ||
| self._stdweeknumber) | ||
| # Ambiguous dates default to the STD side | ||
| dstoff -= self._dst_base_offset | ||
| return dston, dstoff | ||
| def _get_hasdst(self): | ||
| return self._dstmonth != 0 | ||
| @property | ||
| def _dst_base_offset(self): | ||
| return self._dst_base_offset_ | ||
| class tzwin(tzwinbase): | ||
| """ | ||
| Time zone object created from the zone info in the Windows registry | ||
| These are similar to :py:class:`dateutil.tz.tzrange` objects in that | ||
| the time zone data is provided in the format of a single offset rule | ||
| for either 0 or 2 time zone transitions per year. | ||
| :param: name | ||
| The name of a Windows time zone key, e.g. "Eastern Standard Time". | ||
| The full list of keys can be retrieved with :func:`tzwin.list`. | ||
| """ | ||
| def __init__(self, name): | ||
| self._name = name | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) | ||
| with winreg.OpenKey(handle, tzkeyname) as tzkey: | ||
| keydict = valuestodict(tzkey) | ||
| self._std_abbr = keydict["Std"] | ||
| self._dst_abbr = keydict["Dlt"] | ||
| self._display = keydict["Display"] | ||
| # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm | ||
| tup = struct.unpack("=3l16h", keydict["TZI"]) | ||
| stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 | ||
| dstoffset = stdoffset-tup[2] # + DaylightBias * -1 | ||
| self._std_offset = datetime.timedelta(minutes=stdoffset) | ||
| self._dst_offset = datetime.timedelta(minutes=dstoffset) | ||
| # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs | ||
| # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx | ||
| (self._stdmonth, | ||
| self._stddayofweek, # Sunday = 0 | ||
| self._stdweeknumber, # Last = 5 | ||
| self._stdhour, | ||
| self._stdminute) = tup[4:9] | ||
| (self._dstmonth, | ||
| self._dstdayofweek, # Sunday = 0 | ||
| self._dstweeknumber, # Last = 5 | ||
| self._dsthour, | ||
| self._dstminute) = tup[12:17] | ||
| self._dst_base_offset_ = self._dst_offset - self._std_offset | ||
| self.hasdst = self._get_hasdst() | ||
| def __repr__(self): | ||
| return "tzwin(%s)" % repr(self._name) | ||
| def __reduce__(self): | ||
| return (self.__class__, (self._name,)) | ||
| class tzwinlocal(tzwinbase): | ||
| """ | ||
| Class representing the local time zone information in the Windows registry | ||
| While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` | ||
| module) to retrieve time zone information, ``tzwinlocal`` retrieves the | ||
| rules directly from the Windows registry and creates an object like | ||
| :class:`dateutil.tz.tzwin`. | ||
| Because Windows does not have an equivalent of :func:`time.tzset`, on | ||
| Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the | ||
| time zone settings *at the time that the process was started*, meaning | ||
| changes to the machine's time zone settings during the run of a program | ||
| on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. | ||
| Because ``tzwinlocal`` reads the registry directly, it is unaffected by | ||
| this issue. | ||
| """ | ||
| def __init__(self): | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
| with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: | ||
| keydict = valuestodict(tzlocalkey) | ||
| self._std_abbr = keydict["StandardName"] | ||
| self._dst_abbr = keydict["DaylightName"] | ||
| try: | ||
| tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, | ||
| sn=self._std_abbr) | ||
| with winreg.OpenKey(handle, tzkeyname) as tzkey: | ||
| _keydict = valuestodict(tzkey) | ||
| self._display = _keydict["Display"] | ||
| except OSError: | ||
| self._display = None | ||
| stdoffset = -keydict["Bias"]-keydict["StandardBias"] | ||
| dstoffset = stdoffset-keydict["DaylightBias"] | ||
| self._std_offset = datetime.timedelta(minutes=stdoffset) | ||
| self._dst_offset = datetime.timedelta(minutes=dstoffset) | ||
| # For reasons unclear, in this particular key, the day of week has been | ||
| # moved to the END of the SYSTEMTIME structure. | ||
| tup = struct.unpack("=8h", keydict["StandardStart"]) | ||
| (self._stdmonth, | ||
| self._stdweeknumber, # Last = 5 | ||
| self._stdhour, | ||
| self._stdminute) = tup[1:5] | ||
| self._stddayofweek = tup[7] | ||
| tup = struct.unpack("=8h", keydict["DaylightStart"]) | ||
| (self._dstmonth, | ||
| self._dstweeknumber, # Last = 5 | ||
| self._dsthour, | ||
| self._dstminute) = tup[1:5] | ||
| self._dstdayofweek = tup[7] | ||
| self._dst_base_offset_ = self._dst_offset - self._std_offset | ||
| self.hasdst = self._get_hasdst() | ||
| def __repr__(self): | ||
| return "tzwinlocal()" | ||
| def __str__(self): | ||
| # str will return the standard name, not the daylight name. | ||
| return "tzwinlocal(%s)" % repr(self._std_abbr) | ||
| def __reduce__(self): | ||
| return (self.__class__, ()) | ||
| def picknthweekday(year, month, dayofweek, hour, minute, whichweek): | ||
| """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ | ||
| first = datetime.datetime(year, month, 1, hour, minute) | ||
| # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), | ||
| # Because 7 % 7 = 0 | ||
| weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) | ||
| wd = weekdayone + ((whichweek - 1) * ONEWEEK) | ||
| if (wd.month != month): | ||
| wd -= ONEWEEK | ||
| return wd | ||
| def valuestodict(key): | ||
| """Convert a registry key's values to a dictionary.""" | ||
| dout = {} | ||
| size = winreg.QueryInfoKey(key)[1] | ||
| tz_res = None | ||
| for i in range(size): | ||
| key_name, value, dtype = winreg.EnumValue(key, i) | ||
| if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: | ||
| # If it's a DWORD (32-bit integer), it's stored as unsigned - convert | ||
| # that to a proper signed integer | ||
| if value & (1 << 31): | ||
| value = value - (1 << 32) | ||
| elif dtype == winreg.REG_SZ: | ||
| # If it's a reference to the tzres DLL, load the actual string | ||
| if value.startswith('@tzres'): | ||
| tz_res = tz_res or tzres() | ||
| value = tz_res.name_from_string(value) | ||
| value = value.rstrip('\x00') # Remove trailing nulls | ||
| dout[key_name] = value | ||
| return dout |
| # tzwin has moved to dateutil.tz.win | ||
| from .tz.win import * |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers general convenience and utility functions for dealing with | ||
| datetimes. | ||
| .. versionadded:: 2.7.0 | ||
| """ | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, time | ||
| def today(tzinfo=None): | ||
| """ | ||
| Returns a :py:class:`datetime` representing the current day at midnight | ||
| :param tzinfo: | ||
| The time zone to attach (also used to determine the current day). | ||
| :return: | ||
| A :py:class:`datetime.datetime` object representing the current day | ||
| at midnight. | ||
| """ | ||
| dt = datetime.now(tzinfo) | ||
| return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) | ||
| def default_tzinfo(dt, tzinfo): | ||
| """ | ||
| Sets the ``tzinfo`` parameter on naive datetimes only | ||
| This is useful for example when you are provided a datetime that may have | ||
| either an implicit or explicit time zone, such as when parsing a time zone | ||
| string. | ||
| .. doctest:: | ||
| >>> from dateutil.tz import tzoffset | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.utils import default_tzinfo | ||
| >>> dflt_tz = tzoffset("EST", -18000) | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) | ||
| 2014-01-01 12:30:00+00:00 | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) | ||
| 2014-01-01 12:30:00-05:00 | ||
| :param dt: | ||
| The datetime on which to replace the time zone | ||
| :param tzinfo: | ||
| The :py:class:`datetime.tzinfo` subclass instance to assign to | ||
| ``dt`` if (and only if) it is naive. | ||
| :return: | ||
| Returns an aware :py:class:`datetime.datetime`. | ||
| """ | ||
| if dt.tzinfo is not None: | ||
| return dt | ||
| else: | ||
| return dt.replace(tzinfo=tzinfo) | ||
| def within_delta(dt1, dt2, delta): | ||
| """ | ||
| Useful for comparing two datetimes that may have a negligible difference | ||
| to be considered equal. | ||
| """ | ||
| delta = abs(delta) | ||
| difference = dt1 - dt2 | ||
| return -delta <= difference <= delta |
| # -*- 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 |
Sorry, the diff of this file is not supported yet
| import logging | ||
| import os | ||
| import tempfile | ||
| import shutil | ||
| import json | ||
| from subprocess import check_call, check_output | ||
| from tarfile import TarFile | ||
| from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME | ||
| def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): | ||
| """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* | ||
| filename is the timezone tarball from ``ftp.iana.org/tz``. | ||
| """ | ||
| tmpdir = tempfile.mkdtemp() | ||
| zonedir = os.path.join(tmpdir, "zoneinfo") | ||
| moduledir = os.path.dirname(__file__) | ||
| try: | ||
| with TarFile.open(filename) as tf: | ||
| for name in zonegroups: | ||
| tf.extract(name, tmpdir) | ||
| filepaths = [os.path.join(tmpdir, n) for n in zonegroups] | ||
| _run_zic(zonedir, filepaths) | ||
| # write metadata file | ||
| with open(os.path.join(zonedir, METADATA_FN), 'w') as f: | ||
| json.dump(metadata, f, indent=4, sort_keys=True) | ||
| target = os.path.join(moduledir, ZONEFILENAME) | ||
| with TarFile.open(target, "w:%s" % format) as tf: | ||
| for entry in os.listdir(zonedir): | ||
| entrypath = os.path.join(zonedir, entry) | ||
| tf.add(entrypath, entry) | ||
| finally: | ||
| shutil.rmtree(tmpdir) | ||
| def _run_zic(zonedir, filepaths): | ||
| """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. | ||
| Recent versions of ``zic`` default to ``-b slim``, while older versions | ||
| don't even have the ``-b`` option (but default to "fat" binaries). The | ||
| current version of dateutil does not support Version 2+ TZif files, which | ||
| causes problems when used in conjunction with "slim" binaries, so this | ||
| function is used to ensure that we always get a "fat" binary. | ||
| """ | ||
| try: | ||
| help_text = check_output(["zic", "--help"]) | ||
| except OSError as e: | ||
| _print_on_nosuchfile(e) | ||
| raise | ||
| if b"-b " in help_text: | ||
| bloat_args = ["-b", "fat"] | ||
| else: | ||
| bloat_args = [] | ||
| check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) | ||
| def _print_on_nosuchfile(e): | ||
| """Print helpful troubleshooting message | ||
| e is an exception raised by subprocess.check_call() | ||
| """ | ||
| if e.errno == 2: | ||
| logging.error( | ||
| "Could not find zic. Perhaps you need to install " | ||
| "libc-bin or some other package that provides it, " | ||
| "or it's not in your PATH?") |
| Metadata-Version: 2.1 | ||
| Name: python-dateutil | ||
| Version: 2.8.2 | ||
| Summary: Extensions to the standard Python datetime module | ||
| Home-page: https://github.com/dateutil/dateutil | ||
| Author: Gustavo Niemeyer | ||
| Author-email: gustavo@niemeyer.net | ||
| Maintainer: Paul Ganssle | ||
| Maintainer-email: dateutil@python.org | ||
| License: Dual License | ||
| Project-URL: Documentation, https://dateutil.readthedocs.io/en/stable/ | ||
| Project-URL: Source, https://github.com/dateutil/dateutil | ||
| Platform: UNKNOWN | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: License :: OSI Approved :: Apache Software License | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 2 | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.3 | ||
| Classifier: Programming Language :: Python :: 3.4 | ||
| Classifier: Programming Language :: Python :: 3.5 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,>=2.7 | ||
| Description-Content-Type: text/x-rst | ||
| License-File: LICENSE | ||
| dateutil - powerful extensions to datetime | ||
| ========================================== | ||
| |pypi| |support| |licence| | ||
| |gitter| |readthedocs| | ||
| |travis| |appveyor| |pipelines| |coverage| | ||
| .. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: pypi version | ||
| .. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: supported Python version | ||
| .. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build | ||
| :target: https://travis-ci.org/dateutil/dateutil | ||
| :alt: travis build status | ||
| .. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor | ||
| :target: https://ci.appveyor.com/project/dateutil/dateutil | ||
| :alt: appveyor build status | ||
| .. |pipelines| image:: https://dev.azure.com/pythondateutilazure/dateutil/_apis/build/status/dateutil.dateutil?branchName=master | ||
| :target: https://dev.azure.com/pythondateutilazure/dateutil/_build/latest?definitionId=1&branchName=master | ||
| :alt: azure pipelines build status | ||
| .. |coverage| image:: https://codecov.io/gh/dateutil/dateutil/branch/master/graphs/badge.svg?branch=master | ||
| :target: https://codecov.io/gh/dateutil/dateutil?branch=master | ||
| :alt: Code coverage | ||
| .. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg | ||
| :alt: Join the chat at https://gitter.im/dateutil/dateutil | ||
| :target: https://gitter.im/dateutil/dateutil | ||
| .. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.org/project/python-dateutil/ | ||
| :alt: licence | ||
| .. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs | ||
| :alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/ | ||
| :target: https://dateutil.readthedocs.io/en/latest/ | ||
| The `dateutil` module provides powerful extensions to | ||
| the standard `datetime` module, available in Python. | ||
| Installation | ||
| ============ | ||
| `dateutil` can be installed from PyPI using `pip` (note that the package name is | ||
| different from the importable name):: | ||
| pip install python-dateutil | ||
| Download | ||
| ======== | ||
| dateutil is available on PyPI | ||
| https://pypi.org/project/python-dateutil/ | ||
| The documentation is hosted at: | ||
| https://dateutil.readthedocs.io/en/stable/ | ||
| Code | ||
| ==== | ||
| The code and issue tracker are hosted on GitHub: | ||
| https://github.com/dateutil/dateutil/ | ||
| Features | ||
| ======== | ||
| * Computing of relative deltas (next month, next year, | ||
| next Monday, last week of month, etc); | ||
| * Computing of relative deltas between two given | ||
| date and/or datetime objects; | ||
| * Computing of dates based on very flexible recurrence rules, | ||
| using a superset of the `iCalendar <https://www.ietf.org/rfc/rfc2445.txt>`_ | ||
| specification. Parsing of RFC strings is supported as well. | ||
| * Generic parsing of dates in almost any string format; | ||
| * Timezone (tzinfo) implementations for tzfile(5) format | ||
| files (/etc/localtime, /usr/share/zoneinfo, etc), TZ | ||
| environment string (in all known formats), iCalendar | ||
| format files, given ranges (with help from relative deltas), | ||
| local machine timezone, fixed offset timezone, UTC timezone, | ||
| and Windows registry-based time zones. | ||
| * Internal up-to-date world timezone information based on | ||
| Olson's database. | ||
| * Computing of Easter Sunday dates for any given year, | ||
| using Western, Orthodox or Julian algorithms; | ||
| * A comprehensive test suite. | ||
| Quick example | ||
| ============= | ||
| Here's a snapshot, just to give an idea about the power of the | ||
| package. For more examples, look at the documentation. | ||
| Suppose you want to know how much time is left, in | ||
| years/months/days/etc, before the next easter happening on a | ||
| year with a Friday 13th in August, and you want to get today's | ||
| date out of the "date" unix system command. Here is the code: | ||
| .. code-block:: python3 | ||
| >>> from dateutil.relativedelta import * | ||
| >>> from dateutil.easter import * | ||
| >>> from dateutil.rrule import * | ||
| >>> from dateutil.parser import * | ||
| >>> from datetime import * | ||
| >>> now = parse("Sat Oct 11 17:13:46 UTC 2003") | ||
| >>> today = now.date() | ||
| >>> year = rrule(YEARLY,dtstart=now,bymonth=8,bymonthday=13,byweekday=FR)[0].year | ||
| >>> rdelta = relativedelta(easter(year), today) | ||
| >>> print("Today is: %s" % today) | ||
| Today is: 2003-10-11 | ||
| >>> print("Year with next Aug 13th on a Friday is: %s" % year) | ||
| Year with next Aug 13th on a Friday is: 2004 | ||
| >>> print("How far is the Easter of that year: %s" % rdelta) | ||
| How far is the Easter of that year: relativedelta(months=+6) | ||
| >>> print("And the Easter of that year is: %s" % (today+rdelta)) | ||
| And the Easter of that year is: 2004-04-11 | ||
| Being exactly 6 months ahead was **really** a coincidence :) | ||
| Contributing | ||
| ============ | ||
| We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository. | ||
| Author | ||
| ====== | ||
| The dateutil module was written by Gustavo Niemeyer <gustavo@niemeyer.net> | ||
| in 2003. | ||
| It is maintained by: | ||
| * Gustavo Niemeyer <gustavo@niemeyer.net> 2003-2011 | ||
| * Tomi Pieviläinen <tomi.pievilainen@iki.fi> 2012-2014 | ||
| * Yaron de Leeuw <me@jarondl.net> 2014-2016 | ||
| * Paul Ganssle <paul@ganssle.io> 2015- | ||
| Starting with version 2.4.1 and running until 2.8.2, all source and binary | ||
| distributions will be signed by a PGP key that has, at the very least, been | ||
| signed by the key which made the previous release. A table of release signing | ||
| keys can be found below: | ||
| =========== ============================ | ||
| Releases Signing key fingerprint | ||
| =========== ============================ | ||
| 2.4.1-2.8.2 `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ | ||
| =========== ============================ | ||
| New releases *may* have signed tags, but binary and source distributions | ||
| uploaded to PyPI will no longer have GPG signatures attached. | ||
| Contact | ||
| ======= | ||
| Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of | ||
| conduct <https://www.python.org/psf/conduct/>`_. | ||
| License | ||
| ======= | ||
| All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License. | ||
| .. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB: | ||
| https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB | ||
| six>=1.5 |
| .gitattributes | ||
| .gitignore | ||
| .travis.yml | ||
| AUTHORS.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| MANIFEST.in | ||
| NEWS | ||
| README.rst | ||
| RELEASING | ||
| appveyor.yml | ||
| azure-pipelines.yml | ||
| codecov.yml | ||
| pyproject.toml | ||
| requirements-dev.txt | ||
| setup.cfg | ||
| setup.py | ||
| tox.ini | ||
| updatezinfo.py | ||
| zonefile_metadata.json | ||
| .github/pull_request_template.md | ||
| .github/workflows/publish.yml | ||
| .github/workflows/validate.yml | ||
| changelog.d/.gitignore | ||
| changelog.d/template.rst | ||
| ci_tools/make_zonefile_metadata.py | ||
| ci_tools/retry.bat | ||
| ci_tools/retry.sh | ||
| ci_tools/run_tz_master_env.sh | ||
| dateutil/__init__.py | ||
| dateutil/_common.py | ||
| dateutil/_version.py | ||
| dateutil/easter.py | ||
| dateutil/relativedelta.py | ||
| dateutil/rrule.py | ||
| dateutil/tzwin.py | ||
| dateutil/utils.py | ||
| dateutil/parser/__init__.py | ||
| dateutil/parser/_parser.py | ||
| dateutil/parser/isoparser.py | ||
| dateutil/test/__init__.py | ||
| dateutil/test/_common.py | ||
| dateutil/test/conftest.py | ||
| dateutil/test/test_easter.py | ||
| dateutil/test/test_import_star.py | ||
| dateutil/test/test_imports.py | ||
| dateutil/test/test_internals.py | ||
| dateutil/test/test_isoparser.py | ||
| dateutil/test/test_parser.py | ||
| dateutil/test/test_relativedelta.py | ||
| dateutil/test/test_rrule.py | ||
| dateutil/test/test_tz.py | ||
| dateutil/test/test_utils.py | ||
| dateutil/test/property/test_isoparse_prop.py | ||
| dateutil/test/property/test_parser_prop.py | ||
| dateutil/test/property/test_tz_prop.py | ||
| dateutil/tz/__init__.py | ||
| dateutil/tz/_common.py | ||
| dateutil/tz/_factories.py | ||
| dateutil/tz/tz.py | ||
| dateutil/tz/win.py | ||
| dateutil/zoneinfo/__init__.py | ||
| dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | ||
| dateutil/zoneinfo/rebuild.py | ||
| docs/Makefile | ||
| docs/changelog.rst | ||
| docs/conf.py | ||
| docs/easter.rst | ||
| docs/examples.rst | ||
| docs/index.rst | ||
| docs/make.bat | ||
| docs/parser.rst | ||
| docs/relativedelta.rst | ||
| docs/requirements-docs.txt | ||
| docs/rrule.rst | ||
| docs/tz.rst | ||
| docs/tzwin.rst | ||
| docs/utils.rst | ||
| docs/zoneinfo.rst | ||
| docs/exercises/index.rst | ||
| docs/exercises/solutions/mlk-day-rrule.rst | ||
| docs/exercises/solutions/mlk_day_rrule_solution.py | ||
| docs/samples/EST5EDT.ics | ||
| python_dateutil.egg-info/PKG-INFO | ||
| python_dateutil.egg-info/SOURCES.txt | ||
| python_dateutil.egg-info/dependency_links.txt | ||
| python_dateutil.egg-info/requires.txt | ||
| python_dateutil.egg-info/top_level.txt | ||
| python_dateutil.egg-info/zip-safe | ||
| requirements/3.3/constraints.txt | ||
| requirements/3.3/requirements-dev.txt |
| dateutil |
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
94
2.17%15538
0.86%1078120
-0.81%