Welcome, guest | Sign In | My Account | Store | Cart

Python 3 introduced a useful feature: keyword-only arguments. In order to get the same effect in Python 2, you must use **kwargs in your parameter list. Then, at the beginning of your function body you must manually extract what would be the keyword-only arguments once you upgrade to 3.

This recipe helps reduce the boilerplate to a single function call. You still don't get those parameters in your "def" clause (where they are more obvious), but at least it reduces the clutter.

Python, 109 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
"""kwonly module

"""

def emulate_kwonly(kws, required, withdefaults, leftovers=False):
    """Emulate Python 3's kwonly arguments.

    Parameters:
      kws - the kwargs from which to extract the kwonly args.
      required - an iterable holding the required kwonly args.
      withdefaults - an iterable of pairs mapping names to defaults.
      leftovers - allow kws to be non-empty when all the kwonly args
            have already been popped off.

    Returns:
      The remainder of kws, followed by the values for the kwonly args
      in the same order as they stand in required and then in
      withdefaults.

    Examples:
      Below each "def" clause you'll find the clause that would be
      equivalent in Python 3 to the use of emulate_kwonly().

      >>> def f(a, **kwargs):
      ... #def f(a, *, b, c=5):
      ...     kwargs, b, c = emulate_kwonly(kwargs, ("b",), (("c", 5),))
      ...     # continue as normal
      ...
      >>> def g(a, *args, **kwargs):
      ... #def f(a, *args, b, **kwargs):
      ...     kwargs, b = emulate_kwonly(kwargs, ("b",), (), True)
      ...     # continue as normal

    """

    if hasattr(withdefaults, "items"):
        # allows for OrderedDict to be passed
        withdefaults = withdefaults.items()

    kwonly = []

    # extract the required keyword-only arguments
    missing = []
    for name in required:
        if name not in kws:
            missing.append(name)
        else:
            kwonly.append(kws.pop(name))

    # validate required keyword-only arguments
    if len(missing) > 2:
        end = "s: %s, and %s" % (", ".join(missing[:-1]), missing[-1])
    elif len(missing) == 2:
        end = "s: %s and %s" % tuple(missing)
    elif len(missing) == 1:
        end = ": %s" % tuple(missing)
    if missing:
        msg = "missing %s required keyword-only argument%s"
        raise TypeError(msg % (len(missing), end))

    # handle the withdefaults
    for name, value in withdefaults:
        if name not in kws:
            kwonly.append(value)
        else:
            kwonly.append(kws.pop(name))

    # handle any leftovers
    if not leftovers and kws:
        msg = "got an unexpected keyword argument '%s'"
        raise TypeError(msg % (kws.keys()[0]))

    return [kws] + kwonly


if __name__ == "__main__":

    def f(a, **kwargs):
    #def f(a, *, b, c=5):
        kwargs, b, c = emulate_kwonly(kwargs, ("b",), (("c", 5),))
        return a, b, c

    assert f(1, b=2) == (1, 2, 5)
    assert f(1, b=2, c=3) == (1, 2, 3)

    try: f(b=2)
    except TypeError: pass
    else: raise AssertionError

    try: f(1, 2)
    except TypeError: pass
    else: raise AssertionError

    try: f(1, c=2)
    except TypeError: pass
    else: raise AssertionError

    try: f(1, b=2, d=4)
    except TypeError: pass
    else: raise AssertionError


    def g(a, *args, **kwargs):
    #def f(a, *args, b, **kwargs):
        kwargs, b = emulate_kwonly(kwargs, ("b",), (), True)
        return a, b, kwargs

    assert g(1, b=2) == (1, 2, {})
    assert g(1, b=2, c=3) == (1, 2, dict(c=3))