Commits

Joe Amenta committed 4b2f407

Fixer for keyword-only arguments is complete.

Comments (0)

Files changed (3)

lib3to2/fixer_util.py

 def Star(prefix=None):
     return Leaf(token.STAR, '*', prefix=prefix)
 
+def DoubleStar(prefix=None):
+    return Leaf(token.DOUBLESTAR, '**', prefix=prefix)
+
 def Minus(prefix=None):
     return Leaf(token.MINUS, '-', prefix=prefix)
 

lib3to2/fixes/fix_kwargs.py

 """
 Fixer for Python 3 function parameter syntax
+This fixer is rather sensitive to incorrect py3k syntax.
 """
 
 # Note: "relevant" parameters are parameters following the first STAR in the list.
 
 from lib2to3 import fixer_base
-from ..fixer_util import token
+from ..fixer_util import token, indentation, suitify, String, Newline, Comma, DoubleStar, Name
 
-_if_template = "if '%(name)s' in %(kwargs)s: %(name)s = %(kwargs)s['%(name)s']"
+_assign_template = "%(name)s = %(kwargs)s['%(name)s']"
+_if_template = "if '%(name)s' in %(kwargs)s: %(assign)s"
 _else_template = "else: %(name)s = %(default)s"
+_kwargs_default_name = "_3to2kwargs"
 
-def needs_fixing(relevant_params):
+def gen_params(raw_params):
+    """
+    Generator that yields tuples of (name, default_value) for each parameter in the list
+    If no default is given, then it is default_value is None (not Leaf(token.NAME, 'None'))
+    """
+    assert raw_params[0].type == token.STAR and len(raw_params) > 2
+    curr_idx = 2 # the first place a keyword-only parameter name can be is index 2
+    max_idx = len(raw_params)
+    while curr_idx < max_idx:
+        curr_item = raw_params[curr_idx]
+        prev_item = curr_item.prev_sibling
+        if curr_item.type != token.NAME:
+            curr_idx += 1
+            continue
+        if prev_item is not None and prev_item.type == token.DOUBLESTAR:
+            break
+        name = curr_item.value
+        nxt = curr_item.next_sibling
+        if nxt is not None and nxt.type == token.EQUAL:
+            default_value = nxt.next_sibling
+            curr_idx += 2
+        else:
+            default_value = None
+        yield (name, default_value)
+        curr_idx += 1
+
+def remove_params(raw_params, kwargs_default=_kwargs_default_name):
+    """
+    Removes all keyword-only args from the params list and a bare star, if any.
+    Does not add the kwargs dict if needed.
+    Returns True if more action is needed, False if not
+    (more action is needed if no kwargs dict exists)
+    """
+    assert raw_params[0].type == token.STAR
+    if raw_params[1].type == token.COMMA:
+        raw_params[0].remove()
+        raw_params[1].remove()
+        kw_params = raw_params[2:]
+    else:
+        kw_params = raw_params[3:]
+    for param in kw_params:
+        if param.type != token.DOUBLESTAR:
+            param.remove()
+        else:
+            return False
+    else:
+        return True
+    
+def needs_fixing(raw_params, kwargs_default=_kwargs_default_name):
     """
     Returns string with the name of the kwargs dict if the params after the first star need fixing
     Otherwise returns empty string
     found_kwargs = False
     needs_fix = False
 
-    for t in relevant_params:
+    for t in raw_params:
         if t.type == token.COMMA:
             # Commas are irrelevant at this stage.
             continue
             found_kwargs = True
     else:
         # Never found **foobar.  Return a synthetic name, if needed.
-        return '_3to2kwargs' if needs_fix else ''
+        return kwargs_default if needs_fix else ''
 
 class FixKwargs(fixer_base.BaseFix):
 
     explicit = True # not sufficiently tested
     
-    PATTERN = "funcdef< 'def' NAME parameters< '(' typedargslist< params=any* > ')' > ':' suite=any >"
+    PATTERN = "funcdef< 'def' NAME parameters< '(' arglist=typedargslist< params=any* > ')' > ':' suite=any >"
 
     def transform(self, node, results):
-        params = results["params"]
-        for i, item in enumerate(params):
+        params_rawlist = results["params"]
+        for i, item in enumerate(params_rawlist):
             if item.type == token.STAR:
-                relevant_params = params[i:]
+                params_rawlist = params_rawlist[i:]
                 break
         else:
             return
-        # relevant_params is guaranteed to be a list starting with *.
+        # params is guaranteed to be a list starting with *.
         # if fixing is needed, there will be at least 3 items in this list:
         # [STAR, COMMA, NAME] is the minimum that we need to worry about.
-        new_kwargs = needs_fixing(relevant_params)
+        new_kwargs = needs_fixing(params_rawlist)
         # new_kwargs is the name of the kwargs dictionary.
         if not new_kwargs:
             return
+        suitify(node)
 
-        # At this point, relevant_params is guaranteed to be a list
+        # At this point, params_rawlist is guaranteed to be a list
         # beginning with a star that includes at least one keyword-only param
         # e.g., [STAR, NAME, COMMA, NAME, COMMA, DOUBLESTAR, NAME] or
         # [STAR, COMMA, NAME], or [STAR, COMMA, NAME, COMMA, DOUBLESTAR, NAME]
 
-        
+        # Anatomy of a funcdef: ['def', 'name', parameters, ':', suite]
+        # Anatomy of that suite: [NEWLINE, INDENT, first_stmt, all_other_stmts]
+        # We need to insert our new stuff before the first_stmt and change the
+        # first_stmt's prefix.
+
+        suite = node.children[4]
+        first_stmt = suite.children[2]
+        ident = indentation(first_stmt)
+
+        for name, default_value in gen_params(params_rawlist):
+            if default_value is None:
+                suite.insert_child(2, Newline())
+                suite.insert_child(2, String(_assign_template %{'name':name, 'kwargs':new_kwargs}, prefix=ident))
+            else:
+                suite.insert_child(2, Newline())
+                suite.insert_child(2, String(_else_template %{'name':name, 'default':default_value}, prefix=ident))
+                suite.insert_child(2, Newline())
+                suite.insert_child(2, String(_if_template %{'assign':_assign_template %{'name':name, 'kwargs':new_kwargs}, 'name':name, 'kwargs':new_kwargs}, prefix=ident))
+        first_stmt.prefix = ident
+        suite.children[2].prefix = ""
+
+        # Now, we need to fix up the list of params.
+
+        must_add_kwargs = remove_params(params_rawlist)
+        if must_add_kwargs:
+            arglist = results['arglist']
+            if len(arglist.children) > 0 and arglist.children[-1].type != token.COMMA:
+                arglist.append_child(Comma())
+            arglist.append_child(DoubleStar(prefix=" "))
+            arglist.append_child(Name(new_kwargs))
+            

lib3to2/tests/test_kwargs.py

             funky()"""
         a = """
         def spam(ham, *args, **_3to2kwargs):
+            monkeys = _3to2kwargs['monkeys']
             eggs = _3to2kwargs['eggs']
-            monkeys = _3to2kwargs['monkeys']
             funky()"""
         self.check(b, a)
 
             funky()"""
         a = """
         def spam(ham, *args, **stuff):
+            monkeys = stuff['monkeys']
             eggs = stuff['eggs']
-            monkeys = stuff['monkeys']
             funky()"""
         self.check(b, a)
 
             funky()"""
         a = """
         def spam(ham, **_3to2kwargs):
+            monkeys = _3to2kwargs['monkeys']
             eggs = _3to2kwargs['eggs']
-            monkeys = _3to2kwargs['monkeys']            
             funky()"""
         self.check(b, a)
 
 
-    def test_bare_star_named_defaults(self):
+    def test_bare_star_named_simple_defaults(self):
         b = """
         def spam(ham, *, dinosaurs, eggs=3, monkeys=2):
             funky()"""
         a = """
         def spam(ham, **_3to2kwargs):
-            dinosaurs = _3to2kwargs['dinosaurs']
+            if 'monkeys' in _3to2kwargs: monkeys = _3to2kwargs['monkeys']
+            else: monkeys = 2
             if 'eggs' in _3to2kwargs: eggs = _3to2kwargs['eggs']
             else: eggs = 3
-            if 'monkeys' in _3to2kwargs: monkeys = _3to2kwargs['monkeys']
-            else: monkeys = 2
+            dinosaurs = _3to2kwargs['dinosaurs']
             funky()"""
         self.check(b, a)
 
 
-    def test_bare_star_named_defaults_catchall(self):
+    def test_bare_star_named_simple_defaults_catchall(self):
         b = """
         def spam(ham, *, dinosaurs, eggs=3, monkeys=2, **stuff):
             funky()"""
         a = """
         def spam(ham, **stuff):
-            dinosaurs = stuff['dinosaurs']
+            if 'monkeys' in stuff: monkeys = stuff['monkeys']
+            else: monkeys = 2
             if 'eggs' in stuff: eggs = stuff['eggs']
             else: eggs = 3
-            if 'monkeys' in stuff: monkeys = stuff['monkeys']
-            else: monkeys = 2
+            dinosaurs = stuff['dinosaurs']
             funky()"""
         self.check(b, a)
         
+    def test_bare_star_named_complicated_defaults(self):
+        b = """
+        def spam(ham, *, dinosaurs, eggs=call_fn(lambda a: b), monkeys=[i.split() for i in something(args)]):
+            funky()"""
+        a = """
+        def spam(ham, **_3to2kwargs):
+            if 'monkeys' in _3to2kwargs: monkeys = _3to2kwargs['monkeys']
+            else: monkeys = [i.split() for i in something(args)]
+            if 'eggs' in _3to2kwargs: eggs = _3to2kwargs['eggs']
+            else: eggs = call_fn(lambda a: b)
+            dinosaurs = _3to2kwargs['dinosaurs']
+            funky()"""
+        self.check(b, a)
+
+
+    def test_bare_star_named_complicated_defaults_catchall(self):
+        b = """
+        def spam(ham, *, dinosaurs, eggs=call_fn(lambda a: b), monkeys=[i.split() for i in something(args)], **stuff):
+            funky()"""
+        a = """
+        def spam(ham, **stuff):
+            if 'monkeys' in stuff: monkeys = stuff['monkeys']
+            else: monkeys = [i.split() for i in something(args)]
+            if 'eggs' in stuff: eggs = stuff['eggs']
+            else: eggs = call_fn(lambda a: b)
+            dinosaurs = stuff['dinosaurs']
+            funky()"""
+        self.check(b, a)
+        
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.