# 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 functools import partial
from typing import Iterator, Sequence
from robot.errors import VariableError
[docs]
def search_variable(
string: str,
identifiers: Sequence[str] = "$@&%*",
parse_type: bool = False,
ignore_errors: bool = False,
) -> "VariableMatch":
if not (isinstance(string, str) and "{" in string):
return VariableMatch(string)
return _search_variable(string, identifiers, parse_type, ignore_errors)
[docs]
def contains_variable(string: str, identifiers: Sequence[str] = "$@&") -> bool:
match = search_variable(string, identifiers, ignore_errors=True)
return bool(match)
[docs]
def is_variable(string: str, identifiers: Sequence[str] = "$@&") -> bool:
match = search_variable(string, identifiers, ignore_errors=True)
return match.is_variable()
[docs]
def is_scalar_variable(string: str) -> bool:
return is_variable(string, "$")
[docs]
def is_list_variable(string: str) -> bool:
return is_variable(string, "@")
[docs]
def is_dict_variable(string: str) -> bool:
return is_variable(string, "&")
[docs]
def is_assign(
string: str,
identifiers: Sequence[str] = "$@&",
allow_assign_mark: bool = False,
allow_nested: bool = False,
allow_items: bool = False,
) -> bool:
match = search_variable(string, identifiers, ignore_errors=True)
return match.is_assign(allow_assign_mark, allow_nested, allow_items)
[docs]
def is_scalar_assign(
string: str,
allow_assign_mark: bool = False,
allow_nested: bool = False,
allow_items: bool = False,
) -> bool:
return is_assign(string, "$", allow_assign_mark, allow_nested, allow_items)
[docs]
def is_list_assign(
string: str,
allow_assign_mark: bool = False,
allow_nested: bool = False,
allow_items: bool = False,
) -> bool:
return is_assign(string, "@", allow_assign_mark, allow_nested, allow_items)
[docs]
def is_dict_assign(
string: str,
allow_assign_mark: bool = False,
allow_nested: bool = False,
allow_items: bool = False,
) -> bool:
return is_assign(string, "&", allow_assign_mark, allow_nested, allow_items)
[docs]
class VariableMatch:
def __init__(
self,
string: str,
identifier: "str|None" = None,
base: "str|None" = None,
type: "str|None" = None,
items: "tuple[str, ...]" = (),
start: int = -1,
end: int = -1,
):
self.string = string
self.identifier = identifier
self.base = base
self.type = type
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) -> "str|None":
return f"{self.identifier}{{{self.base}}}" if self.identifier else None
@property
def before(self) -> str:
return self.string[: self.start] if self.identifier else self.string
@property
def match(self) -> "str|None":
return self.string[self.start : self.end] if self.identifier else None
@property
def after(self) -> str:
return self.string[self.end :] if self.identifier else ""
[docs]
def is_variable(self) -> bool:
return bool(
self.identifier
and self.base
and self.start == 0
and self.end == len(self.string)
)
[docs]
def is_scalar_variable(self) -> bool:
return self.identifier == "$" and self.is_variable()
[docs]
def is_list_variable(self) -> bool:
return self.identifier == "@" and self.is_variable()
[docs]
def is_dict_variable(self) -> bool:
return self.identifier == "&" and self.is_variable()
[docs]
def is_assign(
self,
allow_assign_mark: bool = False,
allow_nested: bool = False,
allow_items: bool = False,
) -> bool:
if allow_assign_mark and self.string.endswith("="):
match = search_variable(self.string[:-1].rstrip(), ignore_errors=True)
return match.is_assign(allow_nested=allow_nested, 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: bool = False,
allow_nested: bool = False,
) -> bool:
return self.identifier == "$" and self.is_assign(
allow_assign_mark, allow_nested
)
[docs]
def is_list_assign(
self,
allow_assign_mark: bool = False,
allow_nested: bool = False,
) -> bool:
return self.identifier == "@" and self.is_assign(
allow_assign_mark, allow_nested
)
[docs]
def is_dict_assign(
self,
allow_assign_mark: bool = False,
allow_nested: bool = False,
) -> bool:
return self.identifier == "&" and self.is_assign(
allow_assign_mark, allow_nested
)
def __bool__(self) -> bool:
return self.identifier is not None
def __str__(self) -> str:
if not self:
return "<no match>"
type = f": {self.type}" if self.type else ""
items = "".join([f"[{i}]" for i in self.items]) if self.items else ""
return f"{self.identifier}{{{self.base}{type}}}{items}"
def _search_variable(
string: str,
identifiers: Sequence[str],
parse_type: bool = False,
ignore_errors: bool = False,
) -> VariableMatch:
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 == right_brace and not escaped:
open_braces -= 1
if open_braces == 0:
_, next_char = next(indices_and_chars, (-1, None))
# Parsing name.
if left_brace == "{":
match.base = string[start + 2 : index]
if next_char != "[" or match.identifier not in "$@&":
match.end = index + 1
break
left_brace, right_brace = "[", "]"
# Parsing items.
else:
items.append(string[start + 1 : index])
if next_char != "[":
match.end = index + 1
match.items = tuple(items)
break
start = index + 1 # Start of the next item.
open_braces = 1
elif char == left_brace and not escaped:
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.")
if parse_type and ": " in match.base:
match.base, match.type = match.base.rsplit(": ", 1)
return 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 VariableMatches:
def __init__(
self,
string: str,
identifiers: Sequence[str] = "$@&%",
parse_type: bool = False,
ignore_errors: bool = False,
):
self.string = string
self.search_variable = partial(
search_variable,
identifiers=identifiers,
parse_type=parse_type,
ignore_errors=ignore_errors,
)
def __iter__(self) -> Iterator[VariableMatch]:
remaining = self.string
while True:
match = self.search_variable(remaining)
if not match:
break
remaining = match.after
yield match
def __len__(self) -> int:
return sum(1 for _ in self)
def __bool__(self) -> bool:
return bool(self.search_variable(self.string))