aboutsummaryrefslogtreecommitdiff
path: root/tests/test_parse.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_parse.py')
-rw-r--r--tests/test_parse.py1013
1 files changed, 1013 insertions, 0 deletions
diff --git a/tests/test_parse.py b/tests/test_parse.py
new file mode 100644
index 0000000..df3b967
--- /dev/null
+++ b/tests/test_parse.py
@@ -0,0 +1,1013 @@
+# -*- encoding: utf8 -*-
+# -- BASED-ON: https://github.com/r1chardj0n3s/parse/test_parse.py
+# VERSION: parse 1.12.0
+# Same as original file but uses bundled :mod:`parse_type.parse` module
+# instead of :mod:`parse` module
+#
+# NOTE: Part of the tests are/were providd by jenisys.
+# -- ORIGINAL-CODE STARTS-HERE ------------------------------------------------
+'''Test suite for parse.py
+
+This code is copyright 2011 eKit.com Inc (http://www.ekit.com/)
+See the end of the source file for the license of use.
+'''
+
+from __future__ import absolute_import
+import unittest
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+# -- ADAPTATION-END
+from datetime import datetime, time
+from decimal import Decimal
+import re
+
+# -- EXTENSION:
+import os
+PARSE_MODULE = os.environ.get("PARSE_TYPE_PARSE_MODULE", "parse_type.parse")
+if PARSE_MODULE.startswith("parse_type"):
+ # -- USE VENDOR MODULE: parse_type.parse (probably older that original)
+ from parse_type import parse
+else:
+ # -- USE ORIGINAL MODULE: parse
+ import parse
+# -- EXTENSION-END
+
+
+class TestPattern(unittest.TestCase):
+ def _test_expression(self, format, expression):
+ self.assertEqual(parse.Parser(format)._expression, expression)
+
+ def test_braces(self):
+ # pull a simple string out of another string
+ self._test_expression('{{ }}', r'\{ \}')
+
+ def test_fixed(self):
+ # pull a simple string out of another string
+ self._test_expression('{}', r'(.+?)')
+ self._test_expression('{} {}', r'(.+?) (.+?)')
+
+ def test_named(self):
+ # pull a named string out of another string
+ self._test_expression('{name}', r'(?P<name>.+?)')
+ self._test_expression('{name} {other}',
+ r'(?P<name>.+?) (?P<other>.+?)')
+
+ def test_named_typed(self):
+ # pull a named string out of another string
+ self._test_expression('{name:w}', r'(?P<name>\w+)')
+ self._test_expression('{name:w} {other:w}',
+ r'(?P<name>\w+) (?P<other>\w+)')
+
+ def test_beaker(self):
+ # skip some trailing whitespace
+ self._test_expression('{:<}', r'(.+?) *')
+
+ def test_left_fill(self):
+ # skip some trailing periods
+ self._test_expression('{:.<}', r'(.+?)\.*')
+
+ def test_bird(self):
+ # skip some trailing whitespace
+ self._test_expression('{:>}', r' *(.+?)')
+
+ def test_center(self):
+ # skip some surrounding whitespace
+ self._test_expression('{:^}', r' *(.+?) *')
+
+ def test_format_variety(self):
+ def _(fmt, matches):
+ d = parse.extract_format(fmt, {'spam': 'spam'})
+ for k in matches:
+ self.assertEqual(d.get(k), matches[k],
+ 'm["%s"]=%r, expect %r' % (k, d.get(k), matches[k]))
+
+ for t in '%obxegfdDwWsS':
+ _(t, dict(type=t))
+ _('10' + t, dict(type=t, width='10'))
+ _('05d', dict(type='d', width='5', zero=True))
+ _('<', dict(align='<'))
+ _('.<', dict(align='<', fill='.'))
+ _('>', dict(align='>'))
+ _('.>', dict(align='>', fill='.'))
+ _('^', dict(align='^'))
+ _('.^', dict(align='^', fill='.'))
+ _('x=d', dict(type='d', align='=', fill='x'))
+ _('d', dict(type='d'))
+ _('ti', dict(type='ti'))
+ _('spam', dict(type='spam'))
+
+ _('.^010d', dict(type='d', width='10', align='^', fill='.',
+ zero=True))
+ _('.2f', dict(type='f', precision='2'))
+ _('10.2f', dict(type='f', width='10', precision='2'))
+
+ def test_dot_separated_fields(self):
+ # this should just work and provide the named value
+ res = parse.parse('{hello.world}_{jojo.foo.baz}_{simple}', 'a_b_c')
+ assert res.named['hello.world'] == 'a'
+ assert res.named['jojo.foo.baz'] == 'b'
+ assert res.named['simple'] == 'c'
+
+ def test_dict_style_fields(self):
+ res = parse.parse('{hello[world]}_{hello[foo][baz]}_{simple}', 'a_b_c')
+ assert res.named['hello']['world'] == 'a'
+ assert res.named['hello']['foo']['baz'] == 'b'
+ assert res.named['simple'] == 'c'
+
+ def test_dot_separated_fields_name_collisions(self):
+ # this should just work and provide the named value
+ res = parse.parse('{a_.b}_{a__b}_{a._b}_{a___b}', 'a_b_c_d')
+ assert res.named['a_.b'] == 'a'
+ assert res.named['a__b'] == 'b'
+ assert res.named['a._b'] == 'c'
+ assert res.named['a___b'] == 'd'
+
+ def test_invalid_groupnames_are_handled_gracefully(self):
+ self.assertRaises(NotImplementedError, parse.parse,
+ "{hello['world']}", "doesn't work")
+
+
+class TestResult(unittest.TestCase):
+ def test_fixed_access(self):
+ r = parse.Result((1, 2), {}, None)
+ self.assertEqual(r[0], 1)
+ self.assertEqual(r[1], 2)
+ self.assertRaises(IndexError, r.__getitem__, 2)
+ self.assertRaises(KeyError, r.__getitem__, 'spam')
+
+ def test_named_access(self):
+ r = parse.Result((), {'spam': 'ham'}, None)
+ self.assertEqual(r['spam'], 'ham')
+ self.assertRaises(KeyError, r.__getitem__, 'ham')
+ self.assertRaises(IndexError, r.__getitem__, 0)
+
+ def test_contains(self):
+ r = parse.Result(('cat',), {'spam': 'ham'}, None)
+ self.assertTrue('spam' in r)
+ self.assertTrue('cat' not in r)
+ self.assertTrue('ham' not in r)
+
+
+class TestParse(unittest.TestCase):
+ def test_no_match(self):
+ # string does not match format
+ self.assertEqual(parse.parse('{{hello}}', 'hello'), None)
+
+ def test_nothing(self):
+ # do no actual parsing
+ r = parse.parse('{{hello}}', '{hello}')
+ self.assertEqual(r.fixed, ())
+ self.assertEqual(r.named, {})
+
+ def test_no_evaluate_result(self):
+ # pull a fixed value out of string
+ match = parse.parse('hello {}', 'hello world', evaluate_result=False)
+ r = match.evaluate_result()
+ self.assertEqual(r.fixed, ('world', ))
+
+ def test_regular_expression(self):
+ # match an actual regular expression
+ s = r'^(hello\s[wW]{}!+.*)$'
+ e = s.replace('{}', 'orld')
+ r = parse.parse(s, e)
+ self.assertEqual(r.fixed, ('orld',))
+ e = s.replace('{}', '.*?')
+ r = parse.parse(s, e)
+ self.assertEqual(r.fixed, ('.*?',))
+
+ def test_question_mark(self):
+ # issue9: make sure a ? in the parse string is handled correctly
+ r = parse.parse('"{}"?', '"teststr"?')
+ self.assertEqual(r[0], 'teststr')
+
+ def test_pipe(self):
+ # issue22: make sure a | in the parse string is handled correctly
+ r = parse.parse('| {}', '| teststr')
+ self.assertEqual(r[0], 'teststr')
+
+ def test_unicode(self):
+ # issue29: make sure unicode is parsable
+ r = parse.parse('{}', u't€ststr')
+ self.assertEqual(r[0], u't€ststr')
+
+ def test_hexadecimal(self):
+ # issue42: make sure bare hexadecimal isn't matched as "digits"
+ r = parse.parse('{:d}', 'abcdef')
+ self.assertIsNone(r)
+
+ def test_fixed(self):
+ # pull a fixed value out of string
+ r = parse.parse('hello {}', 'hello world')
+ self.assertEqual(r.fixed, ('world', ))
+
+ def test_left(self):
+ # pull left-aligned text out of string
+ r = parse.parse('{:<} world', 'hello world')
+ self.assertEqual(r.fixed, ('hello', ))
+
+ def test_right(self):
+ # pull right-aligned text out of string
+ r = parse.parse('hello {:>}', 'hello world')
+ self.assertEqual(r.fixed, ('world', ))
+
+ def test_center(self):
+ # pull center-aligned text out of string
+ r = parse.parse('hello {:^} world', 'hello there world')
+ self.assertEqual(r.fixed, ('there', ))
+
+ def test_typed(self):
+ # pull a named, typed values out of string
+ r = parse.parse('hello {:d} {:w}', 'hello 12 people')
+ self.assertEqual(r.fixed, (12, 'people'))
+ r = parse.parse('hello {:w} {:w}', 'hello 12 people')
+ self.assertEqual(r.fixed, ('12', 'people'))
+
+ def test_precision(self):
+ # pull a float out of a string
+ r = parse.parse('Pi = {:.7f}', 'Pi = 3.1415926')
+ self.assertEqual(r.fixed, (3.1415926, ))
+ r = parse.parse('Pi/10 = {:8.5f}', 'Pi/10 = 0.31415')
+ self.assertEqual(r.fixed, (0.31415, ))
+
+ def test_precision_fail(self):
+ # floats must have a leading zero
+ # IS THIS CORRECT?
+ r = parse.parse('Pi/10 = {:8.5f}', 'Pi/10 = .31415')
+ self.assertEqual(r, None)
+
+ def test_custom_type(self):
+ # use a custom type
+ r = parse.parse('{:shouty} {:spam}', 'hello world',
+ dict(shouty=lambda s: s.upper(),
+ spam=lambda s: ''.join(reversed(s))))
+ self.assertEqual(r.fixed, ('HELLO', 'dlrow'))
+ r = parse.parse('{:d}', '12', dict(d=lambda s: int(s) * 2))
+ self.assertEqual(r.fixed, (24,))
+ r = parse.parse('{:d}', '12')
+ self.assertEqual(r.fixed, (12,))
+
+ def test_typed_fail(self):
+ # pull a named, typed values out of string
+ self.assertEqual(parse.parse('hello {:d} {:w}', 'hello people 12'),
+ None)
+
+ def test_named(self):
+ # pull a named value out of string
+ r = parse.parse('hello {name}', 'hello world')
+ self.assertEqual(r.named, {'name': 'world'})
+
+ def test_named_repeated(self):
+ # test a name may be repeated
+ r = parse.parse('{n} {n}', 'x x')
+ self.assertEqual(r.named, {'n': 'x'})
+
+ def test_named_repeated_type(self):
+ # test a name may be repeated with type conversion
+ r = parse.parse('{n:d} {n:d}', '1 1')
+ self.assertEqual(r.named, {'n': 1})
+
+ def test_named_repeated_fail_value(self):
+ # test repeated name fails if value mismatches
+ r = parse.parse('{n} {n}', 'x y')
+ self.assertEqual(r, None)
+
+ def test_named_repeated_type_fail_value(self):
+ # test repeated name with type conversion fails if value mismatches
+ r = parse.parse('{n:d} {n:d}', '1 2')
+ self.assertEqual(r, None)
+
+ def test_named_repeated_type_mismatch(self):
+ # test repeated name with mismatched type
+ self.assertRaises(parse.RepeatedNameError, parse.compile,
+ '{n:d} {n:w}')
+
+ def test_mixed(self):
+ # pull a fixed and named values out of string
+ r = parse.parse('hello {} {name} {} {spam}',
+ 'hello world and other beings')
+ self.assertEqual(r.fixed, ('world', 'other'))
+ self.assertEqual(r.named, dict(name='and', spam='beings'))
+
+ def test_named_typed(self):
+ # pull a named, typed values out of string
+ r = parse.parse('hello {number:d} {things}', 'hello 12 people')
+ self.assertEqual(r.named, dict(number=12, things='people'))
+ r = parse.parse('hello {number:w} {things}', 'hello 12 people')
+ self.assertEqual(r.named, dict(number='12', things='people'))
+
+ def test_named_aligned_typed(self):
+ # pull a named, typed values out of string
+ r = parse.parse('hello {number:<d} {things}', 'hello 12 people')
+ self.assertEqual(r.named, dict(number=12, things='people'))
+ r = parse.parse('hello {number:>d} {things}', 'hello 12 people')
+ self.assertEqual(r.named, dict(number=12, things='people'))
+ r = parse.parse('hello {number:^d} {things}',
+ 'hello 12 people')
+ self.assertEqual(r.named, dict(number=12, things='people'))
+
+ def test_multiline(self):
+ r = parse.parse('hello\n{}\nworld', 'hello\nthere\nworld')
+ self.assertEqual(r.fixed[0], 'there')
+
+ def test_spans(self):
+ # test the string sections our fields come from
+ string = 'hello world'
+ r = parse.parse('hello {}', string)
+ self.assertEqual(r.spans, {0: (6, 11)})
+ start, end = r.spans[0]
+ self.assertEqual(string[start:end], r.fixed[0])
+
+ string = 'hello world'
+ r = parse.parse('hello {:>}', string)
+ self.assertEqual(r.spans, {0: (10, 15)})
+ start, end = r.spans[0]
+ self.assertEqual(string[start:end], r.fixed[0])
+
+ string = 'hello 0x12 world'
+ r = parse.parse('hello {val:x} world', string)
+ self.assertEqual(r.spans, {'val': (6, 10)})
+ start, end = r.spans['val']
+ self.assertEqual(string[start:end], '0x%x' % r.named['val'])
+
+ string = 'hello world and other beings'
+ r = parse.parse('hello {} {name} {} {spam}', string)
+ self.assertEqual(r.spans, {0: (6, 11), 'name': (12, 15),
+ 1: (16, 21), 'spam': (22, 28)})
+
+ def test_numbers(self):
+ # pull a numbers out of a string
+ def y(fmt, s, e, str_equals=False):
+ p = parse.compile(fmt)
+ r = p.parse(s)
+ if r is None:
+ self.fail('%r (%r) did not match %r' % (fmt, p._expression, s))
+ r = r.fixed[0]
+ if str_equals:
+ self.assertEqual(str(r), str(e),
+ '%r found %r in %r, not %r' % (fmt, r, s, e))
+ else:
+ self.assertEqual(r, e,
+ '%r found %r in %r, not %r' % (fmt, r, s, e))
+
+ def n(fmt, s, e):
+ if parse.parse(fmt, s) is not None:
+ self.fail('%r matched %r' % (fmt, s))
+ y('a {:d} b', 'a 0 b', 0)
+ y('a {:d} b', 'a 12 b', 12)
+ y('a {:5d} b', 'a 12 b', 12)
+ y('a {:5d} b', 'a -12 b', -12)
+ y('a {:d} b', 'a -12 b', -12)
+ y('a {:d} b', 'a +12 b', 12)
+ y('a {:d} b', 'a 12 b', 12)
+ y('a {:d} b', 'a 0b1000 b', 8)
+ y('a {:d} b', 'a 0o1000 b', 512)
+ y('a {:d} b', 'a 0x1000 b', 4096)
+ y('a {:d} b', 'a 0xabcdef b', 0xabcdef)
+
+ y('a {:%} b', 'a 100% b', 1)
+ y('a {:%} b', 'a 50% b', .5)
+ y('a {:%} b', 'a 50.1% b', .501)
+
+ y('a {:n} b', 'a 100 b', 100)
+ y('a {:n} b', 'a 1,000 b', 1000)
+ y('a {:n} b', 'a 1.000 b', 1000)
+ y('a {:n} b', 'a -1,000 b', -1000)
+ y('a {:n} b', 'a 10,000 b', 10000)
+ y('a {:n} b', 'a 100,000 b', 100000)
+ n('a {:n} b', 'a 100,00 b', None)
+ y('a {:n} b', 'a 100.000 b', 100000)
+ y('a {:n} b', 'a 1.000.000 b', 1000000)
+
+ y('a {:f} b', 'a 12.0 b', 12.0)
+ y('a {:f} b', 'a -12.1 b', -12.1)
+ y('a {:f} b', 'a +12.1 b', 12.1)
+ n('a {:f} b', 'a 12 b', None)
+
+ y('a {:e} b', 'a 1.0e10 b', 1.0e10)
+ y('a {:e} b', 'a 1.0E10 b', 1.0e10)
+ y('a {:e} b', 'a 1.10000e10 b', 1.1e10)
+ y('a {:e} b', 'a 1.0e-10 b', 1.0e-10)
+ y('a {:e} b', 'a 1.0e+10 b', 1.0e10)
+ # can't actually test this one on values 'cos nan != nan
+ y('a {:e} b', 'a nan b', float('nan'), str_equals=True)
+ y('a {:e} b', 'a NAN b', float('nan'), str_equals=True)
+ y('a {:e} b', 'a inf b', float('inf'))
+ y('a {:e} b', 'a +inf b', float('inf'))
+ y('a {:e} b', 'a -inf b', float('-inf'))
+ y('a {:e} b', 'a INF b', float('inf'))
+ y('a {:e} b', 'a +INF b', float('inf'))
+ y('a {:e} b', 'a -INF b', float('-inf'))
+
+ y('a {:g} b', 'a 1 b', 1)
+ y('a {:g} b', 'a 1e10 b', 1e10)
+ y('a {:g} b', 'a 1.0e10 b', 1.0e10)
+ y('a {:g} b', 'a 1.0E10 b', 1.0e10)
+
+ y('a {:b} b', 'a 1000 b', 8)
+ y('a {:b} b', 'a 0b1000 b', 8)
+ y('a {:o} b', 'a 12345670 b', int('12345670', 8))
+ y('a {:o} b', 'a 0o12345670 b', int('12345670', 8))
+ y('a {:x} b', 'a 1234567890abcdef b', 0x1234567890abcdef)
+ y('a {:x} b', 'a 1234567890ABCDEF b', 0x1234567890ABCDEF)
+ y('a {:x} b', 'a 0x1234567890abcdef b', 0x1234567890abcdef)
+ y('a {:x} b', 'a 0x1234567890ABCDEF b', 0x1234567890ABCDEF)
+
+ y('a {:05d} b', 'a 00001 b', 1)
+ y('a {:05d} b', 'a -00001 b', -1)
+ y('a {:05d} b', 'a +00001 b', 1)
+ y('a {:02d} b', 'a 10 b', 10)
+
+ y('a {:=d} b', 'a 000012 b', 12)
+ y('a {:x=5d} b', 'a xxx12 b', 12)
+ y('a {:x=5d} b', 'a -xxx12 b', -12)
+
+ def test_hex_looks_like_binary_issue65(self):
+ r = parse.parse('a {:x} b', 'a 0B b')
+ self.assertEqual(r[0], 11)
+ r = parse.parse('a {:x} b', 'a 0B1 b')
+ self.assertEqual(r[0], 1)
+
+ def test_two_datetimes(self):
+ r = parse.parse('a {:ti} {:ti} b', 'a 1997-07-16 2012-08-01 b')
+ self.assertEqual(len(r.fixed), 2)
+ self.assertEqual(r[0], datetime(1997, 7, 16))
+ self.assertEqual(r[1], datetime(2012, 8, 1))
+
+ def test_datetimes(self):
+ def y(fmt, s, e, tz=None):
+ p = parse.compile(fmt)
+ r = p.parse(s)
+ if r is None:
+ self.fail('%r (%r) did not match %r' % (fmt, p._expression, s))
+ r = r.fixed[0]
+ try:
+ self.assertEqual(r, e,
+ '%r found %r in %r, not %r' % (fmt, r, s, e))
+ except ValueError:
+ self.fail('%r found %r in %r, not %r' % (fmt, r, s, e))
+
+ if tz is not None:
+ self.assertEqual(r.tzinfo, tz,
+ '%r found TZ %r in %r, not %r' % (fmt, r.tzinfo, s, e))
+
+ def n(fmt, s, e):
+ if parse.parse(fmt, s) is not None:
+ self.fail('%r matched %r' % (fmt, s))
+
+ utc = parse.FixedTzOffset(0, 'UTC')
+ aest = parse.FixedTzOffset(10 * 60, '+1000')
+ tz60 = parse.FixedTzOffset(60, '+01:00')
+
+ # ISO 8660 variants
+ # YYYY-MM-DD (eg 1997-07-16)
+ y('a {:ti} b', 'a 1997-07-16 b', datetime(1997, 7, 16))
+
+ # YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
+ y('a {:ti} b', 'a 1997-07-16 19:20 b',
+ datetime(1997, 7, 16, 19, 20, 0))
+ y('a {:ti} b', 'a 1997-07-16T19:20 b',
+ datetime(1997, 7, 16, 19, 20, 0))
+ y('a {:ti} b', 'a 1997-07-16T19:20Z b',
+ datetime(1997, 7, 16, 19, 20, tzinfo=utc))
+ y('a {:ti} b', 'a 1997-07-16T19:20+0100 b',
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60))
+ y('a {:ti} b', 'a 1997-07-16T19:20+01:00 b',
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60))
+ y('a {:ti} b', 'a 1997-07-16T19:20 +01:00 b',
+ datetime(1997, 7, 16, 19, 20, tzinfo=tz60))
+
+ # YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
+ y('a {:ti} b', 'a 1997-07-16 19:20:30 b',
+ datetime(1997, 7, 16, 19, 20, 30))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30 b',
+ datetime(1997, 7, 16, 19, 20, 30))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30Z b',
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=utc))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30+01:00 b',
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30 +01:00 b',
+ datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60))
+
+ # YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
+ y('a {:ti} b', 'a 1997-07-16 19:20:30.500000 b',
+ datetime(1997, 7, 16, 19, 20, 30, 500000))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30.500000 b',
+ datetime(1997, 7, 16, 19, 20, 30, 500000))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30.5Z b',
+ datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=utc))
+ y('a {:ti} b', 'a 1997-07-16T19:20:30.5+01:00 b',
+ datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=tz60))
+
+ aest_d = datetime(2011, 11, 21, 10, 21, 36, tzinfo=aest)
+ dt = datetime(2011, 11, 21, 10, 21, 36)
+ dt00 = datetime(2011, 11, 21, 10, 21)
+ d = datetime(2011, 11, 21)
+
+ # te RFC2822 e-mail format datetime
+ y('a {:te} b', 'a Mon, 21 Nov 2011 10:21:36 +1000 b', aest_d)
+ y('a {:te} b', 'a Mon, 21 Nov 2011 10:21:36 +10:00 b', aest_d)
+ y('a {:te} b', 'a 21 Nov 2011 10:21:36 +1000 b', aest_d)
+
+ # tg global (day/month) format datetime
+ y('a {:tg} b', 'a 21/11/2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:tg} b', 'a 21/11/2011 10:21:36 AM +10:00 b', aest_d)
+ y('a {:tg} b', 'a 21-11-2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:tg} b', 'a 21/11/2011 10:21:36 +1000 b', aest_d)
+ y('a {:tg} b', 'a 21/11/2011 10:21:36 b', dt)
+ y('a {:tg} b', 'a 21/11/2011 10:21 b', dt00)
+ y('a {:tg} b', 'a 21-11-2011 b', d)
+ y('a {:tg} b', 'a 21-Nov-2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:tg} b', 'a 21-November-2011 10:21:36 AM +1000 b', aest_d)
+
+ # ta US (month/day) format datetime
+ y('a {:ta} b', 'a 11/21/2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:ta} b', 'a 11/21/2011 10:21:36 AM +10:00 b', aest_d)
+ y('a {:ta} b', 'a 11-21-2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:ta} b', 'a 11/21/2011 10:21:36 +1000 b', aest_d)
+ y('a {:ta} b', 'a 11/21/2011 10:21:36 b', dt)
+ y('a {:ta} b', 'a 11/21/2011 10:21 b', dt00)
+ y('a {:ta} b', 'a 11-21-2011 b', d)
+ y('a {:ta} b', 'a Nov-21-2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:ta} b', 'a November-21-2011 10:21:36 AM +1000 b', aest_d)
+ y('a {:ta} b', 'a November-21-2011 b', d)
+
+ # ts Linux System log format datetime
+ y('a {:ts} b', 'a Nov 21 10:21:36 b', datetime(datetime.today().year, 11, 21, 10, 21, 36))
+ y('a {:ts} b', 'a Nov 1 10:21:36 b', datetime(datetime.today().year, 11, 1, 10, 21, 36))
+ y('a {:ts} b', 'a Nov 1 03:21:36 b', datetime(datetime.today().year, 11, 1, 3, 21, 36))
+
+ # th HTTP log format date/time datetime
+ y('a {:th} b', 'a 21/Nov/2011:10:21:36 +1000 b', aest_d)
+ y('a {:th} b', 'a 21/Nov/2011:10:21:36 +10:00 b', aest_d)
+
+ d = datetime(2011, 11, 21, 10, 21, 36)
+
+ # tc ctime() format datetime
+ y('a {:tc} b', 'a Mon Nov 21 10:21:36 2011 b', d)
+
+ t530 = parse.FixedTzOffset(-5 * 60 - 30, '-5:30')
+ t830 = parse.FixedTzOffset(-8 * 60 - 30, '-8:30')
+
+ # tt Time time
+ y('a {:tt} b', 'a 10:21:36 AM +1000 b', time(10, 21, 36, tzinfo=aest))
+ y('a {:tt} b', 'a 10:21:36 AM +10:00 b', time(10, 21, 36, tzinfo=aest))
+ y('a {:tt} b', 'a 10:21:36 AM b', time(10, 21, 36))
+ y('a {:tt} b', 'a 10:21:36 PM b', time(22, 21, 36))
+ y('a {:tt} b', 'a 10:21:36 b', time(10, 21, 36))
+ y('a {:tt} b', 'a 10:21 b', time(10, 21))
+ y('a {:tt} b', 'a 10:21:36 PM -5:30 b', time(22, 21, 36, tzinfo=t530))
+ y('a {:tt} b', 'a 10:21:36 PM -530 b', time(22, 21, 36, tzinfo=t530))
+ y('a {:tt} b', 'a 10:21:36 PM -05:30 b', time(22, 21, 36, tzinfo=t530))
+ y('a {:tt} b', 'a 10:21:36 PM -0530 b', time(22, 21, 36, tzinfo=t530))
+ y('a {:tt} b', 'a 10:21:36 PM -08:30 b', time(22, 21, 36, tzinfo=t830))
+ y('a {:tt} b', 'a 10:21:36 PM -0830 b', time(22, 21, 36, tzinfo=t830))
+
+ def test_datetime_group_count(self):
+ # test we increment the group count correctly for datetimes
+ r = parse.parse('{:ti} {}', '1972-01-01 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:tg} {}', '1-1-1972 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:ta} {}', '1-1-1972 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:th} {}', '21/Nov/2011:10:21:36 +1000 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:te} {}', '21 Nov 2011 10:21:36 +1000 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:tc} {}', 'Mon Nov 21 10:21:36 2011 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+ r = parse.parse('{:tt} {}', '10:21 spam')
+ self.assertEqual(r.fixed[1], 'spam')
+
+ def test_mixed_types(self):
+ # stress-test: pull one of everything out of a string
+ r = parse.parse('''
+ letters: {:w}
+ non-letters: {:W}
+ whitespace: "{:s}"
+ non-whitespace: \t{:S}\n
+ digits: {:d} {:d}
+ non-digits: {:D}
+ numbers with thousands: {:n}
+ fixed-point: {:f}
+ floating-point: {:e}
+ general numbers: {:g} {:g}
+ binary: {:b}
+ octal: {:o}
+ hex: {:x}
+ ISO 8601 e.g. {:ti}
+ RFC2822 e.g. {:te}
+ Global e.g. {:tg}
+ US e.g. {:ta}
+ ctime() e.g. {:tc}
+ HTTP e.g. {:th}
+ time: {:tt}
+ final value: {}
+ ''',
+ '''
+ letters: abcdef_GHIJLK
+ non-letters: !@#%$ *^%
+ whitespace: " \t\n"
+ non-whitespace: \tabc\n
+ digits: 12345 0b1011011
+ non-digits: abcdef
+ numbers with thousands: 1,000
+ fixed-point: 100.2345
+ floating-point: 1.1e-10
+ general numbers: 1 1.1
+ binary: 0b1000
+ octal: 0o1000
+ hex: 0x1000
+ ISO 8601 e.g. 1972-01-20T10:21:36Z
+ RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000
+ Global e.g. 20/1/1972 10:21:36 AM +1:00
+ US e.g. 1/20/1972 10:21:36 PM +10:30
+ ctime() e.g. Sun Sep 16 01:03:52 1973
+ HTTP e.g. 21/Nov/2011:00:07:11 +0000
+ time: 10:21:36 PM -5:30
+ final value: spam
+ ''')
+ self.assertNotEqual(r, None)
+ self.assertEqual(r.fixed[22], 'spam')
+
+ def test_mixed_type_variant(self):
+ r = parse.parse('''
+ letters: {:w}
+ non-letters: {:W}
+ whitespace: "{:s}"
+ non-whitespace: \t{:S}\n
+ digits: {:d}
+ non-digits: {:D}
+ numbers with thousands: {:n}
+ fixed-point: {:f}
+ floating-point: {:e}
+ general numbers: {:g} {:g}
+ binary: {:b}
+ octal: {:o}
+ hex: {:x}
+ ISO 8601 e.g. {:ti}
+ RFC2822 e.g. {:te}
+ Global e.g. {:tg}
+ US e.g. {:ta}
+ ctime() e.g. {:tc}
+ HTTP e.g. {:th}
+ time: {:tt}
+ final value: {}
+ ''',
+ '''
+ letters: abcdef_GHIJLK
+ non-letters: !@#%$ *^%
+ whitespace: " \t\n"
+ non-whitespace: \tabc\n
+ digits: 0xabcdef
+ non-digits: abcdef
+ numbers with thousands: 1.000.000
+ fixed-point: 0.00001
+ floating-point: NAN
+ general numbers: 1.1e10 nan
+ binary: 0B1000
+ octal: 0O1000
+ hex: 0X1000
+ ISO 8601 e.g. 1972-01-20T10:21:36Z
+ RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000
+ Global e.g. 20/1/1972 10:21:36 AM +1:00
+ US e.g. 1/20/1972 10:21:36 PM +10:30
+ ctime() e.g. Sun Sep 16 01:03:52 1973
+ HTTP e.g. 21/Nov/2011:00:07:11 +0000
+ time: 10:21:36 PM -5:30
+ final value: spam
+ ''')
+ self.assertNotEqual(r, None)
+ self.assertEqual(r.fixed[21], 'spam')
+
+ def test_too_many_fields(self):
+ # Python 3.5 removed the limit of 100 named groups in a regular expression,
+ # so only test for the exception if the limit exists.
+ try:
+ re.compile("".join("(?P<n{n}>{n}-)".format(n=i) for i in range(101)))
+ except AssertionError:
+ p = parse.compile('{:ti}' * 15)
+ self.assertRaises(parse.TooManyFields, p.parse, '')
+
+ def test_letters(self):
+ res = parse.parse('{:l}', '')
+ self.assertIsNone(res)
+ res = parse.parse('{:l}', 'sPaM')
+ self.assertEqual(res.fixed, ('sPaM', ))
+ res = parse.parse('{:l}', 'sP4M')
+ self.assertIsNone(res)
+ res = parse.parse('{:l}', 'sP_M')
+ self.assertIsNone(res)
+
+
+class TestSearch(unittest.TestCase):
+ def test_basic(self):
+ # basic search() test
+ r = parse.search('a {} c', ' a b c ')
+ self.assertEqual(r.fixed, ('b',))
+
+ def test_multiline(self):
+ # multiline search() test
+ r = parse.search('age: {:d}\n', 'name: Rufus\nage: 42\ncolor: red\n')
+ self.assertEqual(r.fixed, (42,))
+
+ def test_pos(self):
+ # basic search() test
+ r = parse.search('a {} c', ' a b c ', 2)
+ self.assertEqual(r, None)
+
+ def test_no_evaluate_result(self):
+ match = parse.search('age: {:d}\n', 'name: Rufus\nage: 42\ncolor: red\n', evaluate_result=False)
+ r = match.evaluate_result()
+ self.assertEqual(r.fixed, (42,))
+
+
+class TestFindall(unittest.TestCase):
+ def test_findall(self):
+ # basic findall() test
+ s = ''.join(r.fixed[0] for r in parse.findall(">{}<",
+ "<p>some <b>bold</b> text</p>"))
+ self.assertEqual(s, "some bold text")
+
+ def test_no_evaluate_result(self):
+ # basic findall() test
+ s = ''.join(m.evaluate_result().fixed[0] for m in parse.findall(">{}<",
+ "<p>some <b>bold</b> text</p>", evaluate_result=False))
+ self.assertEqual(s, "some bold text")
+
+
+class TestBugs(unittest.TestCase):
+ def test_named_date_issue7(self):
+ r = parse.parse('on {date:ti}', 'on 2012-09-17')
+ self.assertEqual(r['date'], datetime(2012, 9, 17, 0, 0, 0))
+
+ # fix introduced regressions
+ r = parse.parse('a {:ti} b', 'a 1997-07-16T19:20 b')
+ self.assertEqual(r[0], datetime(1997, 7, 16, 19, 20, 0))
+ r = parse.parse('a {:ti} b', 'a 1997-07-16T19:20Z b')
+ utc = parse.FixedTzOffset(0, 'UTC')
+ self.assertEqual(r[0], datetime(1997, 7, 16, 19, 20, tzinfo=utc))
+ r = parse.parse('a {date:ti} b', 'a 1997-07-16T19:20Z b')
+ self.assertEqual(r['date'], datetime(1997, 7, 16, 19, 20, tzinfo=utc))
+
+ def test_dotted_type_conversion_pull_8(self):
+ # test pull request 8 which fixes type conversion related to dotted
+ # names being applied correctly
+ r = parse.parse('{a.b:d}', '1')
+ self.assertEqual(r['a.b'], 1)
+ r = parse.parse('{a_b:w} {a.b:d}', '1 2')
+ self.assertEqual(r['a_b'], '1')
+ self.assertEqual(r['a.b'], 2)
+
+ def test_pm_overflow_issue16(self):
+ r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:45 PM')
+ self.assertEqual(r[0], datetime(2011, 2, 1, 12, 45))
+
+ def test_pm_handling_issue57(self):
+ r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:15 PM')
+ self.assertEqual(r[0], datetime(2011, 2, 1, 12, 15))
+ r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:15 AM')
+ self.assertEqual(r[0], datetime(2011, 2, 1, 0, 15))
+
+ def test_user_type_with_group_count_issue60(self):
+ @parse.with_pattern(r'((\w+))', regex_group_count=2)
+ def parse_word_and_covert_to_uppercase(text):
+ return text.strip().upper()
+
+ @parse.with_pattern(r'\d+')
+ def parse_number(text):
+ return int(text)
+
+ # -- CASE: Use named (OK)
+ type_map = dict(Name=parse_word_and_covert_to_uppercase,
+ Number=parse_number)
+ r = parse.parse('Hello {name:Name} {number:Number}',
+ 'Hello Alice 42', extra_types=type_map)
+ self.assertEqual(r.named, dict(name='ALICE', number=42))
+
+ # -- CASE: Use unnamed/fixed (problematic)
+ r = parse.parse('Hello {:Name} {:Number}',
+ 'Hello Alice 42', extra_types=type_map)
+ self.assertEqual(r[0], 'ALICE')
+ self.assertEqual(r[1], 42)
+
+ def test_unmatched_brace_doesnt_match(self):
+ r = parse.parse("{who.txt", "hello")
+ self.assertIsNone(r)
+
+
+# -----------------------------------------------------------------------------
+# TEST SUPPORT FOR: TestParseType
+# -----------------------------------------------------------------------------
+class TestParseType(unittest.TestCase):
+
+ def assert_match(self, parser, text, param_name, expected):
+ result = parser.parse(text)
+ self.assertEqual(result[param_name], expected)
+
+ def assert_mismatch(self, parser, text, param_name):
+ result = parser.parse(text)
+ self.assertTrue(result is None)
+
+ def assert_fixed_match(self, parser, text, expected):
+ result = parser.parse(text)
+ self.assertEqual(result.fixed, expected)
+
+ def assert_fixed_mismatch(self, parser, text):
+ result = parser.parse(text)
+ self.assertEqual(result, None)
+
+ def test_pattern_should_be_used(self):
+ def parse_number(text):
+ return int(text)
+ parse_number.pattern = r"\d+"
+ parse_number.name = "Number" # For testing only.
+
+ extra_types = {parse_number.name: parse_number}
+ format = "Value is {number:Number} and..."
+ parser = parse.Parser(format, extra_types)
+
+ self.assert_match(parser, "Value is 42 and...", "number", 42)
+ self.assert_match(parser, "Value is 00123 and...", "number", 123)
+ self.assert_mismatch(parser, "Value is ALICE and...", "number")
+ self.assert_mismatch(parser, "Value is -123 and...", "number")
+
+ def test_pattern_should_be_used2(self):
+ def parse_yesno(text):
+ return parse_yesno.mapping[text.lower()]
+ parse_yesno.mapping = {
+ "yes": True, "no": False,
+ "on": True, "off": False,
+ "true": True, "false": False,
+ }
+ parse_yesno.pattern = r"|".join(parse_yesno.mapping.keys())
+ parse_yesno.name = "YesNo" # For testing only.
+
+ extra_types = {parse_yesno.name: parse_yesno}
+ format = "Answer: {answer:YesNo}"
+ parser = parse.Parser(format, extra_types)
+
+ # -- ENSURE: Known enum values are correctly extracted.
+ for value_name, value in parse_yesno.mapping.items():
+ text = "Answer: %s" % value_name
+ self.assert_match(parser, text, "answer", value)
+
+ # -- IGNORE-CASE: In parsing, calls type converter function !!!
+ self.assert_match(parser, "Answer: YES", "answer", True)
+ self.assert_mismatch(parser, "Answer: __YES__", "answer")
+
+ def test_with_pattern(self):
+ ab_vals = dict(a=1, b=2)
+
+ @parse.with_pattern(r'[ab]')
+ def ab(text):
+ return ab_vals[text]
+
+ parser = parse.Parser('test {result:ab}', {'ab': ab})
+ self.assert_match(parser, 'test a', 'result', 1)
+ self.assert_match(parser, 'test b', 'result', 2)
+ self.assert_mismatch(parser, "test c", "result")
+
+ def test_with_pattern_and_regex_group_count(self):
+ # -- SPECIAL-CASE: Regex-grouping is used in user-defined type
+ # NOTE: Missing or wroung regex_group_counts cause problems
+ # with parsing following params.
+ @parse.with_pattern(r'(meter|kilometer)', regex_group_count=1)
+ def parse_unit(text):
+ return text.strip()
+
+ @parse.with_pattern(r'\d+')
+ def parse_number(text):
+ return int(text)
+
+ type_converters = dict(Number=parse_number, Unit=parse_unit)
+ # -- CASE: Unnamed-params (affected)
+ parser = parse.Parser('test {:Unit}-{:Number}', type_converters)
+ self.assert_fixed_match(parser, 'test meter-10', ('meter', 10))
+ self.assert_fixed_match(parser, 'test kilometer-20', ('kilometer', 20))
+ self.assert_fixed_mismatch(parser, 'test liter-30')
+
+ # -- CASE: Named-params (uncritical; should not be affected)
+ # REASON: Named-params have additional, own grouping.
+ parser2 = parse.Parser('test {unit:Unit}-{value:Number}', type_converters)
+ self.assert_match(parser2, 'test meter-10', 'unit', 'meter')
+ self.assert_match(parser2, 'test meter-10', 'value', 10)
+ self.assert_match(parser2, 'test kilometer-20', 'unit', 'kilometer')
+ self.assert_match(parser2, 'test kilometer-20', 'value', 20)
+ self.assert_mismatch(parser2, 'test liter-30', 'unit')
+
+ def test_with_pattern_and_wrong_regex_group_count_raises_error(self):
+ # -- SPECIAL-CASE:
+ # Regex-grouping is used in user-defined type, but wrong value is provided.
+ @parse.with_pattern(r'(meter|kilometer)', regex_group_count=1)
+ def parse_unit(text):
+ return text.strip()
+
+ @parse.with_pattern(r'\d+')
+ def parse_number(text):
+ return int(text)
+
+ # -- CASE: Unnamed-params (affected)
+ BAD_REGEX_GROUP_COUNTS_AND_ERRORS = [
+ (None, ValueError),
+ (0, ValueError),
+ (2, IndexError),
+ ]
+ for bad_regex_group_count, error_class in BAD_REGEX_GROUP_COUNTS_AND_ERRORS:
+ parse_unit.regex_group_count = bad_regex_group_count # -- OVERRIDE-HERE
+ type_converters = dict(Number=parse_number, Unit=parse_unit)
+ parser = parse.Parser('test {:Unit}-{:Number}', type_converters)
+ self.assertRaises(error_class, parser.parse, 'test meter-10')
+
+ def test_with_pattern_and_regex_group_count_is_none(self):
+ # -- CORNER-CASE: Increase code-coverage.
+ data_values = dict(a=1, b=2)
+
+ @parse.with_pattern(r'[ab]')
+ def parse_data(text):
+ return data_values[text]
+ parse_data.regex_group_count = None # ENFORCE: None
+
+ # -- CASE: Unnamed-params
+ parser = parse.Parser('test {:Data}', {'Data': parse_data})
+ self.assert_fixed_match(parser, 'test a', (1,))
+ self.assert_fixed_match(parser, 'test b', (2,))
+ self.assert_fixed_mismatch(parser, 'test c')
+
+ # -- CASE: Named-params
+ parser2 = parse.Parser('test {value:Data}', {'Data': parse_data})
+ self.assert_match(parser2, 'test a', 'value', 1)
+ self.assert_match(parser2, 'test b', 'value', 2)
+ self.assert_mismatch(parser2, 'test c', 'value')
+
+ def test_case_sensitivity(self):
+ r = parse.parse('SPAM {} SPAM', 'spam spam spam')
+ self.assertEqual(r[0], 'spam')
+ self.assertEqual(parse.parse('SPAM {} SPAM', 'spam spam spam', case_sensitive=True), None)
+
+ def test_decimal_value(self):
+ value = Decimal('5.5')
+ str_ = 'test {}'.format(value)
+ parser = parse.Parser('test {:F}')
+ self.assertEqual(parser.parse(str_)[0], value)
+
+ def test_width_str(self):
+ res = parse.parse('{:.2}{:.2}', 'look')
+ self.assertEqual(res.fixed, ('lo', 'ok'))
+ res = parse.parse('{:2}{:2}', 'look')
+ self.assertEqual(res.fixed, ('lo', 'ok'))
+ res = parse.parse('{:4}{}', 'look at that')
+ self.assertEqual(res.fixed, ('look', ' at that'))
+
+ def test_width_constraints(self):
+ res = parse.parse('{:4}', 'looky')
+ self.assertEqual(res.fixed, ('looky', ))
+ res = parse.parse('{:4.4}', 'looky')
+ self.assertIsNone(res)
+ res = parse.parse('{:4.4}', 'ook')
+ self.assertIsNone(res)
+ res = parse.parse('{:4}{:.4}', 'look at that')
+ self.assertEqual(res.fixed, ('look at ', 'that'))
+
+ def test_width_multi_int(self):
+ res = parse.parse('{:02d}{:02d}', '0440')
+ self.assertEqual(res.fixed, (4, 40))
+ res = parse.parse('{:03d}{:d}', '04404')
+ self.assertEqual(res.fixed, (44, 4))
+
+ def test_width_empty_input(self):
+ res = parse.parse('{:.2}', '')
+ self.assertIsNone(res)
+ res = parse.parse('{:2}', 'l')
+ self.assertIsNone(res)
+ res = parse.parse('{:2d}', '')
+ self.assertIsNone(res)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# Copyright (c) 2011 eKit.com Inc (http://www.ekit.com/)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# vim: set filetype=python ts=4 sw=4 et si tw=75