# 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.
import builtins
import re
import token
from collections.abc import MutableMapping
from io import StringIO
from tokenize import generate_tokens, untokenize
from robot.errors import DataError
from robot.utils import get_error_message, type_name
from .notfound import variable_not_found
from .search import VariableMatches
[docs]
def evaluate_expression(
expression,
variables,
modules=None,
namespace=None,
resolve_variables=False,
):
original = expression
try:
if not isinstance(expression, str):
raise TypeError(f"Expression must be string, got {type_name(expression)}.")
if resolve_variables:
expression = variables.replace_scalar(expression)
if not isinstance(expression, str):
return expression
if not expression:
raise ValueError("Expression cannot be empty.")
return _evaluate(expression, variables.store, modules, namespace)
except DataError as err:
error = str(err)
variable_recommendation = ""
except Exception as err:
error = get_error_message()
variable_recommendation = ""
if isinstance(err, NameError) and "RF_VAR_" in error:
name = re.search(r"RF_VAR_([\w_]*)", error).group(1)
error = (
f"Robot Framework variable '${name}' is used in a scope "
f"where it cannot be seen."
)
else:
variable_recommendation = _recommend_special_variables(original)
raise DataError(
f"Evaluating expression {expression!r} failed: {error}\n\n"
f"{variable_recommendation}".strip()
)
def _evaluate(expression, variable_store, modules=None, namespace=None):
if "$" in expression:
expression = _decorate_variables(expression, variable_store)
# Given namespace must be included in our custom local namespace to make
# it possible to detect which names are not found and should be imported
# automatically as modules. It must be also be used as the global namespace
# with `eval()` because lambdas and possibly other special constructs don't
# see the local namespace at all.
namespace = dict(namespace or ())
if modules:
namespace.update(_import_modules(modules))
local_ns = EvaluationNamespace(variable_store, namespace)
return eval(expression, namespace, local_ns)
def _decorate_variables(expression, variable_store):
variable_started = False
variable_found = False
tokens = []
prev_toknum = None
for toknum, tokval, _, _, _ in generate_tokens(StringIO(expression).readline):
if variable_started:
if toknum == token.NAME:
if tokval not in variable_store:
variable_not_found(
f"${tokval}",
variable_store.as_dict(decoration=False),
deco_braces=False,
)
tokval = "RF_VAR_" + tokval
variable_found = True
else:
tokens.append((prev_toknum, "$"))
variable_started = False
if tokval == "$":
variable_started = True
prev_toknum = toknum
else:
tokens.append((toknum, tokval))
return untokenize(tokens).strip() if variable_found else expression
def _import_modules(module_names):
modules = {}
for name in module_names.replace(" ", "").split(","):
if not name:
continue
modules[name] = __import__(name)
# If we just import module 'root.sub', module 'root' is not found.
while "." in name:
name, _ = name.rsplit(".", 1)
modules[name] = __import__(name)
return modules
def _recommend_special_variables(expression):
matches = VariableMatches(expression)
if not matches:
return ""
example = []
for match in matches:
example[-1:] = [match.before, match.identifier + match.base, match.after]
example = "".join(_remove_possible_quoting(example))
return (
f"Variables in the original expression {expression!r} were resolved before "
f"the expression was evaluated. Try using {example!r} syntax to avoid that. "
f"See Evaluating Expressions appendix in Robot Framework User Guide for more "
f"details."
)
def _remove_possible_quoting(example_tokens):
before = var = after = None
for index, item in enumerate(example_tokens):
if index == 0:
before = item
elif index % 2 == 1:
var = item
else:
after = item
if before[-3:] in ('"""', "'''") and after[:3] == before[-3:]:
before, after = before[:-3], after[3:]
elif before[-1:] in ('"', "'") and after[:1] == before[-1:]:
before, after = before[:-1], after[1:]
yield before
yield var
before = after
yield after
[docs]
class EvaluationNamespace(MutableMapping):
def __init__(self, variable_store, namespace):
self.namespace = namespace
self.variables = variable_store
def __getitem__(self, key):
if key.startswith("RF_VAR_"):
return self.variables[key[7:]]
if key in self.namespace:
return self.namespace[key]
return self._import_module(key)
def __setitem__(self, key, value):
self.namespace[key] = value
def __delitem__(self, key):
self.namespace.pop(key)
def _import_module(self, name):
if hasattr(builtins, name):
raise KeyError
try:
return __import__(name)
except ImportError:
raise NameError(f"name '{name}' is not defined nor importable as module")
def __iter__(self):
yield from self.variables
yield from self.namespace
def __len__(self):
return len(self.variables) + len(self.namespace)