5
\$\begingroup\$

Background

In Python, function arguments are defined within the parentheses following the function name in the function definition. There are different ways to present function arguments, and they can be categorised into three types:

  1. Positional-only arguments,
  2. Positional or keyword arguments, and
  3. Keyword-only arguments

Each of these may also have a default value.

Example function (de)composition

Let's break down an example function definition:

def fn(q: str, /, z: int, x: float, b: int = 1, *, c: int = 1) -> None:
    ...
  1. Positional-only argument (q: str): The parameter q is a positional-only argument because it is defined before the slash (/) in the argument list. It means that this argument can only be passed by its position and not by using a keyword. For example, you can call the function as fn("hello", 2.5, 3).

  2. Positional or keyword argument (z: int, x: float): The parameters z and x are defined after the slash (/), but they are not marked as keyword-only arguments. It means that these arguments can be passed either by their position or by using their corresponding keyword. For example, you can call the function as fn("hello", 5, x=2.5) or fn("hello", z=5, x=2.5).

  3. Default argument (b: int = 1): The parameter b has a default value of 1. It means that if no argument is provided for b when calling the function, it will automatically be assigned the default value. For example, you can call the function as fn("hello", 5, 2.5, c=3) or fn("hello", 5, 2.5, 2).

  4. Keyword-only argument (c: int = 1): The parameter c is a keyword-only argument because it is defined after the asterisk (*). It means that this argument can only be passed by using its corresponding keyword and cannot be passed by position. For example, you can call the function as fn("hello", 5, 2.5, c=3).

  5. Return type (-> None): The -> None annotation specifies the return type of the function. In this case, the function is expected to return None. This can be ignored.

N.B. Non-default positional arguments can not come after a default argument. That is, def fn_bad(a: int = 1, / b: int, *, c: int = 1, d: str) -> None:... is invalid due to b. b is a positional or keyword argument but it comes after a defaulted positional argument. d is valid however since this is a keyword only argument and their order does not matter.

Valid method invocations

There are 14 possible ways to call the function fn based on its function definition. Here are all the combinations:

  1. fn("hello", 2, 3.0)
  2. fn("hello", 2, 3.0, 1)
  3. fn("hello", 2, 3.0, c=1)
  4. fn("hello", 2, x=3.0)
  5. fn("hello", 2, x=3.0, b=1)
  6. fn("hello", 2, x=3.0, c=1)
  7. fn("hello", z=2, x=3.0)
  8. fn("hello", z=2, x=3.0, b=1)
  9. fn("hello", z=2, x=3.0, c=1)
  10. fn("hello", 2, 3.0, b=1, c=1)
  11. fn("hello", z=2, x=3.0, b=1, c=1)
  12. fn("hello", z=2, x=3.0, b=1, c=1)
  13. fn("hello", 2, x=3.0, b=1, c=1)
  14. fn("hello", 2, 3.0, 1, c=1)

In the above examples, "hello" is passed as the value for the positional-only argument q, 2 is passed for the positional or keyword argument z, 3.0 is passed for the positional or keyword argument x, 1 is passed for the default argument b, and 1 is passed for the keyword-only argument c. The arguments can be passed either by position or by using their corresponding keywords, depending on the argument type.

Keyword arguments are order independent, that is fn("hello", b=1, c=1, x=3.0, z=2) and fn("hello", x=3.0, b=1, z=2, c=1) are congruent.

Invalid invocation examples include: fn("hello", 2, x=3.0, 1) since a positional argument comes after a keyword argument, and fn("hello", z=2, b=1, c=1) due to missing required argument x

The Challenge

Create a script which generates all possible python function invocation signatures, describing the function arguments as their argument name, if it is passed as a keyword argument, and the type of the argument.

To make things fair across languages; let's say: the inputs are three lists; positional_only, positional_or_keyword and keyword_only where their elements describe the python function's arguments in the format of [string name, string type, bool has_default] Or some similar data structure. These inputs are not counted towards the character count. The output can be any intelligible format.

(N.B. This challenge disregards any function which contains args or kwargs catch all variables (*foo, or **bar))

Test cases (need more)

# def fn(q: str, /, z: int, x: float, b: int = 1, *, c: int = 1) -> None:
positional_only = [["q", "str", false]]
positional_or_keyword = [["z", "int", false], ["x", "float", false], ["b", "int", true]]
keyword_only =[["c", "int", true]]

generate_signatures(positional_only, positional_or_keyword, keyword_only)

# returns:
[
    [(None, 'str'), ('x', 'float'), ('z', 'int')],
    [(None, 'str'), ('c', 'int'), ('x', 'float'), ('z', 'int')],
    [(None, 'str'), ('b', 'int'), ('x', 'float'), ('z', 'int')],
    [(None, 'str'), ('b', 'int'), ('c', 'int'), ('x', 'float'), ('z', 'int')],
    [(None, 'str'), (None, 'int'), ('x', 'float')],
    [(None, 'str'), (None, 'int'), ('c', 'int'), ('x', 'float')],
    [(None, 'str'), (None, 'int'), ('b', 'int'), ('x', 'float')],
    [(None, 'str'), (None, 'int'), ('b', 'int'), ('c', 'int'), ('x', 'float')],
    [(None, 'str'), (None, 'int'), (None, 'float')],
    [(None, 'str'), (None, 'int'), (None, 'float'), ('c', 'int')],
    [(None, 'str'), (None, 'int'), (None, 'float'), ('b', 'int')],
    [(None, 'str'), (None, 'int'), (None, 'float'), ('b', 'int'), ('c', 'int')],
    [(None, 'str'), (None, 'int'), (None, 'float'), (None, 'int')],
    [(None, 'str'), (None, 'int'), (None, 'float'), (None, 'int'), ('c', 'int')],
]


# def fn_2(a: int, b: int = 1, *, d: int, c:int = 1) -> None: ...
positional_only = []
positional_or_keyword = [["a", "int", false], ["b", "int", true]]
keyword_only =[["d", "int", false], ["c", "int", true]]

generate_signatures(positional_only, positional_or_keyword, keyword_only)

# returns
[
    [('a', int), ('d', int)],
    [('a', int), ('c', int), ('d', int)],
    [('a', int), ('b', int), ('d', int)],
    [('a', int), ('b', int), ('c', int), ('d', int)],
    [(None, int), ('d', int)],
    [(None, int), ('c', int), ('d', int)],
    [(None, int), ('b', int), ('d', int)],
    [(None, int), ('b', int), ('c', int), ('d', int)],
    [(None, int), (None, int), ('d', int)],
    [(None, int), (None, int), ('c', int), ('d', int)]
]

# def fn_3(a: int, b:int = 1, /, q:int = 1, *, r: int): ...
positional_only = [(None, 'int', False), (None, 'int', True)]
positional_or_keyword = [('q', 'int', True)]
keyword_only = [('r', 'int', False)]

generate_signatures(positional_only, positional_or_keyword, keyword_only)

# returns
[
    [(None, int), ('r', int)],
    [(None, int), ('q', int), ('r', int)],
    [(None, int), (None, int), ('r', int)],
    [(None, int), (None, int), ('q', int), ('r', int)]
    [(None, int), (None, int), (None, int), ('r', int)]
]

The way these outputs are structured is a tuple of argument name and type. If the argument name is None then the argument is a positional argument.

That is (None, int) is a positional argument (e.g. fn(1)) whereas ("x", int) is a keyword argument e.g. fn(x=1).

[(None, int), (None, int), ('c', int), ('d', int)] describes a function call of fn(1, 1, c=1, d=1)

Testing your outputs

A neat way to check is go to vscode.dev, install the python plugin, convert your list of lists to function invocations and paste it into a new document. The static analyser will tell you which are (in)valid. I think there is a way to run pyright in code, but this was quicker:

for i, sig in enumerate(sigs):
    print(f"# {i} - {sig}")
    s = 'fn('
    for arg in sig:
        if arg[0] is not None:
            s += f"{arg[0]}="
        typ = arg[1]
        val = None
        if typ == 'str': 
            val = "\"str\""
        elif typ == 'int':
            val = "1"
        elif typ == 'float':
            val = "1.0"
        else:
            val = "UNKNOWN"
        s += f"{val},"
    print(f"{s})\n")

Bonus challenge

Given fn_a(...) and fn_b(...) determine any ambiguous valid signatures to these methods.

e.g. fn_a(x: str, y: int) -> None: ... fn_b(z: str, /, x: int) -> None: ... are distinct for fn(x="foo", y=1) and fn(x="foo", z=1) but conflict for the signature fn("foo", 1) That is, both functions contain the valid signature of [(None, "str"), (None, "int")]

\$\endgroup\$
3
  • \$\begingroup\$ So the order of keyword arguments is not relevant for this challenge? \$\endgroup\$
    – Neil
    Commented May 25, 2023 at 14:02
  • \$\begingroup\$ @Neil yes, that is correct, since in python def fn(a: int, b: int, *, c: int):... the calls fn(c=1, b=1, a=1) and fn(a=1, b=1, c=1) are "identical" \$\endgroup\$
    – Mardoxx
    Commented May 25, 2023 at 14:04
  • 3
    \$\begingroup\$ Nice challenge! Did you intend for it to be code golf? If so, you'll want to add the code-golf tag and remove popularity-contest. \$\endgroup\$
    – DLosc
    Commented May 25, 2023 at 16:28

3 Answers 3

1
\$\begingroup\$

Python3, 302 bytes

from itertools import*
V=lambda v:not any(not v[i][0] and v[i-1][0]for i in range(len(v))if i)
def f(a,b,c):
 P=product(*[*[[(None,t)]+[[]]*k for n,t,k in a],*[[(None,t),(n,t)]+[[]]*k for n,t,k in b],*[[(n,t)]+[[]]*k for n,t,k in c]])
 return[*map(eval,{str(K)for i in P if V(K:=[j for j in i if j])})]

Try it online!

For those interested, here is a larger solution which parses the function signature:

import ast
from itertools import*
A=lambda x:x.annotation.id
H=lambda v:not any(not v[i][0] and v[i-1][0]for i in range(len(v))if i)
def f(s):
 a=ast.parse(s).body[0].args
 V=['kw_defaults','defaults']
 d=sum(len(getattr(a,i))for i in V)
 l=sum(len(i)for j in a._fields if(i:=getattr(a,j))and j not in V)
 K=0
 P=product(*[*[[(None,A(i))]for i in a.posonlyargs if(K:=K+1)],*[[(None,A(i)),(i.arg,A(i))]+[[]]*(((K:=K+1))>l-d)for i in a.args],*[[(i.arg,A(i)),[]]for i in a.kwonlyargs]])
 return[K for i in P if H(K:=[j for j in i if j])]

Try it online!

\$\endgroup\$
5
  • \$\begingroup\$ Fantastic! This is really good, how does it work? \$\endgroup\$
    – Mardoxx
    Commented May 24, 2023 at 17:33
  • \$\begingroup\$ @Mardoxx Thank you! In short, a list of lists is built up and passed to itertools.product; each sublist stores a potential valid method of argument passing (position, keyword, or default, depending on its type). Then, itertools.product produces all combinations (Cartesian product) of the list of lists. Then, each sublist in the result is checked to ensure that the positioning of position and keyword is valid. \$\endgroup\$
    – Ajax1234
    Commented May 24, 2023 at 19:35
  • \$\begingroup\$ I have updated test cases and this doesn't work for fn_3(a: int, b:int = 1, /, q:int = 1, *, r: int): ... I tried making a small edit to yours but resulted in duplicating one of the signatures, I need a bigger brain!! \$\endgroup\$
    – Mardoxx
    Commented May 25, 2023 at 13:58
  • \$\begingroup\$ @Mardoxx Thanks. FYI I think you have [(None, int), (None, int), ('r', int)] as a duplicate in your sample output for this final test case. Other than that, great challenge :) \$\endgroup\$
    – Ajax1234
    Commented May 25, 2023 at 14:23
  • \$\begingroup\$ indeed I do! My 7k byte answer is invalid also :D \$\endgroup\$
    – Mardoxx
    Commented May 25, 2023 at 14:24
0
\$\begingroup\$

Haskell, 208 bytes

import Data.List
y=elem '='
o(a:b)=[c++d|d<-o b,c<-[a]:[[]|y a]];o[]=[[]]
s s=inits s`zip`tails s
i(p,a,n)=nub[(map(\\"=")d,map((++"=").(\\"="))g)|(b,c)<-s$a,(d,e)<-s$p++b,all y e,f<-o$c++n,g<-permutations f]

Try it online!

The idea here is to split the semipositionals into positional or named. The default-having positionals can be omitted when trailing all mere positionals. The named can be omitted when default-having. The named can be freely permuted, while the positionals cannot.

\$\endgroup\$
3
  • \$\begingroup\$ I'm not sure this is correct assuming your outputs are a list/tuple of parameters which are to be flattened since... (["q"], ["b=", "z", "c=", "x"]) contains positional arguments after keyword arguments. (["q"], ["z", "x"]) and (["q"], ["x", "z"]) I'm not sure how these would work, since with positional arguments they always are assigned in the order the parameters are defined in the function's signature. \$\endgroup\$
    – Mardoxx
    Commented May 25, 2023 at 9:09
  • \$\begingroup\$ The way I laid out the output in the question is (None, int) is a positional argument e.g. fn(1) whereas ("x", int) is a keyword argument e.g. fn(x=1) \$\endgroup\$
    – Mardoxx
    Commented May 25, 2023 at 9:14
  • \$\begingroup\$ I added some equal sign cosmetics to the output for you to recognize. ["b=", "z", "c=", "x"] were all keyword named parameters in the second half of the resulting (positional,named) tuples. Copying the = from declaration input to invocation output where different = were needed was confusing. I erred less typing would be more popular. \$\endgroup\$ Commented May 25, 2023 at 18:29
0
\$\begingroup\$

Charcoal, 68 bytes

≔⁺θηυF…·LΦθ¬⊟ιLυ«≔⁻⁺ηζ…υιε≔Φε§κ²δEX²Lδ⭆¹⁺E…υι⟦ω§ξ¹⟧E⁺⁻εδΦδ﹪÷κX²π²…ξ²

Try it online! Link is to verbose version of code. Explanation:

≔⁺θηυ

Get all of the positional arguments.

≔⁺θηυF…·↨¹Eθ¬⊟ιLυ«

Count the number of mandatory positional-only parameters and loop from there up to all positional parameters.

≔⁻⁺ηζ…υιε

Get the remaining named arguments.

≔Φε§κ²δ

Get those that are optional.

EX²Lδ⭆¹⁺E…υι⟦ω§ξ¹⟧E⁺⁻εδΦδ﹪÷κX²π²…ξ²

For each possible combination of optional named parameters, concatenate the positional parameters and the mandatory named parameters with them.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.