Skip to content

Commit

Permalink
Support for postional only args in dynamic library api (#4701)
Browse files Browse the repository at this point in the history
Fixes #4660.
  • Loading branch information
aaltat committed Apr 3, 2023
1 parent fa20cd4 commit 72ef3c7
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 2 deletions.
29 changes: 29 additions & 0 deletions atest/robot/keywords/dynamic_positional_only_args.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
*** Settings ***
Suite Setup Run Tests ${EMPTY} keywords/dynamic_positional_only_args.robot
Force Tags require-py3.8
Resource atest_resource.robot

*** Test Cases ***
One Argument
Check Test Case ${TESTNAME}

Three arguments
Check Test Case ${TESTNAME}

Pos and named
Check Test Case ${TESTNAME}

Pos and names too few arguments
Check Test Case ${TESTNAME}

Three arguments too many arguments
Check Test Case ${TESTNAME}

Pos with default
Check Test Case ${TESTNAME}

All args
Check Test Case ${TESTNAME}

Arg with too may / separators
Check Test Case ${TESTNAME}
22 changes: 22 additions & 0 deletions atest/testdata/keywords/DynamicPositionalOnly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class DynamicPositionalOnly:
kws = {
"one argument": ["one", "/"],
"three arguments": ["a", "b", "c", "/"],
"with normal": ["posonly", "/", "normal"],
"default str": ["required", "optional=default", "/"],
"default tuple": ["required", ("optional", "default"), "/"],
"all args kw": [("one", "value"), "/", ("named", "other"), "*varargs", "**kwargs"],
"arg with separator": ["/one"],
"Arg with too many / separators": ["one", "/", "two", "/"]
}

def get_keyword_names(self):
return [key for key in self.kws]

def run_keyword(self, name, args, kwargs=None):
if kwargs:
return f"{name}-{args}-{kwargs}"
return f"{name}-{args}"

def get_keyword_arguments(self, name):
return self.kws[name]
66 changes: 66 additions & 0 deletions atest/testdata/keywords/dynamic_positional_only_args.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
*** Settings ***
Library DynamicPositionalOnly.py
Force Tags require-py3.8

*** Test Cases ***
One Argument
${result} = One Argument value
Should be equal ${result} one argument-('value',)
${result} = One Argument one=value
Should be equal ${result} one argument-('one=value',)
${result} = One Argument foo=value
Should be equal ${result} one argument-('foo=value',)

Three arguments
${result} = Three Arguments a b c
Should be equal ${result} three arguments-('a', 'b', 'c')
${result} = Three Arguments x=a y=b z=c
Should be equal ${result} three arguments-('x=a', 'y=b', 'z=c')
${result} = Three Arguments a=a b=b c=c
Should be equal ${result} three arguments-('a=a', 'b=b', 'c=c')

Pos and named
${result} = with normal a b
Should be equal ${result} with normal-('a', 'b')
${result} = with normal posonly=posonly normal=111
Should be equal ${result} with normal-('posonly=posonly',)-{'normal': '111'}
${result} = with normal aaa normal=111
Should be equal ${result} with normal-('aaa',)-{'normal': '111'}

Pos and names too few arguments
[Documentation] FAIL Keyword 'DynamicPositionalOnly.With Normal' expected 2 arguments, got 1.
with normal normal=aaa

Three arguments too many arguments
[Documentation] FAIL Keyword 'DynamicPositionalOnly.Three Arguments' expected 3 arguments, got 4.
Three Arguments a b c /

Pos with default
${result} = default str a
Should be equal ${result} default str-('a',)
${result} = default str a optional=b
Should be equal ${result} default str-('a', 'optional=b')
${result} = default str optional=b
Should be equal ${result} default str-('optional=b',)
${result} = default tuple a
Should be equal ${result} default tuple-('a',)
${result} = default tuple a optional=b
Should be equal ${result} default tuple-('a', 'optional=b')
${result} = default tuple optional=b
Should be equal ${result} default tuple-('optional=b',)
${result} = default tuple optional=b optional=c
Should be equal ${result} default tuple-('optional=b', 'optional=c')
Arg with separator /one=
Should be equal ${result} default tuple-('optional=b', 'optional=c')

All args
${result} = all args kw other value 1 2 kw1=1 kw2=2
Should be equal ${result} all args kw-('other', 'value', '1', '2')-{'kw1': '1', 'kw2': '2'}
${result} = all args kw other
Should be equal ${result} all args kw-('other',)
${result} = all args kw
Should be equal ${result} all args kw-()

Arg with too may / separators
[Documentation] FAIL No keyword with name 'Arg with too many / separators' found.
Arg with too many / separators one two
Original file line number Diff line number Diff line change
Expand Up @@ -2901,6 +2901,11 @@ they are specified in Python and explained in the following table.
| | separate items. New in | |
| | Robot Framework 3.2. | |
+--------------------+----------------------------+----------------------------+
| `Positional-only | Arguments before `/` | `['pos', '/']`, |
| arguments`_ | marker are considered as | `['pos', '/', 'named']` |
| | positional only. New in | |
| | Robot Framework 6.1. | |
+--------------------+----------------------------+----------------------------+
| `Variable number | Argument after possible | `['*varargs']`, |
| of arguments`_ | positional arguments and | `['argument', '*rest']`, |
| (varargs) | their defaults has `*` | `['a', 'b=42', '*c']` |
Expand All @@ -2912,7 +2917,7 @@ they are specified in Python and explained in the following table.
| | free named arguments`__. | |
+--------------------+----------------------------+----------------------------+
| `Named-only | Arguments after varargs or | `['*varargs', 'named']`, |
| arguments`_ | a lone `*` if there are no | `['*', 'named'], |
| arguments`_ | a lone `*` if there are no | `['*', 'named']`, |
| | varargs. With or without | `['*', 'x', 'y=default']`, |
| | defaults. Requires | `['a', '*b', 'c', '**d']` |
| | `run_keyword` to `support | |
Expand Down Expand Up @@ -2948,7 +2953,8 @@ accepting all arguments. This automatic argument spec is either
`run_keyword` `support free named arguments`__ or not.

.. note:: Support to specify arguments as tuples like `('name', 'default')`
is new in Robot Framework 3.2.
is new in Robot Framework 3.2. Support for positional only arguments
in dynamic library API is new in Robot Framework 6.1.

__ `Free named arguments with dynamic libraries`_
__ `Named-only arguments with dynamic libraries`_
Expand Down Expand Up @@ -3064,6 +3070,34 @@ source path defined.
.. note:: Returning source information for keywords is a new feature in
Robot Framework 3.2.

Positional only argument syntax with dynamic libraries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The dynamic library API supports the
`positional-only arguments`_. Python 3.8 introduced positional-only arguments
that make it possible to specify that an argument can only be given as a
positional argument, not as a named argument like name=value. Positional-only
arguments are specified before normal arguments and a special / marker must
be used after them:

.. sourcecode:: python

def keyword(posonly, /, normal=None):
print(f"Got positional-only argument {posonly} and normal argument {normal}.")

The above keyword could be used like this:

.. sourcecode:: robotframework

*** Test Cases ***
Positional-only argument #args
Keyword x # posonly gets value "x" and normal uses default value.
Keyword normal=x # posonly gets value "normal=x" and normal uses default value.
Keyword normal=x y # posonly gets value "normal=x" and normal gets value "y.


.. note:: Positional-only argument support in dynamic libary API is a new
feature in Robot Framework 6.1.

Named argument syntax with dynamic libraries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
17 changes: 17 additions & 0 deletions src/robot/running/arguments/argumentparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,16 @@ class ArgumentSpecParser(ArgumentParser):

def parse(self, argspec, name=None):
spec = ArgumentSpec(name, self._type)
positional_only_separator_seen = False
named_only = False
for arg in argspec:
arg = self._validate_arg(arg)
if self._is_positional_only_separator(arg):
if positional_only_separator_seen:
self._report_error('Too many positional only separators.')
positional_only_separator_seen = True
spec.positional_only, spec.positional_or_named = spec.positional_or_named, []
continue
if spec.var_named:
self._report_error('Only last argument can be kwargs.')
elif isinstance(arg, tuple):
Expand Down Expand Up @@ -146,6 +153,10 @@ def _is_var_positional(self, arg):
def _format_var_positional(self, varargs):
raise NotImplementedError

@abstractmethod
def _is_positional_only_separator(self, arg):
raise NotImplementedError

def _format_arg(self, arg):
return arg

Expand Down Expand Up @@ -189,6 +200,9 @@ def _is_named_only_separator(self, arg):
def _format_var_positional(self, varargs):
return varargs[1:]

def _is_positional_only_separator(self, arg):
return arg == "/"


class UserKeywordArgumentParser(ArgumentSpecParser):

Expand Down Expand Up @@ -221,3 +235,6 @@ def _format_var_positional(self, varargs):

def _format_arg(self, arg):
return arg[2:-1]

def _is_positional_only_separator(self, arg):
return False

0 comments on commit 72ef3c7

Please sign in to comment.