diff options
author | Jason Madden <jamadden@gmail.com> | 2017-12-16 12:20:30 -0600 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2017-12-16 12:20:30 -0600 |
commit | 376b5438893ec6b3f456ceb166407bffeacc1b7d (patch) | |
tree | 11dbd734131c78bb73cfbf10c7b212dbf0a8a37c | |
parent | a71b187693f80eb432013db57b9f4dbc31fea26a (diff) | |
download | zope-i18n-376b5438893ec6b3f456ceb166407bffeacc1b7d.tar.gz |
Checkpoint on coverage for formats.py. There's a bug involving timezones and time only
-rw-r--r-- | src/zope/i18n/format.py | 83 | ||||
-rw-r--r-- | src/zope/i18n/tests/test_formats.py | 62 |
2 files changed, 100 insertions, 45 deletions
diff --git a/src/zope/i18n/format.py b/src/zope/i18n/format.py index 0c1ffd6..220e07f 100644 --- a/src/zope/i18n/format.py +++ b/src/zope/i18n/format.py @@ -56,14 +56,17 @@ class DateTimeFormat(object): _DATETIMECHARS = "aGyMdEDFwWhHmsSkKz" + calendar = None + _pattern = None + _bin_pattern = None + def __init__(self, pattern=None, calendar=None): if calendar is not None: self.calendar = calendar self._pattern = pattern self._bin_pattern = None - if self._pattern is not None: - self._bin_pattern = parseDateTimePattern(self._pattern, - self._DATETIMECHARS) + if pattern is not None: + self.setPattern(pattern) def setPattern(self, pattern): "See zope.i18n.interfaces.IFormat" @@ -94,15 +97,15 @@ class DateTimeFormat(object): results = re.match(regex, text).groups() except AttributeError: raise DateTimeParseError( - 'The datetime string did not match the pattern %r.' - % pattern) + 'The datetime string did not match the pattern %r.' + % pattern) # Sometimes you only want the parse results if not asObject: return results # Map the parsing results to a datetime object ordered = [None, None, None, None, None, None, None] - bin_pattern = list(filter(lambda x: isinstance(x, tuple), bin_pattern)) + bin_pattern = [x for x in bin_pattern if isinstance(x, tuple)] # Handle years; note that only 'yy' and 'yyyy' are allowed if ('y', 2) in bin_pattern: @@ -176,24 +179,25 @@ class DateTimeFormat(object): # paid for dealing with localtimes. if ordered[3:] == [None, None, None, None]: return datetime.date(*[e or 0 for e in ordered[:3]]) - elif ordered[:3] == [None, None, None]: + if ordered[:3] == [None, None, None]: if pytz_tzinfo: + # XXX: This raises a TypeError: + # unsupported operator + for datetime.time and datetime.timedelta return tzinfo.localize( datetime.time(*[e or 0 for e in ordered[3:]]) ) - else: - return datetime.time( - *[e or 0 for e in ordered[3:]], **{'tzinfo' :tzinfo} - ) - else: - if pytz_tzinfo: - return tzinfo.localize(datetime.datetime( - *[e or 0 for e in ordered] - )) - else: - return datetime.datetime( - *[e or 0 for e in ordered], **{'tzinfo' :tzinfo} - ) + return datetime.time( + *[e or 0 for e in ordered[3:]], **{'tzinfo' :tzinfo} + ) + + if pytz_tzinfo: + return tzinfo.localize(datetime.datetime( + *[e or 0 for e in ordered] + )) + + return datetime.datetime( + *[e or 0 for e in ordered], **{'tzinfo' :tzinfo} + ) def format(self, obj, pattern=None): "See zope.i18n.interfaces.IFormat" @@ -222,8 +226,10 @@ class NumberFormat(object): type = None + _pattern = None + _bin_pattern = None - def __init__(self, pattern=None, symbols={}): + def __init__(self, pattern=None, symbols=()): # setup default symbols self.symbols = { u"decimal": u".", @@ -240,10 +246,8 @@ class NumberFormat(object): u"nan": u'' } self.symbols.update(symbols) - self._pattern = pattern - self._bin_pattern = None - if self._pattern is not None: - self._bin_pattern = parseNumberPattern(self._pattern) + if pattern is not None: + self.setPattern(pattern) def setPattern(self, pattern): "See zope.i18n.interfaces.IFormat" @@ -290,7 +294,7 @@ class NumberFormat(object): if bin_pattern[sign][EXPONENTIAL][0] == '+': pre_symbols += self.symbols['plusSign'] regex += '[%s]?[0-9]{%i,100}' %(pre_symbols, min_exp_size) - regex +=')' + regex += ')' if bin_pattern[sign][PADDING3] is not None: regex += '[' + bin_pattern[sign][PADDING3] + ']+' if bin_pattern[sign][SUFFIX] != '': @@ -308,7 +312,7 @@ class NumberFormat(object): sign = -1 else: raise NumberParseError('Not a valid number for this pattern %r.' - % pattern) + % pattern) # Remove possible grouping separators num_str = num_str.replace(self.symbols['group'], '') # Extract number @@ -500,9 +504,9 @@ def parseDateTimePattern(pattern, DATETIMECHARS="aGyMdEDFwWhHmsSkKz"): char = '' quote_start = -2 - for pos in range(len(pattern)): + for pos, next_char in enumerate(pattern): prev_char = char - char = pattern[pos] + char = next_char # Handle quotations if char == "'": if state == DEFAULT: @@ -553,11 +557,11 @@ def parseDateTimePattern(pattern, DATETIMECHARS="aGyMdEDFwWhHmsSkKz"): if state == IN_QUOTE: if quote_start == -1: raise DateTimePatternParseError( - 'Waaa: state = IN_QUOTE and quote_start = -1!') + 'Waaa: state = IN_QUOTE and quote_start = -1!') else: raise DateTimePatternParseError( - 'The quote starting at character %i is not closed.' % - quote_start) + 'The quote starting at character %i is not closed.' % + quote_start) elif state == IN_DATETIMEFIELD: result.append((helper[0], len(helper))) elif state == DEFAULT: @@ -584,7 +588,7 @@ def buildDateTimeParseInfo(calendar, pattern): elif entry[1] == 4: info[entry] = r'([0-9]{4})' else: - raise DateTimePatternParseError("Only 'yy' and 'yyyy' allowed." ) + raise DateTimePatternParseError("Only 'yy' and 'yyyy' allowed.") # am/pm marker (Text) for entry in _findFormattingCharacterInPattern('a', pattern): @@ -659,17 +663,18 @@ def buildDateTimeInfo(dt, calendar, pattern): # Getting the timezone right tzinfo = dt.tzinfo or pytz.utc tz_secs = tzinfo.utcoffset(dt).seconds - tz_secs = (tz_secs > 12*3600) and tz_secs-24*3600 or tz_secs + tz_secs = tz_secs - 24 * 3600 if tz_secs > 12 * 3600 else tz_secs tz_mins = int(math.fabs(tz_secs % 3600 / 60)) tz_hours = int(math.fabs(tz_secs / 3600)) - tz_sign = (tz_secs < 0) and '-' or '+' + tz_sign = '-' if tz_secs < 0 else '+' tz_defaultname = "%s%i%.2i" %(tz_sign, tz_hours, tz_mins) tz_name = tzinfo.tzname(dt) or tz_defaultname tz_fullname = getattr(tzinfo, 'zone', None) or tz_name - info = {('y', 2): text_type(dt.year)[2:], - ('y', 4): text_type(dt.year), - } + info = { + ('y', 2): text_type(dt.year)[2:], + ('y', 4): text_type(dt.year), + } # Generic Numbers for field, value in (('d', dt.day), ('D', int(dt.strftime('%j'))), @@ -791,7 +796,7 @@ def parseNumberPattern(pattern): helper += char else: raise NumberPatternParseError( - 'Wrong syntax at beginning of pattern.') + 'Wrong syntax at beginning of pattern.') elif state == READ_PADDING_1: padding_1 = char diff --git a/src/zope/i18n/tests/test_formats.py b/src/zope/i18n/tests/test_formats.py index 970efba..a77fc8e 100644 --- a/src/zope/i18n/tests/test_formats.py +++ b/src/zope/i18n/tests/test_formats.py @@ -98,9 +98,16 @@ class LocaleCalendarStub(object): raise NotImplementedError() -class TestDateTimePatternParser(TestCase): +class _TestCase(TestCase): + if not hasattr(TestCase, 'assertRaisesRegex'): + # Avoid deprecation warnings in Python 3 + assertRaisesRegex = TestCase.assertRaisesRegexp + + +class TestDateTimePatternParser(_TestCase): """Extensive tests for the ICU-based-syntax datetime pattern parser.""" + def testParseSimpleTimePattern(self): self.assertEqual(parseDateTimePattern('HH'), [('H', 2)]) @@ -179,7 +186,7 @@ class TestDateTimePatternParser(TestCase): def testParseDateTimePatternError(self): # Quote not closed - with self.assertRaisesRegexp( + with self.assertRaisesRegex( DateTimePatternParseError, 'The quote starting at character 2 is not closed.'): parseDateTimePattern("HH' Uhr") @@ -188,7 +195,7 @@ class TestDateTimePatternParser(TestCase): parseDateTimePattern("HHHHH") -class TestBuildDateTimeParseInfo(TestCase): +class TestBuildDateTimeParseInfo(_TestCase): """This class tests the functionality of the buildDateTimeParseInfo() method with the German locale. """ @@ -255,7 +262,7 @@ class TestBuildDateTimeParseInfo(TestCase): self.assertEqual(self.info(('E', 3)), '('+'|'.join(names)+')') -class TestDateTimeFormat(TestCase): +class TestDateTimeFormat(_TestCase): """Test the functionality of an implmentation of the ILocaleProvider interface.""" @@ -372,6 +379,49 @@ class TestDateTimeFormat(TestCase): 'dddd. MMM yyyy hhhh:mm a'), datetime.datetime(2003, 1, 1, 00, 00, 00, 00)) + def testParseNotObject(self): + self.assertEqual( + ('2017', '01', '01'), + self.format.parse('2017-01-01', 'yyyy-MM-dd', asObject=False)) + + def testParseTwoDigitYearIs20thCentury(self): + self.assertEqual( + datetime.date(1952, 1, 1), + self.format.parse('52-01-01', 'yy-MM-dd')) + + # 30 is the cut off + self.assertEqual( + datetime.date(1931, 1, 1), + self.format.parse('31-01-01', 'yy-MM-dd')) + + self.assertEqual( + datetime.date(2030, 1, 1), + self.format.parse('30-01-01', 'yy-MM-dd')) + + def testParseAMPMMissing(self): + with self.assertRaisesRegex( + DateTimeParseError, + 'Cannot handle 12-hour format without am/pm marker.'): + self.format.parse('02.01.03 09:48', 'dd.MM.yy hh:mm') + + def testParseBadTimezone(self): + # Produces an object without pytz info + self.assertEqual( + datetime.time(21, 48, 1), + self.format.parse( + '21:48:01 Bad/Timezone', + 'HH:mm:ss zzzz')) + + def testParsePyTzTimezone(self): + # XXX: Bug: This raises + # TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.timedelta' + # at pytz/tzinfo.py:309 + tzinfo = pytz.timezone("US/Central") + with self.assertRaisesRegex(TypeError, 'datetime.timedelta'): + self.format.parse( + '21:48:01 US/Central', + 'HH:mm:ss zzzz') + def testFormatSimpleDateTime(self): # German short self.assertEqual( @@ -619,7 +669,7 @@ class TestDateTimeFormat(TestCase): -class TestNumberPatternParser(TestCase): +class TestNumberPatternParser(_TestCase): """Extensive tests for the ICU-based-syntax number pattern parser.""" def testParseSimpleIntegerPattern(self): @@ -837,7 +887,7 @@ class TestNumberPatternParser(TestCase): (None, '', None, '###0', '', '', None, 'DEM', None, 0))) -class TestNumberFormat(TestCase): +class TestNumberFormat(_TestCase): """Test the functionality of an implmentation of the NumberFormat.""" format = NumberFormat(symbols={ |