Source code for robot.variables.search

#  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, py2to3, rstrip


[docs]def search_variable(string, identifiers='$@&%*', ignore_errors=False): if not (is_string(string) and '{' in string): return VariableMatch(string) return VariableSearcher(identifiers, ignore_errors).search(string)
[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, '$')
# See comment to `VariableMatch.is_list/dict_variable` for explanation why # `is_list/dict_variable` need different implementation than # `is_scalar_variable` above. This ought to be changed in RF 4.0.
[docs]def is_list_variable(string): match = search_variable(string, '@', ignore_errors=True) return match.is_list_variable()
[docs]def is_dict_variable(string): match = search_variable(string, '&', ignore_errors=True) return match.is_dict_variable()
[docs]def is_assign(string, identifiers='$@&', allow_assign_mark=False): match = search_variable(string, identifiers, ignore_errors=True) return match.is_assign(allow_assign_mark)
[docs]def is_scalar_assign(string, allow_assign_mark=False): return is_assign(string, '$', allow_assign_mark)
[docs]def is_list_assign(string, allow_assign_mark=False): return is_assign(string, '@', allow_assign_mark)
[docs]def is_dict_assign(string, allow_assign_mark=False): return is_assign(string, '&', allow_assign_mark)
[docs]@py2to3 class VariableMatch(object): 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()
# The reason `is_list/dict_variable` check they don't have items is that # at the moment e.g. `@{var}[item]` still returns a scalar value. # This will change in RF 4.0 and then obviously this code must be changed: # https://github.com/robotframework/robotframework/issues/3487
[docs] def is_list_variable(self): return (self.identifier == '@' and self.is_variable() and not self.items)
[docs] def is_dict_variable(self): return (self.identifier == '&' and self.is_variable() and not self.items)
[docs] def is_assign(self, allow_assign_mark=False): if allow_assign_mark and self.string.endswith('='): return search_variable(rstrip(self.string[:-1])).is_assign() return (self.is_variable() and self.identifier in '$@&' and not self.items and not search_variable(self.base))
[docs] def is_scalar_assign(self, allow_assign_mark=False): return self.identifier == '$' and self.is_assign(allow_assign_mark)
[docs] def is_list_assign(self, allow_assign_mark=False): return self.identifier == '@' and self.is_assign(allow_assign_mark)
[docs] def is_dict_assign(self, allow_assign_mark=False): return self.identifier == '&' and self.is_assign(allow_assign_mark)
def __nonzero__(self): return self.identifier is not None def __unicode__(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)
[docs]class VariableSearcher(object): def __init__(self, identifiers, ignore_errors=False): self.identifiers = identifiers self._ignore_errors = ignore_errors self.start = -1 self.variable_chars = [] self.item_chars = [] self.items = [] self._open_brackets = 0 # Used both with curly and square brackets self._escaped = False
[docs] def search(self, string): if not self._search(string): return VariableMatch(string) match = VariableMatch(string=string, identifier=self.variable_chars[0], base=''.join(self.variable_chars[2:-1]), start=self.start, end=self.start + len(self.variable_chars)) if self.items: match.items = tuple(self.items) match.end += sum(len(i) for i in self.items) + 2 * len(self.items) return match
def _search(self, string, offset=0): start = self._find_variable_start(string) if start == -1: return False self.start = start + offset self._open_brackets += 1 self.variable_chars = [string[start], '{'] start += 2 state = self.variable_state for char in string[start:]: state = state(char) self._escaped = False if char != '\\' else not self._escaped if state is None: break if state: try: self._validate_end_state(state) except VariableError: if self._ignore_errors: return False raise return True def _find_variable_start(self, string): start = 1 while True: start = string.find('{', start) - 1 if start < 0: return -1 if self._start_index_is_ok(string, start): return start start += 2 def _start_index_is_ok(self, string, index): return (string[index] in self.identifiers and not self._is_escaped(string, index)) def _is_escaped(self, string, index): escaped = False while index > 0 and string[index-1] == '\\': index -= 1 escaped = not escaped return escaped
[docs] def variable_state(self, char): self.variable_chars.append(char) if char == '}' and not self._escaped: self._open_brackets -= 1 if self._open_brackets == 0: if not self._can_have_items(): return None return self.waiting_item_state elif char == '{' and not self._escaped: self._open_brackets += 1 return self.variable_state
def _can_have_items(self): return self.variable_chars[0] in '$@&'
[docs] def waiting_item_state(self, char): if char == '[': self._open_brackets += 1 return self.item_state return None
[docs] def item_state(self, char): if char == ']' and not self._escaped: self._open_brackets -= 1 if self._open_brackets == 0: self.items.append(''.join(self.item_chars)) self.item_chars = [] # Don't support chained item access with old @ and & syntax. # The old syntax was deprecated in RF 3.2 and in RF 3.3 it'll # be reassigned to mean using item in list/dict context. if self.variable_chars[0] in '@&': return None return self.waiting_item_state elif char == '[' and not self._escaped: self._open_brackets += 1 self.item_chars.append(char) return self.item_state
def _validate_end_state(self, state): if state == self.variable_state: incomplete = ''.join(self.variable_chars) raise VariableError("Variable '%s' was not closed properly." % incomplete) if state == self.item_state: variable = ''.join(self.variable_chars) items = ''.join('[%s]' % i for i in self.items) incomplete = ''.join(self.item_chars) raise VariableError("Variable item '%s%s[%s' was not closed " "properly." % (variable, items, incomplete))
[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]@py2to3 class VariableIterator(object): 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 __nonzero__(self): try: next(iter(self)) except StopIteration: return False else: return True