From 72ef3c74414c4d3faddb68648e43067e4860c2ac Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Mon, 3 Apr 2023 14:14:15 +0300 Subject: [PATCH] Support for postional only args in dynamic library api (#4701) Fixes #4660. --- .../dynamic_positional_only_args.robot | 29 ++++++++ .../keywords/DynamicPositionalOnly.py | 22 +++++++ .../dynamic_positional_only_args.robot | 66 +++++++++++++++++++ .../CreatingTestLibraries.rst | 38 ++++++++++- src/robot/running/arguments/argumentparser.py | 17 +++++ 5 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 atest/robot/keywords/dynamic_positional_only_args.robot create mode 100644 atest/testdata/keywords/DynamicPositionalOnly.py create mode 100644 atest/testdata/keywords/dynamic_positional_only_args.robot diff --git a/atest/robot/keywords/dynamic_positional_only_args.robot b/atest/robot/keywords/dynamic_positional_only_args.robot new file mode 100644 index 00000000000..160180cdd59 --- /dev/null +++ b/atest/robot/keywords/dynamic_positional_only_args.robot @@ -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} diff --git a/atest/testdata/keywords/DynamicPositionalOnly.py b/atest/testdata/keywords/DynamicPositionalOnly.py new file mode 100644 index 00000000000..3b942203a54 --- /dev/null +++ b/atest/testdata/keywords/DynamicPositionalOnly.py @@ -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] diff --git a/atest/testdata/keywords/dynamic_positional_only_args.robot b/atest/testdata/keywords/dynamic_positional_only_args.robot new file mode 100644 index 00000000000..e4bdeae3f55 --- /dev/null +++ b/atest/testdata/keywords/dynamic_positional_only_args.robot @@ -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 diff --git a/doc/userguide/src/ExtendingRobotFramework/CreatingTestLibraries.rst b/doc/userguide/src/ExtendingRobotFramework/CreatingTestLibraries.rst index 22cd332b81f..eba46ac3ead 100644 --- a/doc/userguide/src/ExtendingRobotFramework/CreatingTestLibraries.rst +++ b/doc/userguide/src/ExtendingRobotFramework/CreatingTestLibraries.rst @@ -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']` | @@ -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 | | @@ -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`_ @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/robot/running/arguments/argumentparser.py b/src/robot/running/arguments/argumentparser.py index b4d9d49db0f..f736a797591 100644 --- a/src/robot/running/arguments/argumentparser.py +++ b/src/robot/running/arguments/argumentparser.py @@ -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): @@ -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 @@ -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): @@ -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