changeset 2627:8e767010c418

Merge main.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Mon, 23 Jul 2007 21:51:49 +0200
parents a791fc964e09 (current diff) f3b684afca81 (diff)
children 35690680ecac
files
diffstat 17 files changed, 1235 insertions(+), 455 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/test_sourcecode.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/_tests/test_sourcecode.py	Mon Jul 23 21:51:49 2007 +0200
@@ -30,7 +30,7 @@
 FIX_TS_RE = re.compile(r' +$', re.M) # 'fix' mode: everything matching the trailing space re will be removed
 
 RECENTLY = time.time() - 7 * 24*60*60 # we only check stuff touched recently.
-RECENTLY = 0 # check everything!
+#RECENTLY = 0 # check everything!
 # After doing a fresh clone, this procedure is recommended:
 # 1. Run the tests once to see if everything is OK (as cloning updates the mtime,
 #    it will test every file).
--- a/MoinMoin/_tests/test_wikiutil.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/_tests/test_wikiutil.py	Mon Jul 23 21:51:49 2007 +0200
@@ -2,12 +2,13 @@
 """
     MoinMoin - MoinMoin.wikiutil Tests
 
-    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+                2007 by MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
 import py
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
+
 from MoinMoin import wikiutil
 
 
@@ -111,7 +112,7 @@
     def testSystemPagesGroupNotEmpty(self):
         assert self.request.dicts.members('SystemPagesGroup')
 
-class TestSystemPage(unittest.TestCase):
+class TestSystemPage:
     systemPages = (
         # First level, on SystemPagesGroup
         'SystemPagesInEnglishGroup',
@@ -126,14 +127,12 @@
     def testSystemPage(self):
         """wikiutil: good system page names accepted, bad rejected"""
         for name in self.systemPages:
-            self.assert_(wikiutil.isSystemPage(self.request, name),
-                '"%(name)s" is a system page' % locals())
+            assert wikiutil.isSystemPage(self.request, name)
         for name in self.notSystemPages:
-            self.failIf(wikiutil.isSystemPage(self.request, name),
-                '"%(name)s" is NOT a system page' % locals())
+            assert not  wikiutil.isSystemPage(self.request, name)
 
 
-class TestTemplatePage(unittest.TestCase):
+class TestTemplatePage:
     good = (
         'aTemplate',
         'MyTemplate',
@@ -146,55 +145,446 @@
         'XTemplateInFront',
     )
 
-    # require default page_template_regex config
-    def setUp(self):
-        self.config = self.TestConfig(defaults=['page_template_regex'])
-    def tearDown(self):
-        self.config.restore()
-
     def testTemplatePage(self):
         """wikiutil: good template names accepted, bad rejected"""
         for name in self.good:
-            self.assert_(wikiutil.isTemplatePage(self.request, name),
-                '"%(name)s" is a valid template name' % locals())
+            assert  wikiutil.isTemplatePage(self.request, name)
         for name in self.bad:
-            self.failIf(wikiutil.isTemplatePage(self.request, name),
-                '"%(name)s" is NOT a valid template name' % locals())
+            assert not wikiutil.isTemplatePage(self.request, name)
 
 
-class TestParmeterParser(unittest.TestCase):
-
-    def testNoWantedArguments(self):
-        args = ''
-        argParser = wikiutil.ParameterParser('')
-        self.arg_list, self.arg_dict = argParser.parse_parameters(args)
-        result = len(self.arg_dict)
-        expected = 0
-        self.assert_(result == expected,
-                     'Expected "%(expected)s" but got "%(result)s"' % locals())
+class TestParmeterParser:
 
-    def testWantedArguments(self):
-        test_args = ('',
-                     'width=100',
-                     'width=100, height=200', )
+    def testParameterParser(self):
+        tests = [
+            # trivial
+            ('', '', 0, {}),
 
-        argParser = wikiutil.ParameterParser("%(width)s%(height)s")
-        for args in test_args:
-            self.arg_list, self.arg_dict = argParser.parse_parameters(args)
-            result = len(self.arg_dict)
-            expected = 2
-            self.assert_(result == expected,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+            # fixed
+            ('%s%i%f%b', '"test",42,23.0,True', 4, {0: 'test', 1: 42, 2: 23.0, 3: True}),
+
+            # fixed and named
+            ('%s%(x)i%(y)i', '"test"', 1, {0: 'test', 'x': None, 'y': None}),
+            ('%s%(x)i%(y)i', '"test",1', 1, {0: 'test', 'x': 1, 'y': None}),
+            ('%s%(x)i%(y)i', '"test",1,2', 1, {0: 'test', 'x': 1, 'y': 2}),
+            ('%s%(x)i%(y)i', '"test",x=1', 1, {0: 'test', 'x': 1, 'y': None}),
+            ('%s%(x)i%(y)i', '"test",x=1,y=2', 1, {0: 'test', 'x': 1, 'y': 2}),
+            ('%s%(x)i%(y)i', '"test",y=2', 1, {0: 'test', 'x': None, 'y': 2}),
+
+            # test mixed acceptance
+            ("%ifs", '100', 1, {0: 100}),
+            ("%ifs", '100.0', 1, {0: 100.0}),
+            ("%ifs", '"100"', 1, {0: "100"}),
+
+            # boolean
+            ("%(t)b%(f)b", '', 0, {'t': None, 'f': None}),
+            ("%(t)b%(f)b", 't=1', 0, {'t': True, 'f': None}),
+            ("%(t)b%(f)b", 'f=False', 0, {'t': None, 'f': False}),
+            ("%(t)b%(f)b", 't=True, f=0', 0, {'t': True, 'f': False}),
+
+            # integer
+            ("%(width)i%(height)i", '', 0, {'width': None, 'height': None}),
+            ("%(width)i%(height)i", 'width=100', 0, {'width': 100, 'height': None}),
+            ("%(width)i%(height)i", 'height=200', 0, {'width': None, 'height': 200}),
+            ("%(width)i%(height)i", 'width=100, height=200', 0, {'width': 100, 'height': 200}),
+
+            # float
+            ("%(width)f%(height)f", '', 0, {'width': None, 'height': None}),
+            ("%(width)f%(height)f", 'width=100.0', 0, {'width': 100.0, 'height': None}),
+            ("%(width)f%(height)f", 'height=2.0E2', 0, {'width': None, 'height': 200.0}),
+            ("%(width)f%(height)f", 'width=1000.0E-1, height=200.0', 0, {'width': 100.0, 'height': 200.0}),
+
+            # string
+            ("%(width)s%(height)s", '', 0, {'width': None, 'height': None}),
+            ("%(width)s%(height)s", 'width="really wide"', 0, {'width': 'really wide', 'height': None}),
+            ("%(width)s%(height)s", 'height="not too high"', 0, {'width': None, 'height': 'not too high'}),
+            ("%(width)s%(height)s", 'width="really wide", height="not too high"', 0, {'width': 'really wide', 'height': 'not too high'}),
+            # conversion from given type to expected type
+            ("%(width)s%(height)s", 'width=100', 0, {'width': '100', 'height': None}),
+            ("%(width)s%(height)s", 'width=100, height=200', 0, {'width': '100', 'height': '200'}),
+
+            # complex test
+            ("%i%sf%s%ifs%(a)s|%(b)s", ' 4,"DI\'NG", b=retry, a="DING"', 2, {0: 4, 1: "DI'NG", 'a': 'DING', 'b': 'retry'}),
+
+            ]
+        for format, args, expected_fixed_count, expected_dict in tests:
+            argParser = wikiutil.ParameterParser(format)
+            fixed_count, arg_dict = argParser.parse_parameters(args)
+            assert (fixed_count, arg_dict) == (expected_fixed_count, expected_dict)
 
     def testTooMuchWantedArguments(self):
-        py.test.skip("fails because of unfinished wikiutil.ParameterParser code crashing")
         args = 'width=100, height=200, alt=Example'
         argParser = wikiutil.ParameterParser("%(width)s%(height)s")
-        self.arg_list, self.arg_dict = argParser.parse_parameters(args)
-        result = len(self.arg_dict)
-        expected = 2
-        self.assert_(result == expected,
-                     'Expected "%(expected)s" but got "%(result)s"' % locals())
+        py.test.raises(ValueError, argParser.parse_parameters, args)
+
+    def testMalformedArguments(self):
+        args = '='
+        argParser = wikiutil.ParameterParser("%(width)s%(height)s")
+        py.test.raises(ValueError, argParser.parse_parameters, args)
+
+    def testWrongTypeFixedPosArgument(self):
+        args = '0.0'
+        argParser = wikiutil.ParameterParser("%b")
+        py.test.raises(ValueError, argParser.parse_parameters, args)
+
+    def testWrongTypeNamedArgument(self):
+        args = 'flag=0.0'
+        argParser = wikiutil.ParameterParser("%(flag)b")
+        py.test.raises(ValueError, argParser.parse_parameters, args)
+
+
+class TestParamParsing:
+    def testMacroArgs(self):
+        abcd = [u'a', u'b', u'c', u'd']
+        abcd_dict = {u'a': u'1', u'b': u'2', u'c': u'3', u'd': u'4'}
+        tests = [
+                  # regular and quoting tests
+                  (u'd = 4,c=3,b=2,a= 1 ',    ([], abcd_dict, [])),
+                  (u'a,b,c,d',                (abcd, {}, [])),
+                  (u' a , b , c , d ',        (abcd, {}, [])),
+                  (u'   a   ',                ([u'a'], {}, [])),
+                  (u'"  a  "',                ([u'  a  '], {}, [])),
+                  (u'a,b,c,d, "a,b,c,d"',     (abcd+[u'a,b,c,d'], {}, [])),
+                  (u'quote " :), b',          ([u'quote " :)', u'b'], {}, [])),
+                  (u'"quote "" :)", b',       ([u'quote " :)', u'b'], {}, [])),
+                  (u'=7',                     ([], {u'': u'7'}, [])),
+                  (u',,',                     ([None, None, None], {}, [])),
+                  (u',"",',                   ([None, u'', None], {}, [])),
+                  (u',"", ""',                ([None, u'', u''], {}, [])),
+                  (u'  ""  ,"", ""',          ([u'', u'', u''], {}, [])),
+                  # some name=value test
+                  (u'd = 4,c=3,b=2,a= 1 ',    ([], abcd_dict, [])),
+                  (u'd=d,e="a,b,c,d"',        ([], {u'd': u'd',
+                                                    u'e': u'a,b,c,d'}, [])),
+                  (u'd = d,e = "a,b,c,d"',    ([], {u'd': u'd',
+                                                    u'e': u'a,b,c,d'}, [])),
+                  (u'd = d, e = "a,b,c,d"',   ([], {u'd': u'd',
+                                                    u'e': u'a,b,c,d'}, [])),
+                  (u'd = , e = "a,b,c,d"',    ([], {u'd': None,
+                                                    u'e': u'a,b,c,d'}, [])),
+                  (u'd = "", e = "a,b,c,d"',  ([], {u'd': u'',
+                                                    u'e': u'a,b,c,d'}, [])),
+                  (u'd = "", e = ',           ([], {u'd': u'', u'e': None},
+                                               [])),
+                  (u'd=""',                   ([], {u'd': u''}, [])),
+                  (u'd = "", e = ""',         ([], {u'd': u'', u'e': u''},
+                                               [])),
+                  # no, None as key isn't accepted
+                  (u' = "",  e = ""',         ([], {u'': u'', u'e': u''},
+                                               [])),
+                  # can quote both name and value:
+                  (u'd = d," e "= "a,b,c,d"', ([], {u'd': u'd',
+                                                    u' e ': u'a,b,c,d'}, [])),
+                  # trailing args
+                  (u'1,2,a=b,3,4',            ([u'1', u'2'], {u'a': u'b'},
+                                               [u'3', u'4'])),
+                  # can quote quotes:
+                  (u'd = """d"',              ([], {u'd': u'"d'}, [])),
+                  (u'd = """d"""',            ([], {u'd': u'"d"'}, [])),
+                  (u'd = "d"" ", e=7',        ([], {u'd': u'd" ', u'e': u'7'},
+                                               [])),
+                  (u'd = "d""", e=8',         ([], {u'd': u'd"', u'e': u'8'},
+                                               [])),
+                ]
+        for args, expected in tests:
+            result = wikiutil.parse_quoted_separated(args)
+            assert expected == result
+            for val in result[0]:
+                assert val is None or isinstance(val, unicode)
+            for val in result[1].keys():
+                assert val is None or isinstance(val, unicode)
+            for val in result[1].values():
+                assert val is None or isinstance(val, unicode)
+            for val in result[2]:
+                assert val is None or isinstance(val, unicode)
+
+    def testLimited(self):
+        tests = [
+                  # regular and quoting tests
+                  (u'd = 4,c=3,b=2,a= 1 ',    ([], {u'd': u'4',
+                                                    u'c': u'3,b=2,a= 1'}, [])),
+                  (u'a,b,c,d',                ([u'a', u'b,c,d'], {}, [])),
+                  (u'a=b,b,c,d',              ([], {u'a': u'b'}, [u'b,c,d'])),
+                ]
+        for args, expected in tests:
+            result = wikiutil.parse_quoted_separated(args, seplimit=1)
+            assert expected == result
+            for val in result[0]:
+                assert val is None or isinstance(val, unicode)
+            for val in result[1].keys():
+                assert val is None or isinstance(val, unicode)
+            for val in result[1].values():
+                assert val is None or isinstance(val, unicode)
+            for val in result[2]:
+                assert val is None or isinstance(val, unicode)
+
+    def testNoNameValue(self):
+        abcd = [u'a', u'b', u'c', u'd']
+        tests = [
+                  # regular and quoting tests
+                  (u'd = 4,c=3,b=2,a= 1 ',    [u'd = 4', u'c=3',
+                                               u'b=2', u'a= 1']),
+                  (u'a,b,c,d',                abcd),
+                  (u' a , b , c , d ',        abcd),
+                  (u'   a   ',                [u'a']),
+                  (u'"  a  "',                [u'  a  ']),
+                  (u'a,b,c,d, "a,b,c,d"',     abcd + [u'a,b,c,d']),
+                  (u'quote " :), b',          [u'quote " :)', u'b']),
+                  (u'"quote "" :)", b',       [u'quote " :)', u'b']),
+                  (u'd=d,e="a,b,c,d"',        [u'd=d', u'e="a', u'b',
+                                               u'c', u'd"']),
+                ]
+        for args, expected in tests:
+            result = wikiutil.parse_quoted_separated(args, name_value=False)
+            assert expected == result
+            for val in result:
+                assert val is None or isinstance(val, unicode)
+
+
+class TestArgGetters:
+    def testGetBoolean(self):
+        tests = [
+            # default testing for None value
+            (None, None, None, None),
+            (None, None, False, False),
+            (None, None, True, True),
+
+            # some real values
+            (u'0', None, None, False),
+            (u'1', None, None, True),
+            (u'false', None, None, False),
+            (u'true', None, None, True),
+            (u'FALSE', None, None, False),
+            (u'TRUE', None, None, True),
+            (u'no', None, None, False),
+            (u'yes', None, None, True),
+            (u'NO', None, None, False),
+            (u'YES', None, None, True),
+        ]
+        for arg, name, default, expected in tests:
+            assert wikiutil.get_bool(self.request, arg, name, default) == expected
+
+    def testGetBooleanRaising(self):
+        # wrong default type
+        py.test.raises(AssertionError, wikiutil.get_bool, self.request, None, None, 42)
+
+        # anything except None or unicode raises TypeError
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, True)
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, 42)
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, 42.0)
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, '')
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, tuple())
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, [])
+        py.test.raises(TypeError, wikiutil.get_bool, self.request, {})
+
+        # any value not convertable to boolean raises ValueError
+        py.test.raises(ValueError, wikiutil.get_bool, self.request, u'')
+        py.test.raises(ValueError, wikiutil.get_bool, self.request, u'42')
+        py.test.raises(ValueError, wikiutil.get_bool, self.request, u'wrong')
+        py.test.raises(ValueError, wikiutil.get_bool, self.request, u'"True"') # must not be quoted!
+
+    def testGetInt(self):
+        tests = [
+            # default testing for None value
+            (None, None, None, None),
+            (None, None, -23, -23),
+            (None, None, 42, 42),
+
+            # some real values
+            (u'0', None, None, 0),
+            (u'42', None, None, 42),
+            (u'-23', None, None, -23),
+        ]
+        for arg, name, default, expected in tests:
+            assert wikiutil.get_int(self.request, arg, name, default) == expected
+
+    def testGetIntRaising(self):
+        # wrong default type
+        py.test.raises(AssertionError, wikiutil.get_int, self.request, None, None, 42.23)
+
+        # anything except None or unicode raises TypeError
+        py.test.raises(TypeError, wikiutil.get_int, self.request, True)
+        py.test.raises(TypeError, wikiutil.get_int, self.request, 42)
+        py.test.raises(TypeError, wikiutil.get_int, self.request, 42.0)
+        py.test.raises(TypeError, wikiutil.get_int, self.request, '')
+        py.test.raises(TypeError, wikiutil.get_int, self.request, tuple())
+        py.test.raises(TypeError, wikiutil.get_int, self.request, [])
+        py.test.raises(TypeError, wikiutil.get_int, self.request, {})
+
+        # any value not convertable to int raises ValueError
+        py.test.raises(ValueError, wikiutil.get_int, self.request, u'')
+        py.test.raises(ValueError, wikiutil.get_int, self.request, u'23.42')
+        py.test.raises(ValueError, wikiutil.get_int, self.request, u'wrong')
+        py.test.raises(ValueError, wikiutil.get_int, self.request, u'"4711"') # must not be quoted!
+
+    def testGetFloat(self):
+        tests = [
+            # default testing for None value
+            (None, None, None, None),
+            (None, None, -23.42, -23.42),
+            (None, None, 42.23, 42.23),
+
+            # some real values
+            (u'0', None, None, 0),
+            (u'42.23', None, None, 42.23),
+            (u'-23.42', None, None, -23.42),
+            (u'-23.42E3', None, None, -23.42E3),
+            (u'23.42E-3', None, None, 23.42E-3),
+        ]
+        for arg, name, default, expected in tests:
+            assert wikiutil.get_float(self.request, arg, name, default) == expected
+
+    def testGetFloatRaising(self):
+        # wrong default type
+        py.test.raises(AssertionError, wikiutil.get_float, self.request, None, None, u'42')
+
+        # anything except None or unicode raises TypeError
+        py.test.raises(TypeError, wikiutil.get_float, self.request, True)
+        py.test.raises(TypeError, wikiutil.get_float, self.request, 42)
+        py.test.raises(TypeError, wikiutil.get_float, self.request, 42.0)
+        py.test.raises(TypeError, wikiutil.get_float, self.request, '')
+        py.test.raises(TypeError, wikiutil.get_float, self.request, tuple())
+        py.test.raises(TypeError, wikiutil.get_float, self.request, [])
+        py.test.raises(TypeError, wikiutil.get_float, self.request, {})
+
+        # any value not convertable to int raises ValueError
+        py.test.raises(ValueError, wikiutil.get_float, self.request, u'')
+        py.test.raises(ValueError, wikiutil.get_float, self.request, u'wrong')
+        py.test.raises(ValueError, wikiutil.get_float, self.request, u'"47.11"') # must not be quoted!
+
+    def testGetUnicode(self):
+        tests = [
+            # default testing for None value
+            (None, None, None, None),
+            (None, None, u'', u''),
+            (None, None, u'abc', u'abc'),
+
+            # some real values
+            (u'', None, None, u''),
+            (u'abc', None, None, u'abc'),
+            (u'"abc"', None, None, u'"abc"'),
+        ]
+        for arg, name, default, expected in tests:
+            assert wikiutil.get_unicode(self.request, arg, name, default) == expected
+
+    def testGetUnicodeRaising(self):
+        # wrong default type
+        py.test.raises(AssertionError, wikiutil.get_unicode, self.request, None, None, 42)
+
+        # anything except None or unicode raises TypeError
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, True)
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, 42)
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, 42.0)
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, '')
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, tuple())
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, [])
+        py.test.raises(TypeError, wikiutil.get_unicode, self.request, {})
+
+
+def _test_invoke_int(i=int):
+    assert i == 1
+
+
+def _test_invoke_int_fixed(a, b, i=int):
+    assert a == 7
+    assert b == 8
+    assert i == 1 or i is None
+
+
+class TestExtensionInvoking:
+    def _test_invoke_bool(self, b=bool):
+        assert b is False
+
+    def _test_invoke_bool_def(self, v=bool, b=False):
+        assert b == v
+        assert isinstance(b, bool)
+        assert isinstance(v, bool)
+
+    def _test_invoke_int_None(self, i=int):
+        assert i == 1 or i is None
+
+    def _test_invoke_float_None(self, i=float):
+        assert i == 1.4 or i is None
+
+    def _test_invoke_float_required(self, i=wikiutil.required_arg(float)):
+        assert i == 1.4
+
+    def _test_invoke_choice(self, a, choice=[u'a', u'b', u'c']):
+        assert a == 7
+        assert choice == u'a'
+
+    def _test_invoke_choicet(self, a, choice=(u'a', u'b', u'c')):
+        assert a == 7
+        assert choice == u'a'
+
+    def _test_trailing(self, a, _trailing_args=[]):
+        assert _trailing_args == [u'a']
+
+    def _test_arbitrary_kw(self, expect, _kwargs={}):
+        assert _kwargs == expect
+
+    def testInvoke(self):
+        ief = wikiutil.invoke_extension_function
+        ief(self.request, self._test_invoke_bool, u'False')
+        ief(self.request, self._test_invoke_bool, u'b=False')
+        ief(self.request, _test_invoke_int, u'1')
+        ief(self.request, _test_invoke_int, u'i=1')
+        ief(self.request, self._test_invoke_bool_def, u'False, False')
+        ief(self.request, self._test_invoke_bool_def, u'b=False, v=False')
+        ief(self.request, self._test_invoke_bool_def, u'False')
+        ief(self.request, self._test_invoke_int_None, u'i=1')
+        ief(self.request, self._test_invoke_int_None, u'i=')
+        ief(self.request, self._test_invoke_int_None, u'')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_int_None, u'x')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_int_None, u'""')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_int_None, u'i=""')
+        py.test.raises(ValueError, ief, self.request,
+                       _test_invoke_int_fixed, u'a=7', [7, 8])
+        ief(self.request, _test_invoke_int_fixed, u'i=1', [7, 8])
+        py.test.raises(ValueError, ief, self.request,
+                       _test_invoke_int_fixed, u'i=""', [7, 8])
+        ief(self.request, _test_invoke_int_fixed, u'i=', [7, 8])
+
+        for choicefn in (self._test_invoke_choice, self._test_invoke_choicet):
+            ief(self.request, choicefn, u'', [7])
+            ief(self.request, choicefn, u'choice=a', [7])
+            ief(self.request, choicefn, u'choice=', [7])
+            ief(self.request, choicefn, u'choice="a"', [7])
+            py.test.raises(ValueError, ief, self.request,
+                           choicefn, u'x', [7])
+            py.test.raises(ValueError, ief, self.request,
+                           choicefn, u'choice=x', [7])
+
+        ief(self.request, self._test_invoke_float_None, u'i=1.4')
+        ief(self.request, self._test_invoke_float_None, u'i=')
+        ief(self.request, self._test_invoke_float_None, u'')
+        ief(self.request, self._test_invoke_float_None, u'1.4')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_float_None, u'x')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_float_None, u'""')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_float_None, u'i=""')
+        ief(self.request, self._test_trailing, u'a=7, a')
+        ief(self.request, self._test_trailing, u'7, a')
+        ief(self.request, self._test_arbitrary_kw, u'test=x, \xc3=test',
+            [{u'\xc3': 'test', 'test': u'x'}])
+        ief(self.request, self._test_arbitrary_kw, u'test=x, "\xc3"=test',
+            [{u'\xc3': 'test', 'test': u'x'}])
+        ief(self.request, self._test_arbitrary_kw, u'test=x, "7 \xc3"=test',
+            [{u'7 \xc3': 'test', 'test': u'x'}])
+        ief(self.request, self._test_arbitrary_kw, u'test=x, 7 \xc3=test',
+            [{u'7 \xc3': 'test', 'test': u'x'}])
+        ief(self.request, self._test_arbitrary_kw, u'7 \xc3=test, test= x ',
+            [{u'7 \xc3': 'test', 'test': u'x'}])
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_float_required, u'')
+        ief(self.request, self._test_invoke_float_required, u'1.4')
+        ief(self.request, self._test_invoke_float_required, u'i=1.4')
+        py.test.raises(ValueError, ief, self.request,
+                       self._test_invoke_float_required, u',')
 
 coverage_modules = ['MoinMoin.wikiutil']
-
--- a/MoinMoin/action/AttachFile.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/action/AttachFile.py	Mon Jul 23 21:51:49 2007 +0200
@@ -343,7 +343,7 @@
                 viewlink = '<a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;do=view&amp;target=%(urlfile)s">%(label_view)s</a>' % parmdict
 
             if (packages.ZipPackage(request, os.path.join(attach_dir, file).encode(config.charset)).isPackage() and
-                 request.user.isSuperUser()):
+                 request.user.isSuperUser() and request.user.may.write(pagename)):
                 viewlink += ' | <a href="%(baseurl)s/%(urlpagename)s?action=%(action)s&amp;do=install&amp;target=%(urlfile)s">%(label_install)s</a>' % parmdict
             elif (zipfile.is_zipfile(os.path.join(attach_dir, file).encode(config.charset)) and
                 mt.minor == 'zip' and request.user.may.read(pagename) and request.user.may.delete(pagename)
--- a/MoinMoin/macro/Hits.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/macro/Hits.py	Mon Jul 23 21:51:49 2007 +0200
@@ -2,60 +2,34 @@
 """
     MoinMoin - Hits Macro
 
-    This macro is used to show the cummulative hits of the wikipage where the Macro is called from.
-    Optional you could count how much this or all pages was changed or viewed.
+    This macro is used to show the cumulative hits of the wikipage where the Macro is called from.
+    Optionally you could count how much this page or all pages were changed or viewed.
 
     [[Hits([all=(0,1)],[filter=(VIEWPAGE,SAVEPAGE)]]
 
-        all: if set to 1 then cummulative hits over all wiki pages is returned. Default is 0
+        all: if set to 1/True/yes then cumulative hits over all wiki pages is returned.
+             Default is 0/False/no.
         filter: if set to SAVEPAGE then the saved pages are counted. Default is VIEWPAGE.
 
-   @copyright: 2004-2007 MoinMoin:ReimarBauer
+   @copyright: 2004-2007 MoinMoin:ReimarBauer,
                2005 BenjaminVrolijk
    @license: GNU GPL, see COPYING for details.
 """
+
 Dependencies = ['time'] # do not cache
 
 from MoinMoin import wikiutil
 from MoinMoin.logfile import eventlog
 
-class Hits:
-    def __init__(self, macro, args):
-        self.macro = macro
-        self.request = macro.request
-        self.formatter = macro.formatter
-        argParser = wikiutil.ParameterParser("%(all)s%(filter)s")
-        try:
-            self.arg_list, self.arg_dict = argParser.parse_parameters(args)
-        except ValueError:
-            # TODO Set defaults until raise in ParameterParser.parse_parameters is changed
-            self.arg_dict = {}
-            self.arg_dict["filter"] = None
-            self.arg_dict["all"] = 0
-
-        self.count = 0
-
-    def renderInText(self):
-        return self.formatter.text("%s" % (self.getHits()))
 
-    def getHits(self):
-        formatter = self.macro.formatter
-        kw = self.arg_dict
-        if not kw["filter"]: kw["filter"] = "VIEWPAGE"
+def macro_Hits(macro, all=False, filter=(u'VIEWPAGE', u'SAVEPAGE')):
+    this_page = macro.formatter.page.page_name
+    event_log = eventlog.EventLog(macro.request)
+    event_log.set_filter([str(filter)])
+    count = 0
+    for event in event_log.reverse():
+        pagename = event[2].get('pagename')
+        if all or pagename == this_page:
+            count += 1
 
-        event_log = eventlog.EventLog(self.request)
-        event_log.set_filter([kw["filter"]])
-        for event in event_log.reverse():
-            pagename = event[2].get('pagename', None)
-            if not kw["all"]:
-                if pagename == formatter.page.page_name:
-                    self.count += 1
-            else:
-                self.count += 1
-
-        return self.count
-
-def execute(macro, args):
-    """ Temporary glue code to use with moin current macro system """
-    return Hits(macro, args).renderInText()
-
+    return u'%d' % count
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/macro/InterWiki.py	Mon Jul 23 21:51:49 2007 +0200
@@ -0,0 +1,40 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    Outputs the interwiki map.
+
+    @copyright: 2007 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details
+"""
+
+Dependencies = ["pages"] # if interwikimap is editable
+
+from MoinMoin import wikiutil
+
+def macro_InterWiki(macro):
+    interwiki_list = wikiutil.load_wikimap(macro.request)
+    iwlist = interwiki_list.items() # this is where we cached it
+    iwlist.sort()
+    fmt = macro.formatter
+    output = []
+    output.append(fmt.definition_list(1))
+    for tag, url in iwlist:
+        output.append(fmt.definition_term(1))
+        output.append(fmt.code(1))
+        output.append(fmt.url(1, wikiutil.join_wiki(url, 'RecentChanges')))
+        output.append(fmt.text(tag))
+        output.append(fmt.url(0))
+        output.append(fmt.code(0))
+        output.append(fmt.definition_term(0))
+        output.append(fmt.definition_desc(1))
+        output.append(fmt.code(1))
+        if '$PAGE' not in url:
+            output.append(fmt.url(1, url))
+            output.append(fmt.text(url))
+            output.append(fmt.url(0))
+        else:
+            output.append(fmt.text(url))
+        output.append(fmt.code(0))
+        output.append(fmt.definition_desc(1))
+    output.append(fmt.definition_list(0))
+    return u''.join(output)
+
--- a/MoinMoin/macro/MonthCalendar.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/macro/MonthCalendar.py	Mon Jul 23 21:51:49 2007 +0200
@@ -87,6 +87,8 @@
         * do a correct calculation of "today" using user's timezone
     * 2.2:
         * added template argument for specifying an edit template for new pages
+    * 2.3:
+        * adapted to moin 1.7 new macro parameter parsing
 
     Usage:
         [[MonthCalendar(BasePage,year,month,monthoffset,monthoffset2,height6)]]
@@ -182,55 +184,25 @@
         year += 1
     return year, month
 
-def parseargs(args, defpagename, defyear, defmonth, defoffset, defoffset2, defheight6, defanniversary, deftemplate):
+def parseargs(request, args, defpagename, defyear, defmonth, defoffset, defoffset2, defheight6, defanniversary, deftemplate):
     """ parse macro arguments """
-    strpagename = args.group('basepage')
-    if strpagename:
-        parmpagename = wikiutil.unquoteWikiname(strpagename)
-    else:
-        parmpagename = defpagename
+    args = wikiutil.parse_quoted_separated(args, name_value=False)
+    args += [None] * 8 # fill up with None to trigger defaults
+    parmpagename, parmyear, parmmonth, parmoffset, parmoffset2, parmheight6, parmanniversary, parmtemplate = args[:8]
+    parmpagename = wikiutil.get_unicode(request, parmpagename, 'pagename', defpagename)
+    parmyear = wikiutil.get_int(request, parmyear, 'year', defyear)
+    parmmonth = wikiutil.get_int(request, parmmonth, 'month', defmonth)
+    parmoffset = wikiutil.get_int(request, parmoffset, 'offset', defoffset)
+    parmoffset2 = wikiutil.get_int(request, parmoffset2, 'offset2', defoffset2)
+    parmheight6 = wikiutil.get_bool(request, parmheight6, 'height6', defheight6)
+    parmanniversary = wikiutil.get_bool(request, parmanniversary, 'anniversary', defanniversary)
+    parmtemplate = wikiutil.get_unicode(request, parmtemplate, 'template', deftemplate)
+
     # multiple pagenames separated by "*" - split into list of pagenames
     parmpagename = re.split(r'\*', parmpagename)
 
-    strtemplate = args.group('template')
-    if strtemplate:
-        parmtemplate = wikiutil.unquoteWikiname(strtemplate)
-    else:
-        parmtemplate = deftemplate
-
-    def getint(args, name, default):
-        s = args.group(name)
-        i = default
-        if s:
-            try:
-                i = int(s)
-            except:
-                pass
-        return i
-
-    parmyear = getint(args, 'year', defyear)
-    parmmonth = getint(args, 'month', defmonth)
-    parmoffset = getint(args, 'offset', defoffset)
-    parmoffset2 = getint(args, 'offset2', defoffset2)
-    parmheight6 = getint(args, 'height6', defheight6)
-    parmanniversary = getint(args, 'anniversary', defanniversary)
-
     return parmpagename, parmyear, parmmonth, parmoffset, parmoffset2, parmheight6, parmanniversary, parmtemplate
 
-# FIXME:                          vvvvvv is there a better way for matching a pagename ?
-_arg_basepage = r'\s*(?P<basepage>[^, ]+)?\s*'
-_arg_year = r',\s*(?P<year>\d+)?\s*'
-_arg_month = r',\s*(?P<month>\d+)?\s*'
-_arg_offset = r',\s*(?P<offset>[+-]?\d+)?\s*'
-_arg_offset2 = r',\s*(?P<offset2>[+-]?\d+)?\s*'
-_arg_height6 = r',\s*(?P<height6>[+-]?\d+)?\s*'
-_arg_anniversary = r',\s*(?P<anniversary>[+-]?\d+)?\s*'
-_arg_template = r',\s*(?P<template>[^, ]+)?\s*' # XXX see basepage comment
-_args_re_pattern = r'^(%s)?(%s)?(%s)?(%s)?(%s)?(%s)?(%s)?(%s)?$' % \
-                     (_arg_basepage, _arg_year, _arg_month,
-                      _arg_offset, _arg_offset2, _arg_height6, _arg_anniversary, _arg_template)
-
-
 def execute(macro, text):
     request = macro.request
     formatter = macro.formatter
@@ -240,34 +212,29 @@
     if request.mode_getpagelinks:
         return ''
 
-    args_re = re.compile(_args_re_pattern)
-
     currentyear, currentmonth, currentday, h, m, s, wd, yd, ds = request.user.getTime(time.time())
     thispage = formatter.page.page_name
     # does the url have calendar params (= somebody has clicked on prev/next links in calendar) ?
     if 'calparms' in macro.form:
+        has_calparms = 1 # yes!
         text2 = macro.form['calparms'][0]
-        args2 = args_re.match(text2)
-        if not args2:
-            return ('<p><strong class="error">%s</strong></p>' % _('Invalid MonthCalendar calparms "%s"!')) % (text2, )
-        else:
-            has_calparms = 1 # yes!
+        try:
             cparmpagename, cparmyear, cparmmonth, cparmoffset, cparmoffset2, cparmheight6, cparmanniversary, cparmtemplate = \
-                parseargs(args2, thispage, currentyear, currentmonth, 0, 0, 0, 0, '')
+                parseargs(request, text2, thispage, currentyear, currentmonth, 0, 0, False, False, u'')
+        except (ValueError, TypeError), err:
+            return macro.format_error(err)
     else:
         has_calparms = 0
 
     if text is None: # macro call without parameters
+        text = u''
+
+    # parse and check arguments
+    try:
         parmpagename, parmyear, parmmonth, parmoffset, parmoffset2, parmheight6, anniversary, parmtemplate = \
-            [thispage], currentyear, currentmonth, 0, 0, 0, 0, ''
-    else:
-        # parse and check arguments
-        args = args_re.match(text)
-        if not args:
-            return ('<p><strong class="error">%s</strong></p>' % _('Invalid MonthCalendar arguments "%s"!')) % (text, )
-        else:
-            parmpagename, parmyear, parmmonth, parmoffset, parmoffset2, parmheight6, anniversary, parmtemplate = \
-                parseargs(args, thispage, currentyear, currentmonth, 0, 0, 0, 0, '')
+            parseargs(request, text, thispage, currentyear, currentmonth, 0, 0, False, False, u'')
+    except (ValueError, TypeError), err:
+        return macro.format_error(err)
 
     # does url have calendar params and is THIS the right calendar to modify (we can have multiple
     # calendars on the same page)?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/macro/PageCount.py	Mon Jul 23 21:51:49 2007 +0200
@@ -0,0 +1,31 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    Outputs the page count of the wiki.
+
+    @copyright: 2007 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details
+"""
+
+Dependencies = ['namespace']
+
+from MoinMoin import wikiutil
+
+def macro_PageCount(macro, exists=None):
+        """ Return number of pages readable by current user
+
+        Return either an exact count (slow!) or fast count including deleted pages.
+
+        TODO: make macro syntax more sane
+        """
+        request = macro.request
+        exists = wikiutil.get_unicode(request, exists, 'exists')
+        # Check input
+        only_existing = False
+        if exists == u'exists':
+            only_existing = True
+        elif exists:
+            raise ValueError("Wrong argument: %r" % exists)
+
+        count = request.rootpage.getPageCount(exists=only_existing)
+        return macro.formatter.text("%d" % count)
+
--- a/MoinMoin/macro/__init__.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/macro/__init__.py	Mon Jul 23 21:51:49 2007 +0200
@@ -12,7 +12,8 @@
     Using "form" directly is deprecated and should be replaced by "request.form".
 
     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
-                2006 MoinMoin:ThomasWaldmann
+                2006-2007 MoinMoin:ThomasWaldmann,
+                2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -24,11 +25,10 @@
 from MoinMoin import wikiutil, i18n
 from MoinMoin.Page import Page
 
-names = ["TitleSearch", "WordIndex", "TitleIndex",
-         "GoTo", "InterWiki", "PageCount",
+
+names = ["TitleSearch", "WordIndex", "TitleIndex", "GoTo",
          # Macros with arguments
-         "Icon", "PageList", "Date", "DateTime", "Anchor", "MailTo", "GetVal",
-         "TemplateList",
+         "Icon", "PageList", "Date", "DateTime", "Anchor", "MailTo", "GetVal", "TemplateList",
 ]
 
 #############################################################################
@@ -52,7 +52,7 @@
     """ Macro handler
 
     There are three kinds of macros:
-     * Builtin Macros - implemented in this file and named _macro_[name]
+     * Builtin Macros - implemented in this file and named macro_[name]
      * Language Pseudo Macros - any lang the wiki knows can be use as
        macro and is implemented here by _m_lang()
      * External macros - implemented in either MoinMoin.macro package, or
@@ -62,19 +62,17 @@
 
     Dependencies = {
         "TitleSearch": ["namespace"],
-        "Goto": [],
+        "PageList": ["namespace"],
+        "TemplateList": ["namespace"],
         "WordIndex": ["namespace"],
         "TitleIndex": ["namespace"],
-        "InterWiki": ["pages"],  # if interwikimap is editable
-        "PageCount": ["namespace"],
+        "Goto": [],
         "Icon": ["user"], # users have different themes and user prefs
-        "PageList": ["namespace"],
         "Date": ["time"],
         "DateTime": ["time"],
         "Anchor": [],
         "Mailto": ["user"],
         "GetVal": ["pages"],
-        "TemplateList": ["namespace"],
         }
 
     # we need the lang macros to execute when html is generated,
@@ -94,6 +92,17 @@
         # Initialized on execute
         self.name = None
 
+    def _wrap(self, function, args, fixed=[]):
+        try:
+            return wikiutil.invoke_extension_function(self.request, function,
+                                                      args, fixed)
+        except ValueError, e:
+            return self.format_error(e)
+
+    def format_error(self, err):
+        """ format an error object for output instead of normal macro output """
+        return self.formatter.text(u'[[%s: %s]]' % (self.name, err.args[0]))
+
     def execute(self, macro_name, args):
         """ Get and execute a macro
 
@@ -102,11 +111,16 @@
         """
         self.name = macro_name
         try:
+            call = wikiutil.importPlugin(self.cfg, 'macro', macro_name,
+                                         function='macro_%s' % macro_name)
+            execute = lambda _self, _args: _self._wrap(call, _args, [self])
+        except wikiutil.PluginAttributeError:
+            # fall back to old execute() method, no longer recommended
             execute = wikiutil.importPlugin(self.cfg, 'macro', macro_name)
         except wikiutil.PluginMissingError:
             try:
-                builtins = self.__class__
-                execute = getattr(builtins, '_macro_' + macro_name)
+                call = getattr(self, 'macro_%s' % macro_name)
+                execute = lambda _self, _args: _self._wrap(call, _args)
             except AttributeError:
                 if macro_name in i18n.wikiLanguages():
                     execute = builtins._m_lang
@@ -138,31 +152,52 @@
         except wikiutil.PluginError:
             return self.defaultDependency
 
-    def _macro_TitleSearch(self, args):
+    def macro_TitleSearch(self):
         from MoinMoin.macro.FullSearch import search_box
         return search_box("titlesearch", self)
 
-    def _macro_GoTo(self, args):
-        """ Make a goto box
-
-        @param args: macro arguments
-        @rtype: unicode
-        @return: goto box html fragment
-        """
+    def macro_PageList(self, needle=None):
+        from MoinMoin import search
         _ = self._
-        html = [
-            u'<form method="get" action="">',
-            u'<div>',
-            u'<input type="hidden" name="action" value="goto">',
-            u'<input type="text" name="target" size="30">',
-            u'<input type="submit" value="%s">' % _("Go To Page"),
-            u'</div>',
-            u'</form>',
-            ]
-        html = u'\n'.join(html)
-        return self.formatter.rawHTML(html)
+        case = 0
 
-    def _make_index(self, args, word_re=u'.+'):
+        # If called with empty or no argument, default to regex search for .+, the full page list.
+        needle = wikiutil.get_unicode(self.request, needle, 'needle', u'regex:.+')
+
+        # With whitespace argument, return same error message as FullSearch
+        if not needle.strip():
+            err = _('Please use a more selective search term instead of {{{"%s"}}}') % needle
+            return '<span class="error">%s</span>' % err
+
+        # Return a title search for needle, sorted by name.
+        results = search.searchPages(self.request, needle,
+                titlesearch=1, case=case, sort='page_name')
+        return results.pageList(self.request, self.formatter, paging=False)
+
+    def macro_TemplateList(self, needle=u'.+'):
+        # TODO: this should be renamed (RegExPageNameList?), it does not list only Templates...
+        _ = self._
+        try:
+            needle_re = re.compile(needle, re.IGNORECASE)
+        except re.error, err:
+            raise ValueError("Error in regex %r: %s" % (needle, err))
+
+        # Get page list readable by current user, filtered by needle
+        hits = self.request.rootpage.getPageList(filter=needle_re.search)
+        hits.sort()
+
+        result = []
+        result.append(self.formatter.bullet_list(1))
+        for pagename in hits:
+            result.append(self.formatter.listitem(1))
+            result.append(self.formatter.pagelink(1, pagename, generated=1))
+            result.append(self.formatter.text(pagename))
+            result.append(self.formatter.pagelink(0, pagename))
+            result.append(self.formatter.listitem(0))
+        result.append(self.formatter.bullet_list(0))
+        return ''.join(result)
+
+    def _make_index(self, word_re=u'.+'):
         """ make an index page (used for TitleIndex and WordIndex macro)
 
             word_re is a regex used for splitting a pagename into fragments
@@ -247,103 +282,43 @@
         return u''.join(output)
 
 
-    def _macro_TitleIndex(self, args):
-        return self._make_index(args)
+    def macro_TitleIndex(self):
+        return self._make_index()
 
-    def _macro_WordIndex(self, args):
+    def macro_WordIndex(self):
         if self.request.isSpiderAgent: # reduce bot cpu usage
             return ''
         word_re = u'[%s][%s]+' % (config.chars_upper, config.chars_lower)
-        return self._make_index(args, word_re=word_re)
-
-
-    def _macro_PageList(self, needle):
-        from MoinMoin import search
-        _ = self._
-        case = 0
-
-        # If called with empty or no argument, default to regex search for .+, the full page list.
-        if not needle:
-            needle = 'regex:.+'
-
-        # With whitespace argument, return same error message as FullSearch
-        elif needle.isspace():
-            err = _('Please use a more selective search term instead of {{{"%s"}}}') % needle
-            return '<span class="error">%s</span>' % err
-
-        # Return a title search for needle, sorted by name.
-        results = search.searchPages(self.request, needle,
-                titlesearch=1, case=case, sort='page_name')
-        return results.pageList(self.request, self.formatter, paging=False)
-
-    def _macro_InterWiki(self, args):
-        from StringIO import StringIO
-        interwiki_list = wikiutil.load_wikimap(self.request)
-        buf = StringIO()
-        buf.write('<dl>')
-        iwlist = interwiki_list.items() # this is where we cached it
-        iwlist.sort()
-        for tag, url in iwlist:
-            buf.write('<dt><tt><a href="%s">%s</a></tt></dt>' % (
-                wikiutil.join_wiki(url, 'RecentChanges'), tag))
-            if '$PAGE' not in url:
-                buf.write('<dd><tt><a href="%s">%s</a></tt></dd>' % (url, url))
-            else:
-                buf.write('<dd><tt>%s</tt></dd>' % url)
-        buf.write('</dl>')
-        return self.formatter.rawHTML(buf.getvalue())
-
-    def _macro_PageCount(self, args):
-        """ Return number of pages readable by current user
+        return self._make_index(word_re=word_re)
 
-        Return either an exact count (slow!) or fast count including
-        deleted pages.
-        """
-        # Check input
-        options = {None: 0, '': 0, 'exists': 1}
-        try:
-            exists = options[args]
-        except KeyError:
-            # Wrong argument, return inline error message
-            arg = self.formatter.text(args)
-            return (self.formatter.span(1, css_class="error") +
-                    'Wrong argument: %s' % arg +
-                    self.formatter.span(0))
-
-        count = self.request.rootpage.getPageCount(exists=exists)
-        return self.formatter.text("%d" % count)
-
-    def _macro_Icon(self, args):
-        icon = args.lower()
-        return self.formatter.icon(icon)
+    def macro_GoTo(self):
+        """ Make a goto box
 
-    def _macro_TemplateList(self, args):
+        @rtype: unicode
+        @return: goto box html fragment
+        """
         _ = self._
-        try:
-            needle_re = re.compile(args or '', re.IGNORECASE)
-        except re.error, e:
-            return "<strong>%s: %s</strong>" % (
-                _("ERROR in regex '%s'") % (args, ), e)
+        html = [
+            u'<form method="get" action="">',
+            u'<div>',
+            u'<input type="hidden" name="action" value="goto">',
+            u'<input type="text" name="target" size="30">',
+            u'<input type="submit" value="%s">' % _("Go To Page"),
+            u'</div>',
+            u'</form>',
+            ]
+        html = u'\n'.join(html)
+        return self.formatter.rawHTML(html)
 
-        # Get page list readable by current user, filtered by needle
-        hits = self.request.rootpage.getPageList(filter=needle_re.search)
-        hits.sort()
-
-        result = []
-        result.append(self.formatter.bullet_list(1))
-        for pagename in hits:
-            result.append(self.formatter.listitem(1))
-            result.append(self.formatter.pagelink(1, pagename, generated=1))
-            result.append(self.formatter.text(pagename))
-            result.append(self.formatter.pagelink(0, pagename))
-            result.append(self.formatter.listitem(0))
-        result.append(self.formatter.bullet_list(0))
-        return ''.join(result)
-
+    def macro_Icon(self, icon=u''):
+        # empty icon name isn't valid either
+        if not icon:
+            raise ValueError("You need to give a non-empty icon name")
+        return self.formatter.icon(icon.lower())
 
     def __get_Date(self, args, format_date):
         _ = self._
-        if not args:
+        if args is None:
             tm = time.time() # always UTC
         elif len(args) >= 19 and args[4] == '-' and args[7] == '-' \
                 and args[10] == 'T' and args[13] == ':' and args[16] == ':':
@@ -362,9 +337,8 @@
                         if sign == '-':
                             tzoffset = -tzoffset
                 tm = (year, month, day, hour, minute, second, 0, 0, 0)
-            except ValueError, e:
-                return "<strong>%s: %s</strong>" % (
-                    _("Bad timestamp '%s'") % (args, ), e)
+            except ValueError, err:
+                raise ValueError("Bad timestamp %r: %s" % (args, err))
             # as mktime wants a localtime argument (but we only have UTC),
             # we adjust by our local timezone's offset
             try:
@@ -375,31 +349,25 @@
             # try raw seconds since epoch in UTC
             try:
                 tm = float(args)
-            except ValueError, e:
-                return "<strong>%s: %s</strong>" % (
-                    _("Bad timestamp '%s'") % (args, ), e)
+            except ValueError, err:
+                raise ValueError("Bad timestamp %r: %s" % (args, err))
         return format_date(tm)
 
-    def _macro_Date(self, args):
-        return self.__get_Date(args, self.request.user.getFormattedDate)
-
-    def _macro_DateTime(self, args):
-        return self.__get_Date(args, self.request.user.getFormattedDateTime)
-
-    def _macro_Anchor(self, args):
-        return self.formatter.anchordef(args or "anchor")
+    def macro_Date(self, stamp=None):
+        return self.__get_Date(stamp, self.request.user.getFormattedDate)
 
-    def _macro_MailTo(self, args):
+    def macro_DateTime(self, stamp=None):
+        return self.__get_Date(stamp, self.request.user.getFormattedDateTime)
+
+    def macro_Anchor(self, anchor=None):
+        anchor = wikiutil.get_unicode(self.request, anchor, 'anchor', u'anchor')
+        return self.formatter.anchordef(anchor)
+
+    def macro_MailTo(self, email=unicode, text=u''):
+        if not email:
+            raise ValueError("You need to give an (obfuscated) email address")
+
         from MoinMoin.mail.sendmail import decodeSpamSafeEmail
-        result = ''
-        args = args or ''
-        if ',' not in args:
-            email = args
-            text = ''
-        else:
-            email, text = args.split(',', 1)
-
-        email, text = email.strip(), text.strip()
 
         if self.request.user.valid:
             # decode address and generate mailto: link
@@ -420,8 +388,11 @@
 
         return result
 
-    def _macro_GetVal(self, args):
-        page, key = args.split(',')
+    def macro_GetVal(self, page=None, key=None):
+        page = wikiutil.get_unicode(self.request, page, 'page')
+        key = wikiutil.get_unicode(self.request, key, 'key')
+        if page is None or key is None:
+            raise ValueError("You need to give: pagename, key")
         d = self.request.dicts.dict(page)
         result = d.get(key, '')
         return self.formatter.text(result)
--- a/MoinMoin/macro/_tests/test_Hits.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/macro/_tests/test_Hits.py	Mon Jul 23 21:51:49 2007 +0200
@@ -69,7 +69,7 @@
         eventlog.EventLog(self.request).add(self.request, 'VIEWPAGE', {'pagename': self.pagename})
         eventlog.EventLog(self.request).add(self.request, 'VIEWPAGE', {'pagename': self.pagename})
 
-        result = self._test_macro('Hits', '')
+        result = self._test_macro(u'Hits', u'')
         expected = "3"
         assert result == expected
 
@@ -82,7 +82,7 @@
         eventlog.EventLog(self.request).add(self.request, 'VIEWPAGE', {'pagename': self.pagename})
         eventlog.EventLog(self.request).add(self.request, 'VIEWPAGE', {'pagename': self.pagename})
 
-        result = self._test_macro('Hits', 'all=1')
+        result = self._test_macro(u'Hits', u'all=1')
         expected = "6"
         assert result == expected
 
@@ -92,7 +92,7 @@
 
         # simulate a log entry SAVEPAGE for WikiSandBox to destinguish current page
         eventlog.EventLog(self.request).add(self.request, 'SAVEPAGE', {'pagename': 'WikiSandBox'})
-        result = self._test_macro('Hits', 'filter=SAVEPAGE')
+        result = self._test_macro(u'Hits', u'filter=SAVEPAGE')
         expected = "2"
         assert result == expected
 
@@ -100,7 +100,7 @@
         """ macro test: 'all=1, filter=SAVEPAGE' for Hits (all pages are counted for SAVEPAGE)"""
         self.shouldDeleteTestPage = True
 
-        result = self._test_macro('Hits', 'all=1, filter=SAVEPAGE')
+        result = self._test_macro(u'Hits', u'all=1, filter=SAVEPAGE')
         expected = "3"
         assert result == expected
 
--- a/MoinMoin/macro/_tests/test_macro.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/macro/_tests/test_macro.py	Mon Jul 23 21:51:49 2007 +0200
@@ -7,21 +7,18 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
-
 from MoinMoin import macro
 from MoinMoin.parser.text import Parser
 from MoinMoin.formatter.text_html import Formatter
 
 
-class TestMacro(unittest.TestCase):
+class TestMacro:
     def testTrivialMacro(self):
         """macro: trivial macro works"""
         m = self._make_macro()
         expected = m.formatter.linebreak(0)
         result = m.execute("BR", "")
-        self.assertEqual(result, expected,
-            'Expected "%(expected)s" but got "%(result)s"' % locals())
+        assert result == expected
 
     def _make_macro(self):
         """Test helper"""
--- a/MoinMoin/mail/_tests/test_sendmail.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/mail/_tests/test_sendmail.py	Mon Jul 23 21:51:49 2007 +0200
@@ -6,14 +6,13 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
 from email.Charset import Charset, QP
 from email.Header import Header
 from MoinMoin.mail import sendmail
 from MoinMoin import config
 
 
-class TestdecodeSpamSafeEmail(unittest.TestCase):
+class TestdecodeSpamSafeEmail:
     """mail.sendmail: testing mail"""
 
     _tests = (
@@ -39,13 +38,10 @@
     def testDecodeSpamSafeMail(self):
         """mail.sendmail: decoding spam safe mail"""
         for coded, expected in self._tests:
-            result = sendmail.decodeSpamSafeEmail(coded)
-            self.assertEqual(result, expected,
-                             'Expected "%(expected)s" but got "%(result)s"' %
-                             locals())
+            assert sendmail.decodeSpamSafeEmail(coded) == expected
 
 
-class TestEncodeAddress(unittest.TestCase):
+class TestEncodeAddress:
     """ Address encoding tests
 
     See http://www.faqs.org/rfcs/rfc2822.html section 3.4.
@@ -63,31 +59,27 @@
         """ mail.sendmail: encode simple address: local@domain """
         address = u'local@domain'
         expected = address.encode(config.charset)
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
     def testComposite(self):
         """ mail.sendmail: encode address: 'Phrase <local@domain>' """
         address = u'Phrase <local@domain>'
         phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
         expected = phrase + '<local@domain>'
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
     def testCompositeUnicode(self):
         """ mail.sendmail: encode Uncode address: 'ויקי <local@domain>' """
         address = u'ויקי <local@domain>'
         phrase = str(Header(u'ויקי '.encode('utf-8'), self.charset))
         expected = phrase + '<local@domain>'
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
     def testEmptyPhrase(self):
         """ mail.sendmail: encode address with empty phrase: '<local@domain>' """
         address = u'<local@domain>'
         expected = address.encode(config.charset)
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
     def testEmptyAddress(self):
         """ mail.sendmail: encode address with empty address: 'Phrase <>'
@@ -98,8 +90,7 @@
         address = u'Phrase <>'
         phrase = str(Header(u'Phrase '.encode('utf-8'), self.charset))
         expected = phrase + '<>'
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
     def testInvalidAddress(self):
         """ mail.sendmail: encode invalid address 'Phrase <blah'
@@ -110,8 +101,7 @@
         """
         address = u'Phrase <blah'
         expected = address.encode(config.charset)
-        self.failUnlessEqual(sendmail.encodeAddress(address, self.charset),
-                             expected)
+        assert sendmail.encodeAddress(address, self.charset) == expected
 
 
 coverage_modules = ['MoinMoin.mail.sendmail']
--- a/MoinMoin/parser/_tests/test_text_moin_wiki.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/parser/_tests/test_text_moin_wiki.py	Mon Jul 23 21:51:49 2007 +0200
@@ -180,11 +180,11 @@
     needle = re.compile(text % r'(.+)')
     _tests = (
         # test                                   expected
-        ('[[DateTime(259200)]]',                '1970-01-04 00:00:00'),
-        ('[[DateTime(2003-03-03T03:03:03)]]',   '2003-03-03 03:03:03'),
-        ('[[DateTime(2000-01-01T00:00:00Z)]]',  '2000-01-01 00:00:00'), # works for Europe/Vilnius
-        ('[[Date(2002-02-02T01:02:03Z)]]',      '2002-02-02'),
-        ('[[DateTime(1970-01-06T00:00:00)]]',   '1970-01-06 00:00:00'), # fails e.g. for Europe/Vilnius
+        (u'[[DateTime(259200)]]',                '1970-01-04 00:00:00'),
+        (u'[[DateTime(2003-03-03T03:03:03)]]',   '2003-03-03 03:03:03'),
+        (u'[[DateTime(2000-01-01T00:00:00Z)]]',  '2000-01-01 00:00:00'), # works for Europe/Vilnius
+        (u'[[Date(2002-02-02T01:02:03Z)]]',      '2002-02-02'),
+        (u'[[DateTime(1970-01-06T00:00:00)]]',   '1970-01-06 00:00:00'), # fails e.g. for Europe/Vilnius
         )
 
     def setUp(self):
@@ -561,6 +561,7 @@
     def testManyNestingPreBrackets(self):
         """ tests two nestings  ({{{ }}} and {{{ }}}) in one line for the wiki parser
         """
+        py.test.skip("Broken because not implemented yet")
 
         raw = """{{{
 Test {{{brackets}}} and test {{{brackets}}}
@@ -572,6 +573,15 @@
 
         assert expected == result
 
+    def testMultipleShortPreSections(self):
+        """
+        tests two single {{{ }}} in one line
+        """
+        raw = 'def {{{ghi}}} jkl {{{mno}}} pqr'
+        output = ''.join(self.parse(raw))
+        # expected output copied from 1.5
+        expected = 'def <tt>ghi</tt> jkl <tt>mno</tt><span class="anchor" id="line-0"></span>pqr'
+        assert expected in output
 
 class TestLinkingMarkup(ParserTestCase):
     """ Test wiki markup """
--- a/MoinMoin/parser/text_moin_wiki.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/parser/text_moin_wiki.py	Mon Jul 23 21:51:49 2007 +0200
@@ -874,15 +874,6 @@
         lastpos = 0
 
         ###result.append(u'<span class="info">[scan: <tt>"%s"</tt>]</span>' % line)
-        if line.count('{{{') > 1:
-            self.in_nested_pre = line.count('{{{') - line.count('}}}')
-            if self.in_nested_pre == 0:
-                self.in_nested_pre = 1
-            if line.startswith('{{{'):
-                line = line[3:].strip()
-            self.in_pre = 'no_parser'
-            return "%s%s%s" % (self.formatter.paragraph(1), self.formatter.preformatted(1), line)
-
         for match in scan_re.finditer(line):
             # Add text before the match
             if lastpos < match.start():
--- a/MoinMoin/search/_tests/test_search.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/search/_tests/test_search.py	Mon Jul 23 21:51:49 2007 +0200
@@ -6,11 +6,10 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
 from MoinMoin import search
 
 
-class TestQuotingBug(unittest.TestCase):
+class TestQuotingBug:
     """search: quoting bug tests
 
     http://moinmoin.wikiwikiweb.de/MoinMoinBugs/SearchOneCharString
@@ -19,20 +18,18 @@
     testing parsed queries is much more work.
     """
 
-    def setUp(self):
-        self.parser = search.QueryParser()
-
     def testIsQuoted(self):
         """ search: quoting bug - quoted terms """
+        parser = search.QueryParser()
         for case in ('"yes"', "'yes'"):
-            self.assertEqual(self.parser.isQuoted(case), True)
+            assert parser.isQuoted(case)
 
     def testIsNot(self):
         """ search: quoting bug - unquoted terms """
-        tests = ('', "'", '"', '""', "''", "'\"", '"no', 'no"', "'no",
-                 "no'", '"no\'')
+        tests = ('', "'", '"', '""', "''", "'\"", '"no', 'no"', "'no", "no'", '"no\'')
+        parser = search.QueryParser()
         for case in tests:
-            self.assertEqual(self.parser.isQuoted(case), False)
+            assert not parser.isQuoted(case)
 
 
 coverage_modules = ['MoinMoin.search']
--- a/MoinMoin/util/_tests/test_web.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/util/_tests/test_web.py	Mon Jul 23 21:51:49 2007 +0200
@@ -6,13 +6,12 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
 from MoinMoin import wikiutil
 from MoinMoin.util import web
 from MoinMoin.widget import html
 
 
-class TestMakeQueryString(unittest.TestCase):
+class TestMakeQueryString:
     """util.web: making query string"""
 
     def testMakeQueryStringFromArgument(self):
@@ -26,17 +25,11 @@
             )
 
         for description, arg, expected in tests:
-            result = wikiutil.makeQueryString(arg)
-            self.assertEqual(result, expected,
-                             ('%(description)s: expected "%(expected)s" '
-                              'but got "%(result)s"') % locals())
+            assert wikiutil.makeQueryString(arg) == expected
 
     def testMakeQueryStringFromKeywords(self):
         """ util.web: make query sting from keywords """
-        expected = 'a=1&b=string'
-        result = wikiutil.makeQueryString(a=1, b='string')
-        self.assertEqual(result, expected,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+        assert wikiutil.makeQueryString(a=1, b='string') == 'a=1&b=string'
 
     def testMakeQueryStringFromArgumentAndKeywords(self):
         """ util.web: make query sting from argument and keywords """
@@ -50,48 +43,41 @@
 
         for description, arg, expected in tests:
             # Call makeQueryString with both arg and keyword
-            result = wikiutil.makeQueryString(arg, b='kw')
-            self.assertEqual(result, expected,
-                             ('%(description)s: expected "%(expected)s" '
-                              'but got "%(result)s"') % locals())
+            assert wikiutil.makeQueryString(arg, b='kw') == expected
 
 
-class TestMakeSelection(unittest.TestCase):
+class TestMakeSelection:
     """util.web: creating html select"""
 
     values = ('one', 'two', 'simple', ('complex', 'A tuple & <escaped text>'))
 
-    def setUp(self):
-        html._SORT_ATTRS = 1
-        self.expected = (
+    html._SORT_ATTRS = 1
+    expected = (
         u'<select name="test" size="1">'
         u'<option value="one">one</option>'
         u'<option value="two">two</option>'
         u'<option value="simple">simple</option>'
         u'<option value="complex">A tuple &amp; &lt;escaped text&gt;</option>'
         u'</select>'
-        )
+    )
 
     def testMakeSelectNoSelection(self):
         """util.web: creating html select with no selection"""
         expected = self.expected
         result = unicode(web.makeSelection('test', self.values, size=1))
-        self.assertEqual(result, expected,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+        assert result == expected
 
     def testMakeSelectNoSelection2(self):
         """util.web: creating html select with non existing selection"""
         expected = self.expected
         result = unicode(web.makeSelection('test', self.values, 'three', size=1))
-        self.assertEqual(result, expected,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+        assert result == expected
 
     def testMakeSelectWithSelectedItem(self):
         """util.web: creating html select with selected item"""
         expected = self.expected.replace('value="two"', 'selected value="two"')
         result = unicode(web.makeSelection('test', self.values, 'two', size=1))
-        self.assertEqual(result, expected,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+        assert result == expected
 
 
 coverage_modules = ['MoinMoin.util.web']
--- a/MoinMoin/widget/_tests/test_html.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/widget/_tests/test_html.py	Mon Jul 23 21:51:49 2007 +0200
@@ -6,10 +6,11 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import unittest # LEGACY UNITTEST, PLEASE DO NOT IMPORT unittest IN NEW TESTS, PLEASE CONSULT THE py.test DOCS
+import py
+
 from MoinMoin.widget import html
 
-class TestHTMLWidgets(unittest.TestCase):
+class TestHTMLWidgets:
     """widget.html: testing html widgets"""
 
     def testCreate(self):
@@ -28,16 +29,14 @@
 
         for description, obj, expected in tests:
             result = unicode(obj)
-            self.assertEqual(result, expected,
-                             ('%(description)s: expected "%(expected)s" '
-                              'but got "%(result)s"') % locals())
+            assert result == expected
 
     def testInvalidAttributes(self):
-        """widegt.html: invalid attributes raises exception
+        """widget.html: invalid attributes raises exception
 
         TO DO: add tests for all elements by HTML 4 spec.
         """
-        self.assertRaises(AttributeError, html.BR, name='foo')
+        py.test.raises(AttributeError, html.BR, name='foo')
 
 
     def testCompositeElements(self):
@@ -61,8 +60,7 @@
         for action, data, expected in actions:
             action(data)
             result = unicode(element)
-            self.assertEqual(result, expected,
-                             'Expected "%(expected)s" but got "%(result)s"' % locals())
+            assert result == expected
 
 coverage_modules = ['MoinMoin.widget.html']
 
--- a/MoinMoin/wikiutil.py	Mon Jul 23 21:49:11 2007 +0200
+++ b/MoinMoin/wikiutil.py	Mon Jul 23 21:51:49 2007 +0200
@@ -3,6 +3,8 @@
     MoinMoin - Wiki Utility Functions
 
     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
+                2004 by Florian Festi,
+                2006 by Mikko Virkkil,
                 2005-2007 MoinMoin:ThomasWaldmann,
                 2007 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
@@ -17,6 +19,9 @@
 
 from MoinMoin import config
 from MoinMoin.util import pysupport, lock
+from inspect import getargspec
+from types import MethodType
+
 
 # Exceptions
 class InvalidFileNameError(Exception):
@@ -1189,6 +1194,461 @@
 ### Parameter parsing
 #############################################################################
 
+def parse_quoted_separated(args, separator=',', name_value=True, seplimit=0):
+    """
+    Parses the given arguments according to the other parameters.
+    If name_value is True, it parses keyword arguments (name=value)
+    and returns the keyword arguments in the second return value
+    (a dict) and positional arguments that occurred after any keyword
+    argument in the third return value (a list).
+    The first return value always contains the positional arguments,
+    if name_value is False only it is present.
+
+    Arguments can be quoted with a double-quote ('"') and the quote
+    can be escaped by doubling it, the separator and equal sign (for
+    keyword args) can both be quoted, when keyword args are enabled
+    then the name of a keyword argument can also be quoted.
+
+    Values that are not given are returned as None, while the
+    empty string as a value can be achieved by quoting it; keys
+    are never returned as None.
+
+    If a name or value does not start with a quote, then the quote
+    character looses its special meaning for that name or value.
+
+    @param args: arguments to parse
+    @param separator: the argument separator, defaults to a comma (',')
+    @param name_value: indicates whether to parse keyword arguments
+    @param seplimit: limits the number of parsed arguments
+    @rtype: tuple, list
+    @returns: if name_value is False, returns a list of arguments,
+              otherwise a list of positional, a dict of keyword and
+              a list of trailing arguments
+    """
+    idx = 0
+    if not isinstance(args, unicode):
+        raise TypeError('args must be unicode')
+    max = len(args)
+    ret_positional = [] # positional argument return value
+    ret_trailing = []   # trailing arguments return value
+    positional = ret_positional
+    keyword = {}        # keyword arguments
+    curname = u''       # current name, initially value as well (name=value)
+    cur = None          # current value
+    cur_quoted = False  # indicates whether value was quoted,
+                        # needed None vs. u'' handling
+    quoted = False      # we're inside quotes
+    skipquote = 0       # next quote is a quoted quote
+    noquote = False     # no quotes expected because word didn't start with one
+    seplimit_reached = False # number of separators exhausted
+    separator_count = 0 # number of separators encountered
+    SPACE = [' ', '\t', ]
+    nextitemsep = [separator]   # used for skipping trailing space
+    if name_value:
+        nextitemsep.append('=')
+    while idx < max:
+        char = args[idx]
+        next = None
+        if idx + 1 < max:
+            next = args[idx+1]
+        if skipquote:
+            skipquote -= 1
+        if not quoted and char in SPACE:
+            spaces = ''
+            # accumulate all space
+            while char in SPACE and idx < max - 1:
+                spaces += char
+                idx += 1
+                char = args[idx]
+            # remove space if args end with it
+            if char in SPACE and idx == max - 1:
+                break
+            # remove space at end of argument
+            if char in nextitemsep:
+                continue
+            idx -= 1
+            if not cur is None:
+                if cur:
+                    cur = cur + spaces
+            elif curname:
+                curname = curname + spaces
+        elif not quoted and name_value and char == '=':
+            if cur is None:
+                cur = u''
+                cur_quoted = False
+            else:
+                cur += '='
+            noquote = False
+        elif not quoted and not seplimit_reached and char == separator:
+            if cur is None:
+                cur = curname
+                curname = None
+            if not cur and not cur_quoted:
+                cur = None
+            if curname is not None:
+                keyword[curname] = cur
+                positional = ret_trailing
+            else:
+                positional.append(cur)
+            curname = u''
+            cur = None
+            noquote = False
+            cur_quoted = False
+            separator_count += 1
+            if seplimit and separator_count >= seplimit:
+                seplimit_reached = True
+                nextitemsep.remove(separator)
+        elif not quoted and not noquote and char == '"':
+            quoted = True
+            cur_quoted = True
+        elif quoted and not skipquote and char == '"':
+            if next == '"':
+                skipquote = 2 # will be decremented right away
+            else:
+                quoted = False
+        else:
+            if cur is not None:
+                cur = cur + char
+            else:
+                curname = curname + char
+            noquote = True
+
+        idx += 1
+
+    if cur is None:
+        cur = curname
+        curname = None
+    cur_present = cur is not None
+    if not cur and not cur_quoted:
+        cur = None
+    if curname is not None:
+        keyword[curname] = cur
+    elif cur_present:
+        positional.append(cur)
+
+    if name_value:
+        return ret_positional, keyword, ret_trailing
+    else:
+        return ret_positional
+
+
+def get_bool(request, arg, name=None, default=None):
+    """
+    For use with values returned from parse_quoted_separated or given
+    as macro parameters, return a boolean from a unicode string.
+    Valid input is 'true'/'false', 'yes'/'no' and '1'/'0' or None for
+    the default value.
+
+    @param request: A request instance
+    @param arg: The argument, may be None or a unicode string
+    @param name: Name of the argument, for error messages
+    @param default: default value if arg is None
+    @rtype: boolean or None
+    @returns: the boolean value of the string according to above rules
+              (or default value)
+    """
+    _ = request.getText
+    assert default is None or isinstance(default, bool)
+    if arg is None:
+        return default
+    elif not isinstance(arg, unicode):
+        raise TypeError('Argument must be None or unicode')
+    arg = arg.lower()
+    if arg in [u'0', u'false', u'no']:
+        return False
+    elif arg in [u'1', u'true', u'yes']:
+        return True
+    else:
+        if name:
+            raise ValueError(
+                _('Argument "%s" must be a boolean value, not "%s"') % (
+                    name, arg))
+        else:
+            raise ValueError(
+                _('Argument must be a boolean value, not "%s"') % arg)
+
+
+def get_int(request, arg, name=None, default=None):
+    """
+    For use with values returned from parse_quoted_separated or given
+    as macro parameters, return an integer from a unicode string
+    containing the decimal representation of a number.
+    None is a valid input and yields the default value.
+
+    @param request: A request instance
+    @param arg: The argument, may be None or a unicode string
+    @param name: Name of the argument, for error messages
+    @param default: default value if arg is None
+    @rtype: int or None
+    @returns: the integer value of the string (or default value)
+    """
+    _ = request.getText
+    assert default is None or isinstance(default, int)
+    if arg is None:
+        return default
+    elif not isinstance(arg, unicode):
+        raise TypeError('Argument must be None or unicode')
+    try:
+        return int(arg)
+    except ValueError:
+        if name:
+            raise ValueError(
+                _('Argument "%s" must be an integer value, not "%s"') % (
+                    name, arg))
+        else:
+            raise ValueError(
+                _('Argument must be an integer value, not "%s"') % arg)
+
+
+def get_float(request, arg, name=None, default=None):
+    """
+    For use with values returned from parse_quoted_separated or given
+    as macro parameters, return a float from a unicode string.
+    None is a valid input and yields the default value.
+
+    @param request: A request instance
+    @param arg: The argument, may be None or a unicode string
+    @param name: Name of the argument, for error messages
+    @param default: default return value if arg is None
+    @rtype: float or None
+    @returns: the float value of the string (or default value)
+    """
+    _ = request.getText
+    assert default is None or isinstance(default, float)
+    if arg is None:
+        return default
+    elif not isinstance(arg, unicode):
+        raise TypeError('Argument must be None or unicode')
+    try:
+        return float(arg)
+    except ValueError:
+        if name:
+            raise ValueError(
+                _('Argument "%s" must be a floating point value, not "%s"') % (
+                    name, arg))
+        else:
+            raise ValueError(
+                _('Argument must be a boolean value, not "%s"') % arg)
+
+
+def get_unicode(request, arg, name=None, default=None):
+    """
+    For use with values returned from parse_quoted_separated or given
+    as macro parameters, return a unicode string from a unicode string.
+    None is a valid input and yields the default value.
+
+    @param request: A request instance
+    @param arg: The argument, may be None or a unicode string
+    @param name: Name of the argument, for error messages
+    @param default: default return value if arg is None;
+    @rtype: unicode or None
+    @returns: the unicode string (or default value)
+    """
+    assert default is None or isinstance(default, unicode)
+    if arg is None:
+        return default
+    elif not isinstance(arg, unicode):
+        raise TypeError('Argument must be None or unicode')
+
+    return arg
+
+
+def get_choice(request, arg, name=None, choices=[None]):
+    """
+    For use with values returned from parse_quoted_separated or given
+    as macro parameters, return a unicode string that must be in the
+    choices given. None is a valid input and yields first of the valid
+    choices.
+
+    @param request: A request instance
+    @param arg: The argument, may be None or a unicode string
+    @param name: Name of the argument, for error messages
+    @param choices: the possible choices
+    @rtype: unicode or None
+    @returns: the unicode string (or default value)
+    """
+    assert isinstance(choices, tuple) or isinstance(choices, list)
+    if arg is None:
+        return choices[0]
+    elif not isinstance(arg, unicode):
+        raise TypeError('Argument must be None or unicode')
+    elif not arg in choices:
+        _ = request.getText
+        if name:
+            raise ValueError(
+                _('Argument "%s" must be one of "%s", not "%s"') % (
+                    name, '", "'.join(choices), arg))
+        else:
+            raise ValueError(
+                _('Argument must be one of "%s", not "%s"') % (
+                    '", "'.join(choices), arg))
+
+    return arg
+
+
+class required_arg:
+    """
+    Wrap a type in this class and give it as default argument
+    for a function passed to invoke_extension_function() in
+    order to get generic checking that the argument is given.
+    """
+    def __init__(self, argtype):
+        """
+        Initialise a required_arg
+        @param argtype: the type the argument should have
+        """
+        if not isinstance(argtype, type):
+            raise TypeError("argtype must be a type")
+        self.argtype = argtype
+
+
+def invoke_extension_function(request, function, args, fixed_args=[]):
+    """
+    Parses arguments for an extension call and calls the extension
+    function with the arguments.
+
+    If the macro function has a default value that is a bool,
+    int, long, float or unicode object, then the given value
+    is converted to the type of that default value before passing
+    it to the macro function. That way, macros need not call the
+    wikiutil.get_* functions for any arguments that have a default.
+
+    @param request: the request object
+    @param function: the function to invoke
+    @param args: unicode string with arguments (or evaluating to False)
+    @param fixed_args: fixed arguments to pass as the first arguments
+    @returns: the return value from the function called
+    """
+
+    def _convert_arg(request, value, default, name=None):
+        """
+        Using the get_* functions, convert argument to the type of the default
+        if that is any of bool, int, long, float or unicode; if the default
+        is the type itself then convert to that type (keeps None) or if the
+        default is a list require one of the list items.
+
+        In other cases return the value itself.
+        """
+        if isinstance(default, bool):
+            return get_bool(request, value, name, default)
+        elif isinstance(default, int) or isinstance(default, long):
+            return get_int(request, value, name, default)
+        elif isinstance(default, float):
+            return get_float(request, value, name, default)
+        elif isinstance(default, unicode):
+            return get_unicode(request, value, name, default)
+        elif isinstance(default, tuple) or isinstance(default, list):
+            return get_choice(request, value, name, default)
+        elif default is bool:
+            return get_bool(request, value, name)
+        elif default is int or default is long:
+            return get_int(request, value, name)
+        elif default is float:
+            return get_float(request, value, name)
+        elif isinstance(default, required_arg):
+            return _convert_arg(request, value, default.argtype, name)
+        return value
+
+    assert isinstance(fixed_args, list) or isinstance(fixed_args, tuple)
+
+    _ = request.getText
+
+    kwargs = {}
+    kwargs_to_pass = {}
+    trailing_args = []
+
+    if args:
+        assert isinstance(args, unicode)
+
+        positional, keyword, trailing = parse_quoted_separated(args)
+
+        for kw in keyword:
+            try:
+                kwargs[str(kw)] = keyword[kw]
+            except UnicodeEncodeError:
+                kwargs_to_pass[kw] = keyword[kw]
+
+        trailing_args.extend(trailing)
+
+    else:
+        positional = []
+
+    argnames, varargs, varkw, defaultlist = getargspec(function)
+    # self is implicit!
+    if isinstance(function, MethodType):
+        argnames = argnames[1:]
+    fixed_argc = len(fixed_args)
+    argnames = argnames[fixed_argc:]
+    argc = len(argnames)
+    if not defaultlist:
+        defaultlist = []
+
+    # if the fixed parameters have defaults too...
+    if argc < len(defaultlist):
+        defaultlist = defaultlist[fixed_argc:]
+    defstart = argc - len(defaultlist)
+
+    defaults = {}
+    # reverse to be able to pop() things off
+    positional.reverse()
+    allow_kwargs = False
+    allow_trailing = False
+    # convert all arguments to keyword arguments,
+    # fill all arguments that weren't given with None
+    for idx in range(argc):
+        argname = argnames[idx]
+        if argname == '_kwargs':
+            allow_kwargs = True
+            continue
+        if argname == '_trailing_args':
+            allow_trailing = True
+            continue
+        if positional:
+            kwargs[argname] = positional.pop()
+        if not argname in kwargs:
+            kwargs[argname] = None
+        if idx >= defstart:
+            defaults[argname] = defaultlist[idx - defstart]
+
+    if positional:
+        if not allow_trailing:
+            raise ValueError(_('Too many arguments'))
+        trailing_args.extend(positional)
+
+    if trailing_args:
+        if not allow_trailing:
+            raise ValueError(_('Cannot have arguments without name following'
+                               ' named arguments'))
+        kwargs['_trailing_args'] = trailing_args
+
+    # type-convert all keyword arguments to the type
+    # that the default value indicates
+    for argname in kwargs.keys()[:]:
+        if argname in defaults:
+            # the value of 'argname' from kwargs will be put into the
+            # macro's 'argname' argument, so convert that giving the
+            # name to the converter so the user is told which argument
+            # went wrong (if it does)
+            kwargs[argname] = _convert_arg(request, kwargs[argname],
+                                           defaults[argname], argname)
+            if (kwargs[argname] is None
+                and isinstance(defaults[argname], required_arg)):
+                raise ValueError(_('Argument "%s" is required') % argname)
+
+        if not argname in argnames:
+            # move argname into _kwargs parameter
+            kwargs_to_pass[argname] = kwargs[argname]
+            del kwargs[argname]
+
+    if kwargs_to_pass:
+        kwargs['_kwargs'] = kwargs_to_pass
+        if not allow_kwargs:
+            raise ValueError(_(u'No argument named "%s"') % (
+                kwargs_to_pass.keys()[0]))
+
+    return function(*fixed_args, **kwargs)
+
+
 def parseAttributes(request, attrstring, endtoken=None, extension=None):
     """
     Parse a list of attributes and return a dict plus a possible
@@ -1303,14 +1763,10 @@
             ("John Smith", male=True)
         this will result in the following dict:
             {"name": "John Smith", "age": None, "male": True}
-
-        @copyright: 2004 by Florian Festi,
-                    2006 by Mikko Virkkil
-        @license: GNU GPL, see COPYING for details.
     """
 
     def __init__(self, pattern):
-        #parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
+        # parameter_re = "([^\"',]*(\"[^\"]*\"|'[^']*')?[^\"',]*)[,)]"
         name = "(?P<%s>[a-zA-Z_][a-zA-Z0-9_]*)"
         int_re = r"(?P<int>-?\d+)"
         bool_re = r"(?P<bool>(([10])|([Tt]rue)|([Ff]alse)))"
@@ -1343,7 +1799,7 @@
                 named = True
                 self.param_dict[match.group('name')[1:-1]] = i
             elif named:
-                raise ValueError, "Named parameter expected"
+                raise ValueError("Named parameter expected")
             i += 1
 
     def __str__(self):
@@ -1351,65 +1807,60 @@
                                         self.optional)
 
     def parse_parameters(self, params):
-        """
-        (4, 2)
-        """
-        #Default list to "None"s
+        # Default list/dict entries to None
         parameter_list = [None] * len(self.param_list)
-        parameter_dict = {}
+        parameter_dict = dict([(key, None) for key in self.param_dict])
         check_list = [0] * len(self.param_list)
 
         i = 0
         start = 0
+        fixed_count = 0
         named = False
 
-        if not params:
-            params = '""'
-
         while start < len(params):
             match = re.match(self.param_re, params[start:])
             if not match:
-                raise ValueError, "Misformatted value"
+                raise ValueError("malformed parameters")
             start += match.end()
-            value = None
             if match.group("int"):
-                value = int(match.group("int"))
-                type = 'i'
+                pvalue = int(match.group("int"))
+                ptype = 'i'
             elif match.group("bool"):
-                value = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
-                type = 'b'
+                pvalue = (match.group("bool") == "1") or (match.group("bool") == "True") or (match.group("bool") == "true")
+                ptype = 'b'
             elif match.group("float"):
-                value = float(match.group("float"))
-                type = 'f'
+                pvalue = float(match.group("float"))
+                ptype = 'f'
             elif match.group("string"):
-                value = match.group("string")[1:-1]
-                type = 's'
+                pvalue = match.group("string")[1:-1]
+                ptype = 's'
             elif match.group("name_param"):
-                value = match.group("name_param")
-                type = 'n'
+                pvalue = match.group("name_param")
+                ptype = 'n'
             else:
-                value = None
+                raise ValueError("Parameter parser code does not fit param_re regex")
 
-            parameter_list.append(value)
-            if match.group("name"):
-                if match.group("name") not in self.param_dict:
+            name = match.group("name")
+            if name:
+                if name not in self.param_dict:
                     # TODO we should think on inheritance of parameters
-                    raise ValueError, "Unknown parameter name '%s'" % match.group("name")
-                nr = self.param_dict[match.group("name")]
+                    raise ValueError("unknown parameter name '%s'" % name)
+                nr = self.param_dict[name]
                 if check_list[nr]:
-                    #raise ValueError, "Parameter specified twice"
-                    #TODO: Something saner that raising an exception. This is pretty good, since it ignores it.
-                    pass
+                    raise ValueError("parameter '%s' specified twice" % name)
                 else:
                     check_list[nr] = 1
-                parameter_dict[match.group("name")] = value
-                parameter_list[nr] = value
+                pvalue = self._check_type(pvalue, ptype, self.param_list[nr])
+                parameter_dict[name] = pvalue
+                parameter_list[nr] = pvalue
                 named = True
             elif named:
-                raise ValueError, "Only named parameters allowed"
+                raise ValueError("only named parameters allowed after first named parameter")
             else:
                 nr = i
-                parameter_list[nr] = value
+                if nr not in self.param_dict.values():
+                    fixed_count = nr + 1
+                parameter_list[nr] = self._check_type(pvalue, ptype, self.param_list[nr])
 
             # Let's populate and map our dictionary to what's been found
             for name in self.param_dict:
@@ -1418,50 +1869,37 @@
 
             i += 1
 
-        return parameter_list, parameter_dict
-
-""" never used:
-    def _check_type(value, type, format):
-        if type == 'n' and 's' in format: # n as s
-            return value
-
-        if type in format:
-            return value # x -> x
+        for i in range(fixed_count):
+            parameter_dict[i] = parameter_list[i]
 
-        if type == 'i':
+        return fixed_count, parameter_dict
+
+    def _check_type(self, pvalue, ptype, format):
+        if ptype == 'n' and 's' in format: # n as s
+            return pvalue
+
+        if ptype in format:
+            return pvalue # x -> x
+
+        if ptype == 'i':
             if 'f' in format:
-                return float(value) # i -> f
+                return float(pvalue) # i -> f
             elif 'b' in format:
-                return value # i -> b
-        elif type == 'f':
+                return pvalue != 0 # i -> b
+        elif ptype == 's':
             if 'b' in format:
-                return value  # f -> b
-        elif type == 's':
-            if 'b' in format:
-                return value.lower() != 'false' # s-> b
+                if pvalue.lower() == 'false':
+                    return False # s-> b
+                elif pvalue.lower() == 'true':
+                    return True # s-> b
+                else:
+                    raise ValueError('%r does not match format %r' % (pvalue, format))
 
         if 's' in format: # * -> s
-            return str(value)
-        else:
-            pass # XXX error
-
-def main():
-    pattern = "%i%sf%s%ifs%(a)s|%(b)s"
-    param = ' 4,"DI\'NG", b=retry, a="DING"'
-
-    #p_list, p_dict = parse_parameters(param)
+            return str(pvalue)
 
-    print 'Pattern :', pattern
-    print 'Param :', param
+        raise ValueError('%r does not match format %r' % (pvalue, format))
 
-    P = ParameterParser(pattern)
-    print P
-    print P.parse_parameters(param)
-
-
-if __name__=="__main__":
-    main()
-"""
 
 #############################################################################
 ### Misc