# 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 typing import TYPE_CHECKING
from robot.errors import DataError
from robot.utils import is_dict_like, split_from_equals
from robot.variables import is_dict_variable
from .argumentvalidator import ArgumentValidator
from ..model import Argument
if TYPE_CHECKING:
from .argumentspec import ArgumentSpec
[docs]
class ArgumentResolver:
def __init__(self, spec: 'ArgumentSpec',
resolve_named: bool = True,
resolve_args_until: 'int|None' = None,
dict_to_kwargs: bool = False):
self.named_resolver = NamedArgumentResolver(spec) \
if resolve_named else NullNamedArgumentResolver()
self.variable_replacer = VariableReplacer(spec, resolve_args_until)
self.dict_to_kwargs = DictToKwargs(spec, dict_to_kwargs)
self.argument_validator = ArgumentValidator(spec)
[docs]
def resolve(self, args, named_args=None, variables=None):
if named_args is None:
positional, named = self.named_resolver.resolve(args, variables)
else:
positional, named = args, list(named_args.items())
positional, named = self.variable_replacer.replace(positional, named, variables)
positional, named = self.dict_to_kwargs.handle(positional, named)
self.argument_validator.validate(positional, named, dryrun=variables is None)
return positional, named
[docs]
class NamedArgumentResolver:
def __init__(self, spec: 'ArgumentSpec'):
self.spec = spec
[docs]
def resolve(self, arguments, variables=None):
known_positional_count = max(len(self.spec.positional_only),
len(self.spec.embedded))
positional = list(arguments[:known_positional_count])
named = []
for arg in arguments[known_positional_count:]:
if is_dict_variable(arg):
named.append(arg)
else:
name, value = self._split_named(arg, named, variables)
if name is not None:
named.append((name, value))
elif named:
self._raise_positional_after_named()
else:
positional.append(value)
return positional, named
def _split_named(self, arg, previous_named, variables):
if isinstance(arg, Argument):
return arg.name, arg.value
name, value = split_from_equals(arg)
if value is None or not self._is_named(name, previous_named, variables):
return None, arg
return name, value
def _is_named(self, name, previous_named, variables):
if previous_named or self.spec.var_named:
return True
if variables:
try:
name = variables.replace_scalar(name)
except DataError:
return False
return name in self.spec.named
def _raise_positional_after_named(self):
raise DataError(f"{self.spec.type.capitalize()} '{self.spec.name}' "
f"got positional argument after named arguments.")
[docs]
class NullNamedArgumentResolver:
[docs]
def resolve(self, arguments, variables=None):
return arguments, []
[docs]
class DictToKwargs:
def __init__(self, spec: 'ArgumentSpec', enabled: bool = False):
self.maxargs = spec.maxargs
self.enabled = enabled and bool(spec.var_named)
[docs]
def handle(self, positional, named):
if self.enabled and self._extra_arg_has_kwargs(positional, named):
named = positional.pop().items()
return positional, named
def _extra_arg_has_kwargs(self, positional, named):
if named or len(positional) != self.maxargs + 1:
return False
return is_dict_like(positional[-1])
[docs]
class VariableReplacer:
def __init__(self, spec: 'ArgumentSpec', resolve_until: 'int|None' = None):
self.spec = spec
self.resolve_until = resolve_until
[docs]
def replace(self, positional, named, variables=None):
# `variables` is None in dry-run mode and when using Libdoc.
if variables:
if self.spec.embedded:
embedded = len(self.spec.embedded)
positional = [
variables.replace_scalar(emb) for emb in positional[:embedded]
] + variables.replace_list(positional[embedded:])
else:
positional = variables.replace_list(positional, self.resolve_until)
named = list(self._replace_named(named, variables.replace_scalar))
else:
# If `var` isn't a tuple, it's a &{dict} variables.
named = [var if isinstance(var, tuple) else (var, var) for var in named]
return positional, named
def _replace_named(self, named, replace_scalar):
for item in named:
for name, value in self._get_replaced_named(item, replace_scalar):
if not isinstance(name, str):
raise DataError('Argument names must be strings.')
yield name, value
def _get_replaced_named(self, item, replace_scalar):
if not isinstance(item, tuple):
return replace_scalar(item).items()
name, value = item
return [(replace_scalar(name), replace_scalar(value))]