# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABC, abstractmethod
from inspect import isclass, Parameter, signature
from typing import Any, Callable, get_type_hints
from robot.errors import DataError
from robot.utils import NOT_SET, split_from_equals
from robot.variables import search_variable
from .argumentspec import ArgumentSpec
from .typeinfo import TypeInfo
[docs]
class ArgumentParser(ABC):
def __init__(
self,
type: str = "Keyword",
error_reporter: "Callable[[str], None]|None" = None,
):
self.type = type
self.error_reporter = error_reporter
[docs]
@abstractmethod
def parse(self, source: Any, name: "str|None" = None) -> ArgumentSpec:
raise NotImplementedError
def _report_error(self, error: str):
if self.error_reporter:
self.error_reporter(error)
else:
raise DataError(f"Invalid argument specification: {error}")
[docs]
class PythonArgumentParser(ArgumentParser):
[docs]
def parse(self, method, name=None):
try:
sig = signature(method)
except ValueError: # Can occur with C functions (incl. many builtins).
return ArgumentSpec(name, self.type, var_positional="args")
except TypeError as err: # Occurs if handler isn't actually callable.
raise DataError(str(err))
parameters = list(sig.parameters.values())
# `inspect.signature` drops `self` with bound methods and that's the case when
# inspecting keywords. `__init__` is got directly from class (i.e. isn't bound)
# so we need to handle that case ourselves.
# Partial objects do not have __name__ at least in Python =< 3.10.
if getattr(method, "__name__", None) == "__init__":
parameters = parameters[1:]
spec = self._create_spec(parameters, name)
self._set_types(spec, method)
return spec
def _create_spec(self, parameters, name):
positional_only = []
positional_or_named = []
var_positional = None
named_only = []
var_named = None
defaults = {}
for param in parameters:
kind = param.kind
if kind == Parameter.POSITIONAL_ONLY:
positional_only.append(param.name)
elif kind == Parameter.POSITIONAL_OR_KEYWORD:
positional_or_named.append(param.name)
elif kind == Parameter.VAR_POSITIONAL:
var_positional = param.name
elif kind == Parameter.KEYWORD_ONLY:
named_only.append(param.name)
elif kind == Parameter.VAR_KEYWORD:
var_named = param.name
if param.default is not param.empty:
defaults[param.name] = param.default
return ArgumentSpec(
name,
self.type,
positional_only,
positional_or_named,
var_positional,
named_only,
var_named,
defaults,
)
def _set_types(self, spec, method):
types = self._get_types(method)
if isinstance(types, dict) and "return" in types:
spec.return_type = types.pop("return")
spec.types = types
def _get_types(self, method):
# If types are set using the `@keyword` decorator, use them. Including when
# types are explicitly disabled with `@keyword(types=None)`. Otherwise get
# type hints.
if isclass(method):
method = method.__init__
types = getattr(method, "robot_types", ())
if types or types is None:
return types
try:
return get_type_hints(method)
except Exception: # Can raise pretty much anything
# Not all functions have `__annotations__`.
# https://github.com/robotframework/robotframework/issues/4059
return getattr(method, "__annotations__", {})
[docs]
class ArgumentSpecParser(ArgumentParser):
[docs]
def parse(self, arguments, name=None):
positional_only = []
positional_or_named = []
var_positional = None
named_only = []
var_named = None
defaults = {}
types = {}
named_only_separator_seen = positional_only_separator_seen = False
target = positional_or_named
for arg in arguments:
arg, default = self._validate_arg(arg)
if var_named:
self._report_error("Only last argument can be kwargs.")
elif self._is_positional_only_separator(arg):
if positional_only_separator_seen:
self._report_error("Too many positional-only separators.")
if named_only_separator_seen:
self._report_error(
"Positional-only separator must be before named-only arguments."
)
positional_only = positional_or_named
target = positional_or_named = []
positional_only_separator_seen = True
elif default is not NOT_SET:
self._parse_type(arg, types)
arg = self._format_arg(arg)
target.append(arg)
defaults[arg] = default
elif self._is_var_named(arg):
self._parse_type(arg, types)
var_named = self._format_var_named(arg)
elif self._is_var_positional(arg):
if named_only_separator_seen:
self._report_error("Cannot have multiple varargs.")
elif not self._is_named_only_separator(arg):
self._parse_type(arg, types)
var_positional = self._format_var_positional(arg)
named_only_separator_seen = True
target = named_only
else:
if defaults and not named_only_separator_seen:
self._report_error("Non-default argument after default arguments.")
self._parse_type(arg, types)
arg = self._format_arg(arg)
target.append(arg)
return ArgumentSpec(
name,
self.type,
positional_only,
positional_or_named,
var_positional,
named_only,
var_named,
defaults,
types=types,
)
@abstractmethod
def _validate_arg(self, arg):
raise NotImplementedError
@abstractmethod
def _is_var_positional(self, arg):
raise NotImplementedError
@abstractmethod
def _is_var_named(self, arg):
raise NotImplementedError
@abstractmethod
def _is_positional_only_separator(self, arg):
raise NotImplementedError
@abstractmethod
def _is_named_only_separator(self, arg):
raise NotImplementedError
@abstractmethod
def _format_arg(self, arg):
raise NotImplementedError
@abstractmethod
def _format_var_named(self, arg):
raise NotImplementedError
@abstractmethod
def _format_var_positional(self, arg):
raise NotImplementedError
@abstractmethod
def _parse_type(self, arg, types):
raise NotImplementedError
[docs]
class DynamicArgumentParser(ArgumentSpecParser):
def _validate_arg(self, arg):
if isinstance(arg, tuple):
if not self._is_valid_tuple(arg):
self._report_error(f'Invalid argument "{arg}".')
return None, NOT_SET
if len(arg) == 1:
return arg[0], NOT_SET
return arg[0], arg[1]
if "=" in arg:
return tuple(arg.split("=", 1))
return arg, NOT_SET
def _is_valid_tuple(self, arg):
return (
len(arg) in (1, 2)
and isinstance(arg[0], str)
and not (arg[0].startswith("*") and len(arg) == 2)
)
def _is_var_positional(self, arg):
return arg[:1] == "*"
def _is_var_named(self, arg):
return arg[:2] == "**"
def _is_positional_only_separator(self, arg):
return arg == "/"
def _is_named_only_separator(self, arg):
return arg == "*"
def _format_arg(self, arg):
return arg
def _format_var_positional(self, arg):
return arg[1:]
def _format_var_named(self, arg):
return arg[2:]
def _parse_type(self, arg, types):
pass
[docs]
class UserKeywordArgumentParser(ArgumentSpecParser):
def _validate_arg(self, arg):
arg, default = split_from_equals(arg)
match = search_variable(arg, parse_type=True, ignore_errors=True)
if not (match.is_assign() or self._is_named_only_separator(match)):
self._report_error(f"Invalid argument syntax '{arg}'.")
match = search_variable("")
default = NOT_SET
elif default is None:
default = NOT_SET
elif arg[0] != "$":
kind = "list" if arg[0] == "@" else "dictionary"
self._report_error(
f"Only normal arguments accept default values, "
f"{kind} arguments like '{arg}' do not."
)
default = NOT_SET
return match, default
def _is_var_positional(self, match):
return match.identifier == "@"
def _is_var_named(self, match):
return match.identifier == "&"
def _is_positional_only_separator(self, arg):
return False
def _is_named_only_separator(self, match):
return match.identifier == "@" and not match.base
def _format_arg(self, match):
return match.base
def _format_var_named(self, match):
return match.base
def _format_var_positional(self, match):
return match.base
def _parse_type(self, match, types):
try:
info = TypeInfo.from_variable(match, handle_list_and_dict=False)
except DataError as err:
self._report_error(f"Invalid argument '{match}': {err}")
else:
if info:
types[match.base] = info