0

I want to mimic this behaviour of Python3.8 in Python3.7

Positional-only parameters / was the syntax introduced to indicate that some function parameters must be specified positionally and cannot be used as keyword arguments.

#Python3.8
def f(a,b,/,**kwargs):
    print(a,b,kwargs)

>>> f(1,2,**{'a':100,'b':200,'c':300})
# 1 2 {'a': 100, 'b': 200, 'c': 300}

a,b are used only as positional parameters.

How I do the same in Python3.7

#Python3.7

def f(a,b,**kwargs):
    print(a,b,kwargs)

>>> f(1,2,**{'a':1,'b':2})
# TypeError: f() got multiple values for argument 'a'

How do I make a,b to be only positional parameters. / doesn't work below from Python3.8

Is it possible to mimic / syntax in Python3.7?

Ch3steR
  • 20,090
  • 4
  • 28
  • 58
  • Seems related to this: https://stackoverflow.com/questions/52839856/how-to-implement-positional-only-parameter-in-a-user-defined-function-in-pytho – dspencer Mar 18 '20 at 13:08
  • @dspencer To enforce them as positional the only workaround is to use `*args` but I want to make only 1st two parameters as positional. Still going through all the answer there. Thanks for taking out time. – Ch3steR Mar 18 '20 at 13:13
  • @a_guest Why did you delete the answer. I was still testing it. – Ch3steR Mar 18 '20 at 13:19
  • @Ch3steR Because it cannot work for the use case you specified. It only works if there is no name clash between positional only arguments and keyword arguments. You say you want to have `a` as positional-only parameter *and* have a kwarg `a`. That's not going to work not even with a decorator. Only if you can guarantee not to have name clashes it will work. I can undelete and comment on that if you want. So it will only check that you don't pass positional-only parameters as keyword argument, but it won't allow those names in keyword arguments. – a_guest Mar 18 '20 at 13:26
  • @a_guest I want to have `a` as a positional only argument and have a kwarg `a` too. Is it not possible? – Ch3steR Mar 18 '20 at 13:28
  • 2
    @Ch3steR Hm maybe it's possible with some additional logic. I will think about it and post an update when I have something. – a_guest Mar 18 '20 at 13:30
  • 1
    @Ch3steR It would be possible you defined your function as `foo(a, kwargs)`, i.e. without argument packing, and have it immediately decorated. The decorator would consume the keyword arguments and pass them on as a plain dict. Without the decorator the function works very different of course. Is that acceptable? – a_guest Mar 18 '20 at 13:44
  • @a_guest Yes, I started learning python recently it maybe a bad question. If it's the way to do it post it. I'll accept it as answer. – Ch3steR Mar 18 '20 at 13:52
  • @Ch3steR But if you also want keyword-only parameters or parameters with default values then it becomes really weird since the `kwargs` needs to come before as positional-or-keyword parameter; and you also cannot have a keyword argument named `kwargs` since that introduces a name clash again. So I'd rather not post an answer since it will not support the full syntax and hence I think it won't be helpful. – a_guest Mar 18 '20 at 14:18
  • @a_guest Thanks for taking out your time in helping. Hope someone will post a valid answer. I guess the questions already dead. Should I delete this question and ask again? – Ch3steR Mar 18 '20 at 14:29
  • @Ch3steR It's just not generally possible. Especially using `def foo(a, **kw)` it's not possible to pass both `a` positional and as keyword in Python 3.7. Maybe you can make it work when going to the C-level but I'm not even sure about that. And whatever your solution is, it will be implementation specific. – a_guest Mar 18 '20 at 14:32
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/209852/discussion-between-a-guest-and-ch3ster). – a_guest Mar 18 '20 at 14:32
  • @Ch3steR Please see my updated answer. – a_guest Mar 18 '20 at 21:01

1 Answers1

1

You can create a custom decorator that declares the positional-only arguments, returning a wrapper that parses its own *args, **kwargs such that they fit the signature of the decorated function. Due to possible name clashes between positional-only and keyword arguments, it is not possible to use keyword-argument-packing (**) for this approach (this is the only limitation). Packed keyword arguments need to be declared either as the last positional-or-keyword parameter or as the first keyword-only parameter. Here are two examples:

def foo(a, b, kwargs):  # last positional-or-keyword parameter
    pass

def foo(a, *args, kwargs):  # first keyword-only parameter
    pass

The variable kwargs will receive the remaining **kwargs from the wrapper function, i.e. it can be used similarly as if **kwargs had been used in the decorated function directly (like in Python 3.8+).

The following implementation of the decorator is largely based on the implementation of inspect.Signature.bind with a few minor tweaks to handle positional-only parameters via the decorator-declared names and to handle the additional (artificial) kwargs parameter.

import functools
import inspect
import itertools


def positional_only(*names, kwargs_name='kwargs'):
    def decorator(func):
        signature = inspect.signature(func)

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            new_args = []
            new_kwargs = {}            

            parameters = iter(signature.parameters.values())
            parameters_ex = ()
            arg_vals = iter(args)

            while True:
                try:
                    arg_val = next(arg_vals)
                except StopIteration:
                    try:
                        param = next(parameters)
                    except StopIteration:
                        break
                    else:
                        if param.name == kwargs_name or param.kind == inspect.Parameter.VAR_POSITIONAL:
                            break
                        elif param.name in kwargs:
                            if param.name in names:
                                msg = '{arg!r} parameter is positional only, but was passed as a keyword'
                                msg = msg.format(arg=param.name)
                                raise TypeError(msg) from None
                            parameters_ex = (param,)
                            break
                        elif param.default is not inspect.Parameter.empty:
                            parameters_ex = (param,)
                            break
                        else:
                            msg = 'missing a required argument: {arg!r}'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg) from None
                else:
                    try:
                        param = next(parameters)
                    except StopIteration:
                        raise TypeError('too many positional arguments') from None
                    else:
                        if param.name == kwargs_name or param.kind == inspect.Parameter.KEYWORD_ONLY:
                            raise TypeError('too many positional arguments') from None

                        if param.kind == inspect.Parameter.VAR_POSITIONAL:
                            new_args.append(arg_val)
                            new_args.extend(arg_vals)
                            break

                        if param.name in kwargs and param.name not in names:
                            raise TypeError(
                                'multiple values for argument {arg!r}'.format(
                                    arg=param.name)) from None

                        new_args.append(arg_val)

            for param in itertools.chain(parameters_ex, parameters):
                if param.name == kwargs_name or param.kind == inspect.Parameter.VAR_POSITIONAL:
                    continue

                try:
                    arg_val = kwargs.pop(param.name)
                except KeyError:
                    if (param.kind != inspect.Parameter.VAR_POSITIONAL
                            and param.default is inspect.Parameter.empty):
                        raise TypeError(
                            'missing a required argument: {arg!r}'.format(
                                arg=param.name)) from None
                else:
                    if param.name in names:
                        raise TypeError(
                            '{arg!r} parameter is positional only, '
                            'but was passed as a keyword'.format(arg=param.name))

                    new_kwargs[param.name] = arg_val

            new_kwargs.update(kwargs=kwargs)
            return func(*new_args, **new_kwargs)
        return wrapper
    return decorator

Here is an example of how it can be used:

@positional_only('a')
def foo(a, *args, kwargs, b=9, c):
    print(a, args, b, c, kwargs)

foo(1, **dict(a=2), c=3)  # ok
foo(1, 2, 3, 4, 5, c=6)  # ok
foo(1, b=2, **dict(a=3), c=4)  # ok
foo(a=1, c=2)  # error
foo(c=1)  # error
a_guest
  • 34,165
  • 12
  • 64
  • 118