Source code for robot.running.arguments.argumentparser

#  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