Merge pull request #510 from JackDandy/feature/UpdateDateutil

Feature/update dateutil
This commit is contained in:
JackDandy 2015-09-13 16:53:43 +01:00
commit 30b569aeb1
8 changed files with 263 additions and 175 deletions

View file

@ -15,6 +15,7 @@
* Remove legacy anime split home option from anime settings tab (new option located in general/interface tab) * Remove legacy anime split home option from anime settings tab (new option located in general/interface tab)
* Remove "Manage Torrents" * Remove "Manage Torrents"
* Update Beautiful Soup 4.3.2 to 4.4.0 (r390) * Update Beautiful Soup 4.3.2 to 4.4.0 (r390)
* Update dateutil library to 2.4.2 (083f666)
* Update Hachoir library 1.3.3 to 1.3.4 (r1383) * Update Hachoir library 1.3.3 to 1.3.4 (r1383)
* Change configure quiet option in Hachoir to suppress warnings (add ref:hacks.txt) * Change configure quiet option in Hachoir to suppress warnings (add ref:hacks.txt)
* Add parse media content to determine quality before making final assumptions during re-scan, update, pp * Add parse media content to determine quality before making final assumptions during re-scan, update, pp

View file

@ -4,28 +4,29 @@ This module offers a generic date/time string parser which is able to parse
most known formats to represent a date and/or time. most known formats to represent a date and/or time.
This module attempts to be forgiving with regards to unlikely input formats, 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 returning a datetime object even for dates which are ambiguous. If an element
a date/time stamp is omitted, the following rules are applied: 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 - 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 on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
specified. specified.
- If a time zone is omitted, it is assumed to be UTC. - If a time zone is omitted, a timezone-naive datetime is returned.
If any other elements are missing, they are taken from the `datetime.datetime` If any other elements are missing, they are taken from the
object passed to the parameter `default`. If this results in a day number :class:`datetime.datetime` object passed to the parameter ``default``. If this
exceeding the valid number of days per month, one can fall back to the last results in a day number exceeding the valid number of days per month, one can
day of the month by setting `fallback_on_invalid_day` parameter to `True`. fall back to the last day of the month by setting ``fallback_on_invalid_day``
parameter to ``True``.
Also provided is the `smart_defaults` option, which attempts to fill in the Also provided is the ``smart_defaults`` option, which attempts to fill in the
missing elements from context. If specified, the logic is: missing elements from context. If specified, the logic is:
- If the omitted element is smaller than the largest specified element, select - If the omitted element is smaller than the largest specified element, select
the *earliest* time matching the specified conditions; so `"June 2010"` is the *earliest* time matching the specified conditions; so ``"June 2010"`` is
interpreted as `June 1, 2010 0:00:00`) and the (somewhat strange) interpreted as ``June 1, 2010 0:00:00``) and the (somewhat strange)
`"Feb 1997 3:15 PM"` is interpreted as `February 1, 1997 15:15:00`. ``"Feb 1997 3:15 PM"`` is interpreted as ``February 1, 1997 15:15:00``.
- If the element is larger than the largest specified element, select the - If the element is larger than the largest specified element, select the
*most recent* time matching the specified conditions (e.g parsing `"May"` *most recent* time matching the specified conditions (e.g parsing ``"May"``
in June 2015 returns the date May 1st, 2015, whereas parsing it in April 2015 in June 2015 returns the date May 1st, 2015, whereas parsing it in April 2015
returns May 1st 2014). If using the `date_in_future` flag, this logic is returns May 1st 2014). If using the ``date_in_future`` flag, this logic is
inverted, and instead the *next* time matching the specified conditions is inverted, and instead the *next* time matching the specified conditions is
returned. returned.
@ -46,6 +47,7 @@ import datetime
import string import string
import time import time
import collections import collections
import re
from io import StringIO from io import StringIO
from calendar import monthrange, isleap from calendar import monthrange, isleap
@ -58,6 +60,9 @@ __all__ = ["parse", "parserinfo"]
class _timelex(object): class _timelex(object):
# Fractional seconds are sometimes split by a comma
_split_decimal = re.compile("([\.,])")
def __init__(self, instream): def __init__(self, instream):
if isinstance(instream, binary_type): if isinstance(instream, binary_type):
instream = instream.decode() instream = instream.decode()
@ -80,8 +85,8 @@ class _timelex(object):
""" """
This function breaks the time string into lexical units (tokens), which This function breaks the time string into lexical units (tokens), which
can be parsed by the parser. Lexical units are demarcated by changes in 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 the character set, so any continuous string of letters is considered
unit, any continuous string of numbers is considered one unit. one unit, any continuous string of numbers is considered one unit.
The main complication arises from the fact that dots ('.') can be used 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. both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
@ -101,9 +106,9 @@ class _timelex(object):
whitespace = self.whitespace whitespace = self.whitespace
while not self.eof: while not self.eof:
# We only realize that we've reached the end of a token when we find # We only realize that we've reached the end of a token when we
# a character that's not part of the current token - since that # find a character that's not part of the current token - since
# character may be part of the next token, it's stored in the # that character may be part of the next token, it's stored in the
# charstack. # charstack.
if self.charstack: if self.charstack:
nextchar = self.charstack.pop(0) nextchar = self.charstack.pop(0)
@ -145,7 +150,7 @@ class _timelex(object):
# numbers until we find something that doesn't fit. # numbers until we find something that doesn't fit.
if nextchar in numchars: if nextchar in numchars:
token += nextchar token += nextchar
elif nextchar == '.': elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
token += nextchar token += nextchar
state = '0.' state = '0.'
else: else:
@ -176,14 +181,16 @@ class _timelex(object):
break # emit token break # emit token
if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
token[-1] == '.')): token[-1] in '.,')):
l = token.split('.') l = self._split_decimal.split(token)
token = l[0] token = l[0]
for tok in l[1:]: for tok in l[1:]:
self.tokenstack.append('.')
if tok: if tok:
self.tokenstack.append(tok) self.tokenstack.append(tok)
if state == '0.' and token.count('.') == 0:
token = token.replace(',', '.')
return token return token
def __iter__(self): def __iter__(self):
@ -224,20 +231,20 @@ class _resultbase(object):
class parserinfo(object): class parserinfo(object):
""" """
Class which handles what inputs are accepted. Subclass this to customize the Class which handles what inputs are accepted. Subclass this to customize
language and acceptable values for each parameter. the language and acceptable values for each parameter.
:param dayfirst: :param dayfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
`yearfirst` is set to `True`, this distinguishes between YDM and ``yearfirst`` is set to ``True``, this distinguishes between YDM
YMD. Default is `False`. and YMD. Default is ``False``.
:param yearfirst: :param yearfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (e.g. 01/05/09) as the year. If ``True``, the first number is taken
be the year, otherwise the last number is taken to be the year. to be the year, otherwise the last number is taken to be the year.
Default is `False`. Default is ``False``.
""" """
# m from a.m/p.m, t from ISO T separator # m from a.m/p.m, t from ISO T separator
@ -287,7 +294,7 @@ class parserinfo(object):
self.smart_defaults = smart_defaults self.smart_defaults = smart_defaults
self._year = time.localtime().tm_year self._year = time.localtime().tm_year
self._century = self._year // 100*100 self._century = self._year // 100 * 100
def _convert(self, lst): def _convert(self, lst):
dct = {} dct = {}
@ -313,7 +320,7 @@ class parserinfo(object):
def month(self, name): def month(self, name):
if len(name) >= 3: if len(name) >= 3:
try: try:
return self._months[name.lower()]+1 return self._months[name.lower()] + 1
except KeyError: except KeyError:
pass pass
return None return None
@ -345,7 +352,7 @@ class parserinfo(object):
def convertyear(self, year): def convertyear(self, year):
if year < 100: if year < 100:
year += self._century year += self._century
if abs(year-self._year) >= 50: if abs(year - self._year) >= 50:
if year < self._year: if year < self._year:
year += 100 year += 100
else: else:
@ -373,65 +380,87 @@ class parser(object):
smart_defaults=None, date_in_future=False, smart_defaults=None, date_in_future=False,
fallback_on_invalid_day=None, **kwargs): fallback_on_invalid_day=None, **kwargs):
""" """
Parse the date/time string into a datetime object. Parse the date/time string into a :class:`datetime.datetime` object.
:param timestr: :param timestr:
Any date/time string using the supported formats. Any date/time string using the supported formats.
:param default: :param default:
The default datetime object, if this is a datetime object and not The default datetime object, if this is a datetime object and not
`None`, elements specified in `timestr` replace elements in the ``None``, elements specified in ``timestr`` replace elements in the
default object, unless `smart_defaults` is set to `True`, in which default object, unless ``smart_defaults`` is set to ``True``, in
case to the extent necessary, timestamps are calculated relative to which case to the extent necessary, timestamps are calculated
this date. relative to this date.
:param smart_defaults: :param smart_defaults:
If using smart defaults, the `default` parameter is treated as the If using smart defaults, the ``default`` parameter is treated as
effective parsing date/time, and the context of the datetime string the effective parsing date/time, and the context of the datetime
is determined relative to `default`. If `None`, this parameter is string is determined relative to ``default``. If ``None``, this
inherited from the :class:`parserinfo` object. parameter is inherited from the :class:`parserinfo` object.
:param date_in_future: :param date_in_future:
If `smart_defaults` is `True`, the parser assumes by default that If ``smart_defaults`` is ``True``, the parser assumes by default
the timestamp refers to a date in the past, and will return the that the timestamp refers to a date in the past, and will return
beginning of the most recent timespan which matches the time string the beginning of the most recent timespan which matches the time
(e.g. if `default` is March 3rd, 2013, "Feb" parses to string (e.g. if ``default`` is March 3rd, 2013, "Feb" parses to
"Feb 1, 2013" and "May 3" parses to May 3rd, 2012). Setting this "Feb 1, 2013" and "May 3" parses to May 3rd, 2012). Setting this
parameter to `True` inverts this assumption, and returns the parameter to ``True`` inverts this assumption, and returns the
beginning of the *next* matching timespan. beginning of the *next* matching timespan.
:param fallback_on_invalid_day: :param fallback_on_invalid_day:
If specified `True`, an otherwise invalid date such as "Feb 30" or If specified ``True``, an otherwise invalid date such as "Feb 30"
"June 32" falls back to the last day of the month. If specified as or "June 32" falls back to the last day of the month. If specified
"False", the parser is strict about parsing otherwise valid dates as "False", the parser is strict about parsing otherwise valid
that would turn up as invalid because of the fallback rules (e.g. dates that would turn up as invalid because of the fallback rules
"Feb 2010" run with a default of January 30, 2010 and `smartparser` (e.g. "Feb 2010" run with a default of January 30, 2010 and
set to `False` would would throw an error, rather than falling ``smartparser`` set to ``False`` would would throw an error, rather
back to the end of February). If `None` or unspecified, the date than falling back to the end of February). If ``None`` or
falls back to the most recent valid date only if the invalid date unspecified, the date falls back to the most recent valid date only
is created as a result of an unspecified day in the time string. if the invalid date is created as a result of an unspecified day in
the time string.
:param ignoretz: :param ignoretz:
Whether or not to ignore the time zone. If set ``True``, time zones in parsed strings are ignored and a
naive :class:`datetime.datetime` object is returned.
:param tzinfos: :param tzinfos:
A time zone, to be applied to the date, if `ignoretz` is `True`. Additional time zone names / aliases which may be present in the
This can be either a subclass of `tzinfo`, a time zone string or an string. This argument maps time zone names (and optionally offsets
integer offset. 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 minutes or a :class:`tzinfo` object.
.. doctest::
:options: +NORMALIZE_WHITESPACE
>>> from dateutil.parser import parse
>>> from dateutil.tz import gettz
>>> tzinfos = {"BRST": -10800, "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', -10800))
>>> 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: :param **kwargs:
Keyword arguments as passed to `_parse()`. Keyword arguments as passed to ``_parse()``.
:return: :return:
Returns a `datetime.datetime` object or, if the `fuzzy_with_tokens` Returns a :class:`datetime.datetime` object or, if the
option is `True`, returns a tuple, the first element being a ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
`datetime.datetime` object, the second a tuple containing the first element being a :class:`datetime.datetime` object, the second
fuzzy tokens. a tuple containing the fuzzy tokens.
:raises ValueError: :raises ValueError:
Raised for invalid or unknown string format, if the provided Raised for invalid or unknown string format, if the provided
`tzinfo` is not in a valid format, or if an invalid date would :class:`tzinfo` is not in a valid format, or if an invalid date
be created. would be created.
:raises OverFlowError: :raises OverFlowError:
Raised if the parsed date exceeds the largest valid C integer on Raised if the parsed date exceeds the largest valid C integer on
@ -444,14 +473,11 @@ class parser(object):
if default is None: if default is None:
effective_dt = datetime.datetime.now() effective_dt = datetime.datetime.now()
default = datetime.datetime.now().replace(hour=0, minute=0, default = datetime.datetime.now().replace(hour=0, minute=0,
second=0, microsecond=0) second=0, microsecond=0)
else: else:
effective_dt = default effective_dt = default
if kwargs.get('fuzzy_with_tokens', False): res, skipped_tokens = self._parse(timestr, **kwargs)
res, skipped_tokens = self._parse(timestr, **kwargs)
else:
res = self._parse(timestr, **kwargs)
if res is None: if res is None:
raise ValueError("Unknown string format") raise ValueError("Unknown string format")
@ -464,7 +490,7 @@ class parser(object):
repl[attr] = value repl[attr] = value
# Choose the correct fallback position if requested by the # Choose the correct fallback position if requested by the
# `smart_defaults` parameter. # ``smart_defaults`` parameter.
if smart_defaults: if smart_defaults:
# Determine if it refers to this year, last year or next year # Determine if it refers to this year, last year or next year
if res.year is None: if res.year is None:
@ -472,7 +498,7 @@ class parser(object):
# Explicitly deal with leap year problems # Explicitly deal with leap year problems
if res.month == 2 and (res.day is not None and if res.month == 2 and (res.day is not None and
res.day == 29): res.day == 29):
ly_offset = 4 if date_in_future else -4 ly_offset = 4 if date_in_future else -4
next_year = 4 * (default.year // 4) next_year = 4 * (default.year // 4)
@ -583,36 +609,42 @@ class parser(object):
fuzzy_with_tokens=False): fuzzy_with_tokens=False):
""" """
Private method which performs the heavy lifting of parsing, called from Private method which performs the heavy lifting of parsing, called from
`parse()`, which passes on its `kwargs` to this function. ``parse()``, which passes on its ``kwargs`` to this function.
:param timestr: :param timestr:
The string to parse. The string to parse.
:param dayfirst: :param dayfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
`yearfirst` is set to `True`, this distinguishes between YDM and ``yearfirst`` is set to ``True``, this distinguishes between YDM
YMD. If set to `None`, this value is retrieved from the current and YMD. If set to ``None``, this value is retrieved from the
`parserinfo` object (which itself defaults to `False`). current :class:`parserinfo` object (which itself defaults to
``False``).
:param yearfirst: :param yearfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (e.g. 01/05/09) as the year. If ``True``, the first number is taken
be the year, otherwise the last number is taken to be the year. If to be the year, otherwise the last number is taken to be the year.
this is set to `None`, the value is retrieved from the current If this is set to ``None``, the value is retrieved from the current
`parserinfo` object (which itself defaults to `False`). :class:`parserinfo` object (which itself defaults to ``False``).
:param fuzzy: :param fuzzy:
Whether to allow fuzzy parsing, allowing for string like "Today is Whether to allow fuzzy parsing, allowing for string like "Today is
January 1, 2047 at 8:21:00AM". January 1, 2047 at 8:21:00AM".
:param fuzzy_with_tokens: :param fuzzy_with_tokens:
If `True`, `fuzzy` is automatically set to True, and the parser will If ``True``, ``fuzzy`` is automatically set to True, and the parser
return a tuple where the first element is the parsed will return a tuple where the first element is the parsed
`datetime.datetime` datetimestamp and the second element is a tuple :class:`datetime.datetime` datetimestamp and the second element is
containing the portions of the string which were ignored, e.g. a tuple containing the portions of the string which were ignored:
"Today is January 1, 2047 at 8:21:00AM" should return
`(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))` .. doctest::
>>> from dateutil.parser import parse
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
""" """
if fuzzy_with_tokens: if fuzzy_with_tokens:
fuzzy = True fuzzy = True
@ -796,7 +828,7 @@ class parser(object):
assert mstridx == -1 assert mstridx == -1
mstridx = len(ymd)-1 mstridx = len(ymd)-1
else: else:
return None return None, None
i += 1 i += 1
@ -840,7 +872,7 @@ class parser(object):
i += 1 i += 1
elif not fuzzy: elif not fuzzy:
return None return None, None
else: else:
i += 1 i += 1
continue continue
@ -969,7 +1001,7 @@ class parser(object):
# -[0]3 # -[0]3
res.tzoffset = int(l[i][:2])*3600 res.tzoffset = int(l[i][:2])*3600
else: else:
return None return None, None
i += 1 i += 1
res.tzoffset *= signal res.tzoffset *= signal
@ -987,7 +1019,7 @@ class parser(object):
# Check jumps # Check jumps
if not (info.jump(l[i]) or fuzzy): if not (info.jump(l[i]) or fuzzy):
return None return None, None
if last_skipped_token_i == i - 1: if last_skipped_token_i == i - 1:
# recombine the tokens # recombine the tokens
@ -1002,7 +1034,7 @@ class parser(object):
len_ymd = len(ymd) len_ymd = len(ymd)
if len_ymd > 3: if len_ymd > 3:
# More than three members!? # More than three members!?
return None return None, None
elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
# One member, or two members with a month string # One member, or two members with a month string
if mstridx != -1: if mstridx != -1:
@ -1066,72 +1098,113 @@ class parser(object):
res.month, res.day, res.year = ymd res.month, res.day, res.year = ymd
except (IndexError, ValueError, AssertionError): except (IndexError, ValueError, AssertionError):
return None return None, None
if not info.validate(res): if not info.validate(res):
return None return None, None
if fuzzy_with_tokens: if fuzzy_with_tokens:
return res, tuple(skipped_tokens) return res, tuple(skipped_tokens)
else: else:
return res return res, None
DEFAULTPARSER = parser() DEFAULTPARSER = parser()
def parse(timestr, parserinfo=None, **kwargs): def parse(timestr, parserinfo=None, **kwargs):
""" """
Parse a string in one of the supported formats, using the `parserinfo`
parameters. Parse a string in one of the supported formats, using the
``parserinfo`` parameters.
:param timestr: :param timestr:
A string containing a date/time stamp. A string containing a date/time stamp.
:param parserinfo: :param parserinfo:
A :class:`parserinfo` object containing parameters for the parser. A :class:`parserinfo` object containing parameters for the parser.
If `None`, the default arguments to the `parserinfo` constructor are If ``None``, the default arguments to the :class:`parserinfo`
used. constructor are used.
The `**kwargs` parameter takes the following keyword arguments: The ``**kwargs`` parameter takes the following keyword arguments:
:param default: :param default:
The default datetime object, if this is a datetime object and not The default datetime object, if this is a datetime object and not
`None`, elements specified in `timestr` replace elements in the ``None``, elements specified in ``timestr`` replace elements in the
default object. default object.
:param ignoretz: :param ignoretz:
Whether or not to ignore the time zone (boolean). If set ``True``, time zones in parsed strings are ignored and a naive
:class:`datetime` object is returned.
:param tzinfos: :param tzinfos:
A time zone, to be applied to the date, if `ignoretz` is `True`. Additional time zone names / aliases which may be present in the
This can be either a subclass of `tzinfo`, a time zone string or an string. This argument maps time zone names (and optionally offsets
integer offset. 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 minutes or a :class:`tzinfo` object.
.. doctest::
:options: +NORMALIZE_WHITESPACE
>>> from dateutil.parser import parse
>>> from dateutil.tz import gettz
>>> tzinfos = {"BRST": -10800, "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', -10800))
>>> 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: :param dayfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (e.g. 01/05/09) as the day (``True``) or month (``False``). If
`yearfirst` is set to `True`, this distinguishes between YDM and ``yearfirst`` is set to ``True``, this distinguishes between YDM and
YMD. If set to `None`, this value is retrieved from the current YMD. If set to ``None``, this value is retrieved from the current
:class:`parserinfo` object (which itself defaults to `False`). :class:`parserinfo` object (which itself defaults to ``False``).
:param yearfirst: :param yearfirst:
Whether to interpret the first value in an ambiguous 3-integer date 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 (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 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 this is set to ``None``, the value is retrieved from the current
:class:`parserinfo` object (which itself defaults to `False`). :class:`parserinfo` object (which itself defaults to ``False``).
:param fuzzy: :param fuzzy:
Whether to allow fuzzy parsing, allowing for string like "Today is Whether to allow fuzzy parsing, allowing for string like "Today is
January 1, 2047 at 8:21:00AM". January 1, 2047 at 8:21:00AM".
:param fuzzy_with_tokens: :param fuzzy_with_tokens:
If `True`, `fuzzy` is automatically set to True, and the parser will If ``True``, ``fuzzy`` is automatically set to True, and the parser
return a tuple where the first element is the parsed will return a tuple where the first element is the parsed
`datetime.datetime` datetimestamp and the second element is a tuple :class:`datetime.datetime` datetimestamp and the second element is
containing the portions of the string which were ignored, e.g. a tuple containing the portions of the string which were ignored:
"Today is January 1, 2047 at 8:21:00AM" should return
`(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))` .. doctest::
>>> from dateutil.parser import parse
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
(datetime.datetime(2011, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
:return:
Returns a :class:`datetime.datetime` object or, if the
``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
first element being a :class:`datetime.datetime` object, the second
a tuple containing the fuzzy tokens.
:raises ValueError:
Raised for invalid or unknown string format, if the provided
:class:`tzinfo` is not in a valid format, or if an invalid date
would be created.
:raises OverFlowError:
Raised if the parsed date exceeds the largest valid C integer on
your system.
""" """
if parserinfo: if parserinfo:
return parser(parserinfo).parse(timestr, **kwargs) return parser(parserinfo).parse(timestr, **kwargs)

View file

@ -423,6 +423,7 @@ Here is the behavior of operations with relativedelta:
self.hours == other.hours and self.hours == other.hours and
self.minutes == other.minutes and self.minutes == other.minutes and
self.seconds == other.seconds and self.seconds == other.seconds and
self.microseconds == other.microseconds and
self.leapdays == other.leapdays and self.leapdays == other.leapdays and
self.year == other.year and self.year == other.year and
self.month == other.month and self.month == other.month and

View file

@ -104,12 +104,12 @@ class tzoffset(datetime.tzinfo):
class tzlocal(datetime.tzinfo): class tzlocal(datetime.tzinfo):
def __init__(self):
_std_offset = datetime.timedelta(seconds=-time.timezone) self._std_offset = datetime.timedelta(seconds=-time.timezone)
if time.daylight: if time.daylight:
_dst_offset = datetime.timedelta(seconds=-time.altzone) self._dst_offset = datetime.timedelta(seconds=-time.altzone)
else: else:
_dst_offset = _std_offset self._dst_offset = self._std_offset
def utcoffset(self, dt): def utcoffset(self, dt):
if self._isdst(dt): if self._isdst(dt):

View file

@ -4,6 +4,8 @@ import struct
from six.moves import winreg from six.moves import winreg
from .tz import tzname_in_python2
__all__ = ["tzwin", "tzwinlocal"] __all__ = ["tzwin", "tzwinlocal"]
ONEWEEK = datetime.timedelta(7) ONEWEEK = datetime.timedelta(7)
@ -42,6 +44,7 @@ class tzwinbase(datetime.tzinfo):
else: else:
return datetime.timedelta(0) return datetime.timedelta(0)
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
if self._isdst(dt): if self._isdst(dt):
return self._dstname return self._dstname
@ -89,8 +92,8 @@ class tzwin(tzwinbase):
"%s\%s" % (TZKEYNAME, name)) as tzkey: "%s\%s" % (TZKEYNAME, name)) as tzkey:
keydict = valuestodict(tzkey) keydict = valuestodict(tzkey)
self._stdname = keydict["Std"].encode("iso-8859-1") self._stdname = keydict["Std"]
self._dstname = keydict["Dlt"].encode("iso-8859-1") self._dstname = keydict["Dlt"]
self._display = keydict["Display"] self._display = keydict["Display"]
@ -129,8 +132,8 @@ class tzwinlocal(tzwinbase):
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
keydict = valuestodict(tzlocalkey) keydict = valuestodict(tzlocalkey)
self._stdname = keydict["StandardName"].encode("iso-8859-1") self._stdname = keydict["StandardName"]
self._dstname = keydict["DaylightName"].encode("iso-8859-1") self._dstname = keydict["DaylightName"]
try: try:
with winreg.OpenKey( with winreg.OpenKey(

View file

@ -16,14 +16,14 @@ from dateutil.tz import tzfile
__all__ = ["gettz", "gettz_db_metadata", "rebuild"] __all__ = ["gettz", "gettz_db_metadata", "rebuild"]
_ZONEFILENAME = "dateutil-zoneinfo.tar.gz" ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
_METADATA_FN = 'METADATA' METADATA_FN = 'METADATA'
# python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but # python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but
# it's close enough for python2.6 # it's close enough for python2.6
_tar_open = TarFile.open tar_open = TarFile.open
if not hasattr(TarFile, '__exit__'): if not hasattr(TarFile, '__exit__'):
def _tar_open(*args, **kwargs): def tar_open(*args, **kwargs):
return closing(TarFile.open(*args, **kwargs)) return closing(TarFile.open(*args, **kwargs))
@ -34,7 +34,7 @@ class tzfile(tzfile):
def getzoneinfofile_stream(): def getzoneinfofile_stream():
try: try:
return BytesIO(get_data(__name__, _ZONEFILENAME)) return BytesIO(get_data(__name__, ZONEFILENAME))
except IOError as e: # TODO switch to FileNotFoundError? except IOError as e: # TODO switch to FileNotFoundError?
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
return None return None
@ -43,7 +43,7 @@ def getzoneinfofile_stream():
class ZoneInfoFile(object): class ZoneInfoFile(object):
def __init__(self, zonefile_stream=None): def __init__(self, zonefile_stream=None):
if zonefile_stream is not None: if zonefile_stream is not None:
with _tar_open(fileobj=zonefile_stream, mode='r') as tf: with tar_open(fileobj=zonefile_stream, mode='r') as tf:
# dict comprehension does not work on python2.6 # dict comprehension does not work on python2.6
# TODO: get back to the nicer syntax when we ditch python2.6 # TODO: get back to the nicer syntax when we ditch python2.6
# self.zones = {zf.name: tzfile(tf.extractfile(zf), # self.zones = {zf.name: tzfile(tf.extractfile(zf),
@ -52,7 +52,7 @@ class ZoneInfoFile(object):
self.zones = dict((zf.name, tzfile(tf.extractfile(zf), self.zones = dict((zf.name, tzfile(tf.extractfile(zf),
filename=zf.name)) filename=zf.name))
for zf in tf.getmembers() for zf in tf.getmembers()
if zf.isfile() and zf.name != _METADATA_FN) if zf.isfile() and zf.name != METADATA_FN)
# deal with links: They'll point to their parent object. Less # deal with links: They'll point to their parent object. Less
# waste of memory # waste of memory
# links = {zl.name: self.zones[zl.linkname] # links = {zl.name: self.zones[zl.linkname]
@ -62,7 +62,7 @@ class ZoneInfoFile(object):
zl.islnk() or zl.issym()) zl.islnk() or zl.issym())
self.zones.update(links) self.zones.update(links)
try: try:
metadata_json = tf.extractfile(tf.getmember(_METADATA_FN)) metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
metadata_str = metadata_json.read().decode('UTF-8') metadata_str = metadata_json.read().decode('UTF-8')
self.metadata = json.loads(metadata_str) self.metadata = json.loads(metadata_str)
except KeyError: except KeyError:
@ -100,36 +100,3 @@ def gettz_db_metadata():
return _CLASS_ZONE_INSTANCE[0].metadata return _CLASS_ZONE_INSTANCE[0].metadata
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 _tar_open(filename) as tf:
for name in zonegroups:
tf.extract(name, tmpdir)
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
try:
check_call(["zic", "-d", zonedir] + filepaths)
except OSError as e:
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?")
raise
# write metadata file
with open(os.path.join(zonedir, _METADATA_FN), 'w') as f:
json.dump(metadata, f, indent=4, sort_keys=True)
target = os.path.join(moduledir, _ZONEFILENAME)
with _tar_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)

View file

@ -0,0 +1,43 @@
import logging
import os
import tempfile
import shutil
import json
from subprocess import check_call
from dateutil.zoneinfo import tar_open, 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 tar_open(filename) as tf:
for name in zonegroups:
tf.extract(name, tmpdir)
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
try:
check_call(["zic", "-d", zonedir] + filepaths)
except OSError as e:
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?")
raise
# write metadata file
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
json.dump(metadata, f, indent=4, sort_keys=True)
target = os.path.join(moduledir, ZONEFILENAME)
with tar_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)

View file

@ -48,7 +48,7 @@ def _remove_zoneinfo_failed(filename):
# helper to remove old unneeded zoneinfo files # helper to remove old unneeded zoneinfo files
def _remove_old_zoneinfo(): def _remove_old_zoneinfo():
zonefilename = zoneinfo._ZONEFILENAME zonefilename = zoneinfo.ZONEFILENAME
if None is zonefilename: if None is zonefilename:
return return
cur_zoneinfo = ek.ek(basename, zonefilename) cur_zoneinfo = ek.ek(basename, zonefilename)
@ -83,7 +83,7 @@ def _update_zoneinfo():
logger.WARNING) logger.WARNING)
return return
zonefilename = zoneinfo._ZONEFILENAME zonefilename = zoneinfo.ZONEFILENAME
cur_zoneinfo = zonefilename cur_zoneinfo = zonefilename
if None is not cur_zoneinfo: if None is not cur_zoneinfo:
cur_zoneinfo = ek.ek(basename, zonefilename) cur_zoneinfo = ek.ek(basename, zonefilename)
@ -222,7 +222,7 @@ def get_network_timezone(network, network_dict):
return sb_timezone return sb_timezone
try: try:
if zoneinfo._ZONEFILENAME is not None: if zoneinfo.ZONEFILENAME is not None:
try: try:
n_t = tz.gettz(network_dict[network]) n_t = tz.gettz(network_dict[network])
except: except: