# 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 re
from robot.errors import VariableError
from robot.utils import is_string
[docs]def search_variable(string, identifiers='$@&%*', ignore_errors=False):
if not (is_string(string) and '{' in string):
return VariableMatch(string)
return _search_variable(string, identifiers, ignore_errors)
[docs]def contains_variable(string, identifiers='$@&'):
match = search_variable(string, identifiers, ignore_errors=True)
return bool(match)
[docs]def is_variable(string, identifiers='$@&'):
match = search_variable(string, identifiers, ignore_errors=True)
return match.is_variable()
[docs]def is_scalar_variable(string):
return is_variable(string, '$')
[docs]def is_list_variable(string):
return is_variable(string, '@')
[docs]def is_dict_variable(string):
return is_variable(string, '&')
[docs]def is_assign(string, identifiers='$@&', allow_assign_mark=False, allow_items=False):
match = search_variable(string, identifiers, ignore_errors=True)
return match.is_assign(allow_assign_mark, allow_items=allow_items)
[docs]def is_scalar_assign(string, allow_assign_mark=False, allow_items=False):
return is_assign(string, '$', allow_assign_mark, allow_items)
[docs]def is_list_assign(string, allow_assign_mark=False, allow_items=False):
return is_assign(string, '@', allow_assign_mark, allow_items)
[docs]def is_dict_assign(string, allow_assign_mark=False, allow_items=False):
return is_assign(string, '&', allow_assign_mark, allow_items)
[docs]class VariableMatch:
def __init__(self, string, identifier=None, base=None, items=(), start=-1, end=-1):
self.string = string
self.identifier = identifier
self.base = base
self.items = items
self.start = start
self.end = end
[docs] def resolve_base(self, variables, ignore_errors=False):
if self.identifier:
internal = search_variable(self.base)
self.base = variables.replace_string(
internal,
custom_unescaper=unescape_variable_syntax,
ignore_errors=ignore_errors,
)
@property
def name(self):
return '%s{%s}' % (self.identifier, self.base) if self else None
@property
def before(self):
return self.string[:self.start] if self.identifier else self.string
@property
def match(self):
return self.string[self.start:self.end] if self.identifier else None
@property
def after(self):
return self.string[self.end:] if self.identifier else None
[docs] def is_variable(self):
return bool(self.identifier
and self.base
and self.start == 0
and self.end == len(self.string))
[docs] def is_scalar_variable(self):
return self.identifier == '$' and self.is_variable()
[docs] def is_list_variable(self):
return self.identifier == '@' and self.is_variable()
[docs] def is_dict_variable(self):
return self.identifier == '&' and self.is_variable()
[docs] def is_assign(self,
allow_assign_mark=False, allow_nested=False, allow_items=False):
if allow_assign_mark and self.string.endswith('='):
match = search_variable(self.string[:-1].rstrip(), ignore_errors=True)
return match.is_assign(allow_items=allow_items)
return (self.is_variable()
and self.identifier in '$@&'
and (allow_items or not self.items)
and (allow_nested or not search_variable(self.base)))
[docs] def is_scalar_assign(self, allow_assign_mark=False, allow_nested=False):
return self.identifier == '$' and self.is_assign(allow_assign_mark, allow_nested)
[docs] def is_list_assign(self, allow_assign_mark=False, allow_nested=False):
return self.identifier == '@' and self.is_assign(allow_assign_mark, allow_nested)
[docs] def is_dict_assign(self, allow_assign_mark=False, allow_nested=False):
return self.identifier == '&' and self.is_assign(allow_assign_mark, allow_nested)
def __bool__(self):
return self.identifier is not None
def __str__(self):
if not self:
return '<no match>'
items = ''.join('[%s]' % i for i in self.items) if self.items else ''
return '%s{%s}%s' % (self.identifier, self.base, items)
def _search_variable(string, identifiers, ignore_errors=False):
start = _find_variable_start(string, identifiers)
if start < 0:
return VariableMatch(string)
match = VariableMatch(string, identifier=string[start], start=start)
left_brace, right_brace = '{', '}'
open_braces = 1
escaped = False
items = []
indices_and_chars = enumerate(string[start+2:], start=start+2)
for index, char in indices_and_chars:
if char == left_brace and not escaped:
open_braces += 1
elif char == right_brace and not escaped:
open_braces -= 1
if open_braces == 0:
next_char = string[index+1] if index+1 < len(string) else None
if left_brace == '{': # Parsing name.
match.base = string[start+2:index]
if match.identifier not in '$@&' or next_char != '[':
match.end = index + 1
break
left_brace, right_brace = '[', ']'
else: # Parsing items.
items.append(string[start+1:index])
if next_char != '[':
match.end = index + 1
match.items = tuple(items)
break
next(indices_and_chars) # Consume '['.
start = index + 1 # Start of the next item.
open_braces = 1
else:
escaped = False if char != '\\' else not escaped
if open_braces:
if ignore_errors:
return VariableMatch(string)
incomplete = string[match.start:]
if left_brace == '{':
raise VariableError(f"Variable '{incomplete}' was not closed properly.")
raise VariableError(f"Variable item '{incomplete}' was not closed properly.")
return match if match else VariableMatch(match)
def _find_variable_start(string, identifiers):
index = 1
while True:
index = string.find('{', index) - 1
if index < 0:
return -1
if string[index] in identifiers and _not_escaped(string, index):
return index
index += 2
def _not_escaped(string, index):
escaped = False
while index > 0 and string[index-1] == '\\':
index -= 1
escaped = not escaped
return not escaped
[docs]def unescape_variable_syntax(item):
def handle_escapes(match):
escapes, text = match.groups()
if len(escapes) % 2 == 1 and starts_with_variable_or_curly(text):
return escapes[1:]
return escapes
def starts_with_variable_or_curly(text):
if text[0] in '{}':
return True
match = search_variable(text, ignore_errors=True)
return match and match.start == 0
return re.sub(r'(\\+)(?=(.+))', handle_escapes, item)
[docs]class VariableIterator:
def __init__(self, string, identifiers='$@&%', ignore_errors=False):
self.string = string
self.identifiers = identifiers
self.ignore_errors = ignore_errors
def __iter__(self):
remaining = self.string
while True:
match = search_variable(remaining, self.identifiers, self.ignore_errors)
if not match:
break
remaining = match.after
yield match.before, match.match, remaining
def __len__(self):
return sum(1 for _ in self)
def __bool__(self):
try:
next(iter(self))
except StopIteration:
return False
else:
return True