Skip to content

Commit

Permalink
Checkpoint on coverage for formats.py. There's a bug involving timezo…
Browse files Browse the repository at this point in the history
…nes and time only
  • Loading branch information
jamadden committed Dec 16, 2017
1 parent a71b187 commit 376b543
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 45 deletions.
83 changes: 44 additions & 39 deletions src/zope/i18n/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -94,15 +97,15 @@ def parse(self, text, pattern=None, asObject=True):
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:
Expand Down Expand Up @@ -176,24 +179,25 @@ def parse(self, text, pattern=None, asObject=True):
# 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"
Expand Down Expand Up @@ -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".",
Expand All @@ -240,10 +246,8 @@ def __init__(self, pattern=None, symbols={}):
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"
Expand Down Expand Up @@ -290,7 +294,7 @@ def parse(self, text, pattern=None):
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] != '':
Expand All @@ -308,7 +312,7 @@ def parse(self, text, pattern=None):
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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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'))),
Expand Down Expand Up @@ -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
Expand Down
62 changes: 56 additions & 6 deletions src/zope/i18n/tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,16 @@ def getDayTypeFromAbbreviation(self, abbr):
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)])
Expand Down Expand Up @@ -179,7 +186,7 @@ def testParseQuotesInPattern(self):

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")
Expand All @@ -188,7 +195,7 @@ def testParseDateTimePatternError(self):
parseDateTimePattern("HHHHH")


class TestBuildDateTimeParseInfo(TestCase):
class TestBuildDateTimeParseInfo(_TestCase):
"""This class tests the functionality of the buildDateTimeParseInfo()
method with the German locale.
"""
Expand Down Expand Up @@ -255,7 +262,7 @@ def testWeekdayAbbr(self):
self.assertEqual(self.info(('E', 3)), '('+'|'.join(names)+')')


class TestDateTimeFormat(TestCase):
class TestDateTimeFormat(_TestCase):
"""Test the functionality of an implmentation of the ILocaleProvider
interface."""

Expand Down Expand Up @@ -372,6 +379,49 @@ def testParseUnusualFormats(self):
'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(
Expand Down Expand Up @@ -619,7 +669,7 @@ def testFormatUnusualFormats(self):



class TestNumberPatternParser(TestCase):
class TestNumberPatternParser(_TestCase):
"""Extensive tests for the ICU-based-syntax number pattern parser."""

def testParseSimpleIntegerPattern(self):
Expand Down Expand Up @@ -837,7 +887,7 @@ def testParseStringEscapedSuffix(self):
(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={
Expand Down

0 comments on commit 376b543

Please sign in to comment.