changeset 2507:ea255685d6b0

add macro argument parser and use it to invoke macros with args directly
author Johannes Berg <johannes AT sipsolutions DOT net>
date Sun, 22 Jul 2007 15:58:55 +0200
parents 426a8cb8e44c
children 552e748b86cd
files MoinMoin/_tests/test_wikiutil.py MoinMoin/macro/__init__.py MoinMoin/wikiutil.py
diffstat 3 files changed, 254 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/test_wikiutil.py	Sun Jul 22 05:43:52 2007 +0200
+++ b/MoinMoin/_tests/test_wikiutil.py	Sun Jul 22 15:58:55 2007 +0200
@@ -233,5 +233,76 @@
         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'}, [])),
+                  # 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'])),
+                ]
+        for args, expected in tests:
+            result = wikiutil.parse_quoted_separated(args)
+            assert expected == result
+
+    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
+
+    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
+
+
 coverage_modules = ['MoinMoin.wikiutil']
 
--- a/MoinMoin/macro/__init__.py	Sun Jul 22 05:43:52 2007 +0200
+++ b/MoinMoin/macro/__init__.py	Sun Jul 22 15:58:55 2007 +0200
@@ -94,6 +94,50 @@
         # Initialized on execute
         self.name = None
 
+    def _wrap(self, macro_name, fn, args):
+        """
+        Parses arguments for a macro call and calls the macro
+        function with the arguments.
+        """
+        if args:
+            positional, keyword, trailing = \
+                wikiutil.parse_quoted_separated(args)
+
+            kwargs = {}
+            nonascii = {}
+            for kw in keyword:
+                try:
+                    kwargs[str(kw)] = keyword[kw]
+                except UnicodeEncodeError:
+                    nonascii[kw] = keyword[kw]
+
+            # add trailing args as keyword argument if present,
+            # otherwise remove if the user entered some
+            # (so macros don't get a string where they expect a list)
+            if trailing:
+                kwargs['_trailing_args'] = trailing
+            elif '_trailing_args' in kwargs:
+                del kwargs['_trailing_args']
+
+            # add nonascii args as keyword argument if present,
+            # otherwise remove if the user entered some
+            # (so macros don't get a string where they expect a list)
+            if nonascii:
+                kwargs['_non_ascii_kwargs'] = nonascii
+            elif '_non_ascii_kwargs' in kwargs:
+                del kwargs['_non_ascii_kwargs']
+
+        else:
+            positional = []
+            kwargs = {}
+
+        try:
+            return fn(self, *positional, **kwargs)
+        except TypeError, e:
+            return u'[[%s]]' % str(e)
+        except ValueError, e:
+            return u'[[%s: %s]]' % (macro_name, str(e))
+
     def execute(self, macro_name, args):
         """ Get and execute a macro
 
@@ -102,7 +146,12 @@
         """
         self.name = macro_name
         try:
-            execute = wikiutil.importPlugin(self.cfg, 'macro', macro_name)
+            try:
+                call = wikiutil.importPlugin(self.cfg, 'macro', macro_name,
+                                             function='macro_%s' % macro_name)
+                execute = lambda _self, _args: _self._wrap(macro_name, call, _args)
+            except wikiutil.PluginAttributeError:
+                execute = wikiutil.importPlugin(self.cfg, 'macro', macro_name)
         except wikiutil.PluginMissingError:
             try:
                 builtins = self.__class__
--- a/MoinMoin/wikiutil.py	Sun Jul 22 05:43:52 2007 +0200
+++ b/MoinMoin/wikiutil.py	Sun Jul 22 15:58:55 2007 +0200
@@ -1191,6 +1191,139 @@
 ### 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.
+
+    Arguments that are not given are returned as None, while the
+    empty string can be achieved by quoting it.
+
+    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
+    max = len(args)
+    ret_positional = [] # positional argument return value
+    ret_trailing = []   # trailing arguments return value
+    positional = ret_positional
+    keyword = {}        # keyword arguments
+    curname = ''        # 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 = False   # 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 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 = ''
+                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 = ''
+            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 == '"' and next != '"':
+            quoted = False
+        elif quoted and char == '"' and next == '"':
+            skipquote = True
+        else:
+            if cur is not None:
+                cur = cur + char
+            else:
+                curname = curname + char
+            skipquote = False
+            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 parseAttributes(request, attrstring, endtoken=None, extension=None):
     """
     Parse a list of attributes and return a dict plus a possible