Source code for robot.parsing.model.statements
# 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 ast
import re
import warnings
from abc import ABC, abstractmethod
from collections.abc import Iterator, Sequence
from typing import ClassVar, Literal, overload, Type, TYPE_CHECKING, TypeVar
from robot.conf import Language
from robot.errors import DataError
from robot.running import TypeInfo
from robot.running.arguments import UserKeywordArgumentParser
from robot.utils import normalize_whitespace, seq2str, split_from_equals, test_or_task
from robot.variables import (
contains_variable, is_dict_variable, is_scalar_assign, search_variable,
VariableAssignment
)
from ..lexer import Token
if TYPE_CHECKING:
from .blocks import ValidationContext
T = TypeVar("T", bound="Statement")
FOUR_SPACES = " "
EOL = "\n"
[docs]
class Node(ast.AST, ABC):
_attributes = ("lineno", "col_offset", "end_lineno", "end_col_offset", "errors")
lineno: int
col_offset: int
end_lineno: int
end_col_offset: int
errors: "tuple[str, ...]" = ()
[docs]
class Statement(Node, ABC):
_attributes = ("type", "tokens", *Node._attributes)
type: str
handles_types: "ClassVar[tuple[str, ...]]" = ()
statement_handlers: "ClassVar[dict[str, Type[Statement]]]" = {}
# Accepted configuration options. If the value is a tuple, it lists accepted
# values. If the used value contains a variable, it cannot be validated.
options: "dict[str, tuple|None]" = {}
def __init__(self, tokens: "Sequence[Token]", errors: "Sequence[str]" = ()):
self.tokens = tuple(tokens)
self.errors = tuple(errors)
@property
def lineno(self) -> int:
return self.tokens[0].lineno if self.tokens else -1
@property
def col_offset(self) -> int:
return self.tokens[0].col_offset if self.tokens else -1
@property
def end_lineno(self) -> int:
return self.tokens[-1].lineno if self.tokens else -1
@property
def end_col_offset(self) -> int:
return self.tokens[-1].end_col_offset if self.tokens else -1
[docs]
@classmethod
def register(cls, subcls: Type[T]) -> Type[T]:
types = subcls.handles_types or (subcls.type,)
for typ in types:
cls.statement_handlers[typ] = subcls
return subcls
[docs]
@classmethod
def from_tokens(cls, tokens: "Sequence[Token]") -> "Statement":
"""Create a statement from given tokens.
Statement type is got automatically from token types.
This classmethod should be called from :class:`Statement`, not from
its subclasses. If you know the subclass to use, simply create an
instance of it directly.
"""
handlers = cls.statement_handlers
for token in tokens:
if token.type in handlers:
return handlers[token.type](tokens)
if any(token.type == Token.ASSIGN for token in tokens):
return KeywordCall(tokens)
return EmptyLine(tokens)
[docs]
@classmethod
@abstractmethod
def from_params(cls, *args, **kwargs) -> "Statement":
"""Create a statement from passed parameters.
Required and optional arguments in general match class properties.
Values are used to create matching tokens.
Most implementations support following general properties:
- ``separator`` whitespace inserted between each token. Default is four spaces.
- ``indent`` whitespace inserted before first token. Default is four spaces.
- ``eol`` end of line sign. Default is ``'\\n'``.
This classmethod should be called from the :class:`Statement` subclass
to create, not from the :class:`Statement` class itself.
"""
raise NotImplementedError
@property
def data_tokens(self) -> "list[Token]":
return [t for t in self.tokens if t.type not in Token.NON_DATA_TOKENS]
[docs]
def get_token(self, *types: str) -> "Token|None":
"""Return a token with any of the given ``types``.
If there are no matches, return ``None``. If there are multiple
matches, return the first match.
"""
for token in self.tokens:
if token.type in types:
return token
return None
[docs]
def get_tokens(self, *types: str) -> "list[Token]":
"""Return tokens having any of the given ``types``."""
return [t for t in self.tokens if t.type in types]
@overload
def get_value(self, type: str, default: str) -> str: ...
@overload
def get_value(self, type: str, default: None = None) -> "str|None": ...
[docs]
def get_value(self, type: str, default: "str|None" = None) -> "str|None":
"""Return value of a token with the given ``type``.
If there are no matches, return ``default``. If there are multiple
matches, return the value of the first match.
"""
token = self.get_token(type)
return token.value if token else default
[docs]
def get_values(self, *types: str) -> "tuple[str, ...]":
"""Return values of tokens having any of the given ``types``."""
return tuple(t.value for t in self.tokens if t.type in types)
[docs]
def get_option(self, name: str, default: "str|None" = None) -> "str|None":
"""Return value of a configuration option with the given ``name``.
If the option has not been used, return ``default``.
If the option has been used multiple times, values are joined together.
This is typically an error situation and validated elsewhere.
New in Robot Framework 6.1.
"""
return self._get_options().get(name, default)
def _get_options(self) -> "dict[str, str]":
return dict(opt.split("=", 1) for opt in self.get_values(Token.OPTION))
@property
def lines(self) -> "Iterator[list[Token]]":
line = []
for token in self.tokens:
line.append(token)
if token.type == Token.EOL:
yield line
line = []
if line:
yield line
def _validate_options(self):
for name, value in self._get_options().items():
if self.options[name] is not None:
expected = self.options[name]
if value.upper() not in expected and not contains_variable(value):
self.errors += (
f"{self.type} option '{name}' does not accept value '{value}'. "
f"Valid values are {seq2str(expected)}.",
)
def __iter__(self) -> "Iterator[Token]":
return iter(self.tokens)
def __len__(self) -> int:
return len(self.tokens)
def __getitem__(self, item) -> Token:
return self.tokens[item]
def __repr__(self) -> str:
name = type(self).__name__
tokens = f"tokens={list(self.tokens)}"
errors = f", errors={list(self.errors)}" if self.errors else ""
return f"{name}({tokens}{errors})"
[docs]
class DocumentationOrMetadata(Statement, ABC):
@property
def value(self) -> str:
return "".join(self._get_lines()).rstrip()
def _get_lines(self) -> "Iterator[str]":
base_offset = -1
for tokens in self._get_line_tokens():
yield from self._get_line_values(tokens, base_offset)
first = tokens[0]
if base_offset < 0 or 0 < first.col_offset < base_offset and first.value:
base_offset = first.col_offset
def _get_line_tokens(self) -> "Iterator[list[Token]]":
line: "list[Token]" = []
lineno = -1
# There are no EOLs during execution or if data has been parsed with
# `data_only=True` otherwise, so we need to look at line numbers to
# know when lines change. If model is created programmatically using
# `from_params` or otherwise, line numbers may not be set, but there
# ought to be EOLs. If both EOLs and line numbers are missing,
# everything is considered to be on the same line.
for token in self.get_tokens(Token.ARGUMENT, Token.EOL):
eol = token.type == Token.EOL
if token.lineno != lineno or eol:
if line:
yield line
line = []
if not eol:
line.append(token)
lineno = token.lineno
if line:
yield line
def _get_line_values(self, tokens: "list[Token]", offset: int) -> "Iterator[str]":
token = None
for index, token in enumerate(tokens):
if token.col_offset > offset > 0:
yield " " * (token.col_offset - offset)
elif index > 0:
yield " "
yield self._remove_trailing_backslash(token.value)
offset = token.end_col_offset
if token and not self._has_trailing_backslash_or_newline(token.value):
yield "\n"
def _remove_trailing_backslash(self, value: str) -> str:
if value and value[-1] == "\\":
match = re.search(r"(\\+)$", value)
if match and len(match.group(1)) % 2 == 1:
value = value[:-1]
return value
def _has_trailing_backslash_or_newline(self, line: str) -> bool:
match = re.search(r"(\\+)n?$", line)
return bool(match and len(match.group(1)) % 2 == 1)
[docs]
class SingleValue(Statement, ABC):
@property
def value(self) -> "str|None":
values = self.get_values(Token.NAME, Token.ARGUMENT)
if values and values[0].upper() != "NONE":
return values[0]
return None
[docs]
class MultiValue(Statement, ABC):
@property
def values(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
[docs]
class Fixture(Statement, ABC):
@property
def name(self) -> str:
return self.get_value(Token.NAME, "")
@property
def args(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
[docs]
@Statement.register
class SectionHeader(Statement):
handles_types = (
Token.SETTING_HEADER,
Token.VARIABLE_HEADER,
Token.TESTCASE_HEADER,
Token.TASK_HEADER,
Token.KEYWORD_HEADER,
Token.COMMENT_HEADER,
Token.INVALID_HEADER,
)
[docs]
@classmethod
def from_params(
cls,
type: str,
name: "str|None" = None,
eol: str = EOL,
) -> "SectionHeader":
if not name:
names = (
"Settings",
"Variables",
"Test Cases",
"Tasks",
"Keywords",
"Comments",
)
name = dict(zip(cls.handles_types, names))[type]
header = f"*** {name} ***" if not name.startswith("*") else name
return cls([Token(type, header), Token(Token.EOL, eol)])
@property
def type(self) -> str:
token = self.get_token(*self.handles_types)
return token.type # type: ignore
@property
def name(self) -> str:
token = self.get_token(*self.handles_types)
return normalize_whitespace(token.value).strip("* ") if token else ""
[docs]
@Statement.register
class LibraryImport(Statement):
type = Token.LIBRARY
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
alias: "str|None" = None,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "LibraryImport":
tokens = [
Token(Token.LIBRARY, "Library"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
if alias is not None:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.AS),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, alias),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, "")
@property
def args(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def alias(self) -> "str|None":
separator = self.get_token(Token.AS)
return self.get_tokens(Token.NAME)[-1].value if separator else None
[docs]
@Statement.register
class ResourceImport(Statement):
type = Token.RESOURCE
[docs]
@classmethod
def from_params(
cls,
name: str,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "ResourceImport":
tokens = [
Token(Token.RESOURCE, "Resource"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
Token(Token.EOL, eol),
]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, "")
[docs]
@Statement.register
class VariablesImport(Statement):
type = Token.VARIABLES
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "VariablesImport":
tokens = [
Token(Token.VARIABLES, "Variables"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, "")
@property
def args(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
[docs]
@Statement.register
class Documentation(DocumentationOrMetadata):
type = Token.DOCUMENTATION
[docs]
@classmethod
def from_params(
cls,
value: str,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
settings_section: bool = True,
) -> "Documentation":
if settings_section:
tokens = [
Token(Token.DOCUMENTATION, "Documentation"),
Token(Token.SEPARATOR, separator),
]
else:
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.DOCUMENTATION, "[Documentation]"),
Token(Token.SEPARATOR, separator),
]
multiline_separator = " " * (len(tokens[-2].value) + len(separator) - 3)
doc_lines = value.splitlines()
if doc_lines:
tokens += [
Token(Token.ARGUMENT, doc_lines[0]),
Token(Token.EOL, eol),
]
for line in doc_lines[1:]:
if not settings_section:
tokens += [Token(Token.SEPARATOR, indent)]
tokens += [Token(Token.CONTINUATION)]
if line:
tokens += [Token(Token.SEPARATOR, multiline_separator)]
tokens += [
Token(Token.ARGUMENT, line),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class Metadata(DocumentationOrMetadata):
type = Token.METADATA
[docs]
@classmethod
def from_params(
cls,
name: str,
value: str,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Metadata":
tokens = [
Token(Token.METADATA, "Metadata"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
metadata_lines = value.splitlines()
if metadata_lines:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, metadata_lines[0]),
Token(Token.EOL, eol),
]
for line in metadata_lines[1:]:
tokens += [
Token(Token.CONTINUATION),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, line),
Token(Token.EOL, eol),
]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, "")
[docs]
@Statement.register
class TestTags(MultiValue):
type = Token.TEST_TAGS
[docs]
@classmethod
def from_params(
cls,
values: "Sequence[str]",
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TestTags":
tokens = [Token(Token.TEST_TAGS, "Test Tags")]
for tag in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, tag),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class DefaultTags(MultiValue):
type = Token.DEFAULT_TAGS
[docs]
@classmethod
def from_params(
cls,
values: "Sequence[str]",
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "DefaultTags":
tokens = [Token(Token.DEFAULT_TAGS, "Default Tags")]
for tag in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, tag),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class KeywordTags(MultiValue):
type = Token.KEYWORD_TAGS
[docs]
@classmethod
def from_params(
cls,
values: "Sequence[str]",
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "KeywordTags":
tokens = [Token(Token.KEYWORD_TAGS, "Keyword Tags")]
for tag in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, tag),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class SuiteName(SingleValue):
type = Token.SUITE_NAME
[docs]
@classmethod
def from_params(
cls,
value: str,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "SuiteName":
tokens = [
Token(Token.SUITE_NAME, "Name"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class SuiteSetup(Fixture):
type = Token.SUITE_SETUP
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "SuiteSetup":
tokens = [
Token(Token.SUITE_SETUP, "Suite Setup"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class SuiteTeardown(Fixture):
type = Token.SUITE_TEARDOWN
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "SuiteTeardown":
tokens = [
Token(Token.SUITE_TEARDOWN, "Suite Teardown"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class TestSetup(Fixture):
type = Token.TEST_SETUP
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TestSetup":
tokens = [
Token(Token.TEST_SETUP, "Test Setup"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class TestTeardown(Fixture):
type = Token.TEST_TEARDOWN
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TestTeardown":
tokens = [
Token(Token.TEST_TEARDOWN, "Test Teardown"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class TestTemplate(SingleValue):
type = Token.TEST_TEMPLATE
[docs]
@classmethod
def from_params(
cls,
value: str,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TestTemplate":
tokens = [
Token(Token.TEST_TEMPLATE, "Test Template"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class TestTimeout(SingleValue):
type = Token.TEST_TIMEOUT
[docs]
@classmethod
def from_params(
cls,
value: str,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TestTimeout":
tokens = [
Token(Token.TEST_TIMEOUT, "Test Timeout"),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class Variable(Statement):
type = Token.VARIABLE
options = {"separator": None}
[docs]
@classmethod
def from_params(
cls,
name: str,
value: "str|Sequence[str]",
value_separator: "str|None" = None,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Variable":
values = [value] if isinstance(value, str) else value
tokens = [Token(Token.VARIABLE, name)]
for value in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
]
if value_separator is not None:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"separator={value_separator}"),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
name = self.get_value(Token.VARIABLE, "")
if name.endswith("="):
return name[:-1].rstrip()
return name
@property
def value(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def separator(self) -> "str|None":
return self.get_option("separator")
[docs]
def validate(self, ctx: "ValidationContext"):
VariableValidator().validate(self)
self._validate_options()
[docs]
@Statement.register
class TestCaseName(Statement):
type = Token.TESTCASE_NAME
[docs]
@classmethod
def from_params(cls, name: str, eol: str = EOL) -> "TestCaseName":
tokens = [Token(Token.TESTCASE_NAME, name)]
if eol:
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.TESTCASE_NAME, "")
[docs]
def validate(self, ctx: "ValidationContext"):
if not self.name:
self.errors += (test_or_task("{Test} name cannot be empty.", ctx.tasks),)
[docs]
@Statement.register
class KeywordName(Statement):
type = Token.KEYWORD_NAME
[docs]
@classmethod
def from_params(cls, name: str, eol: str = EOL) -> "KeywordName":
tokens = [Token(Token.KEYWORD_NAME, name)]
if eol:
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.KEYWORD_NAME, "")
[docs]
def validate(self, ctx: "ValidationContext"):
if not self.name:
self.errors += ("User keyword name cannot be empty.",)
[docs]
@Statement.register
class Setup(Fixture):
type = Token.SETUP
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Setup":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.SETUP, "[Setup]"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class Teardown(Fixture):
type = Token.TEARDOWN
[docs]
@classmethod
def from_params(
cls,
name: str,
args: "Sequence[str]" = (),
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Teardown":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.TEARDOWN, "[Teardown]"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class Tags(MultiValue):
type = Token.TAGS
[docs]
@classmethod
def from_params(
cls,
values: "Sequence[str]",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Tags":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.TAGS, "[Tags]"),
]
for tag in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, tag),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class Template(SingleValue):
type = Token.TEMPLATE
[docs]
@classmethod
def from_params(
cls,
value: str,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Template":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.TEMPLATE, "[Template]"),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class Timeout(SingleValue):
type = Token.TIMEOUT
[docs]
@classmethod
def from_params(
cls,
value: str,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Timeout":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.TIMEOUT, "[Timeout]"),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class Arguments(MultiValue):
type = Token.ARGUMENTS
[docs]
@classmethod
def from_params(
cls,
args: "Sequence[str]",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Arguments":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.ARGUMENTS, "[Arguments]"),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
def validate(self, ctx: "ValidationContext"):
errors: "list[str]" = []
UserKeywordArgumentParser(error_reporter=errors.append).parse(self.values)
self.errors = tuple(errors)
[docs]
@Statement.register
class ReturnSetting(MultiValue):
"""Represents the deprecated ``[Return]`` setting.
This class was named ``Return`` prior to Robot Framework 7.0. A forward
compatible ``ReturnSetting`` alias existed already in Robot Framework 6.1.
"""
type = Token.RETURN
[docs]
@classmethod
def from_params(
cls,
args: "Sequence[str]",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "ReturnSetting":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.RETURN, "[Return]"),
]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
[docs]
@Statement.register
class KeywordCall(Statement):
type = Token.KEYWORD
[docs]
@classmethod
def from_params(
cls,
name: str,
assign: "Sequence[str]" = (),
args: "Sequence[str]" = (),
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "KeywordCall":
tokens = [Token(Token.SEPARATOR, indent)]
for assignment in assign:
tokens += [
Token(Token.ASSIGN, assignment),
Token(Token.SEPARATOR, separator),
]
tokens += [Token(Token.KEYWORD, name)]
for arg in args:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def keyword(self) -> str:
return self.get_value(Token.KEYWORD, "")
@property
def args(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def assign(self) -> "tuple[str, ...]":
return self.get_values(Token.ASSIGN)
[docs]
@Statement.register
class TemplateArguments(Statement):
type = Token.ARGUMENT
[docs]
@classmethod
def from_params(
cls,
args: "Sequence[str]",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "TemplateArguments":
tokens = []
for index, arg in enumerate(args):
tokens += [
Token(Token.SEPARATOR, separator if index else indent),
Token(Token.ARGUMENT, arg),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def args(self) -> "tuple[str, ...]":
return self.get_values(self.type)
[docs]
@Statement.register
class ForHeader(Statement):
type = Token.FOR
options = {"start": None, "mode": ("STRICT", "SHORTEST", "LONGEST"), "fill": None}
[docs]
@classmethod
def from_params(
cls,
assign: "Sequence[str]",
values: "Sequence[str]",
flavor: Literal["IN", "IN RANGE", "IN ENUMERATE", "IN ZIP"] = "IN",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "ForHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.FOR),
Token(Token.SEPARATOR, separator),
]
for variable in assign:
tokens += [
Token(Token.VARIABLE, variable),
Token(Token.SEPARATOR, separator),
]
tokens += [Token(Token.FOR_SEPARATOR, flavor)]
for value in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def assign(self) -> "tuple[str, ...]":
return self.get_values(Token.VARIABLE)
@property
def variables(self) -> "tuple[str, ...]": # TODO: Remove in RF 8.0.
warnings.warn(
"'ForHeader.variables' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'ForHeader.assign' instead."
)
return self.assign
@property
def values(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def flavor(self) -> "str|None":
separator = self.get_token(Token.FOR_SEPARATOR)
return normalize_whitespace(separator.value) if separator else None
@property
def start(self) -> "str|None":
return self.get_option("start") if self.flavor == "IN ENUMERATE" else None
@property
def mode(self) -> "str|None":
return self.get_option("mode") if self.flavor == "IN ZIP" else None
@property
def fill(self) -> "str|None":
return self.get_option("fill") if self.flavor == "IN ZIP" else None
[docs]
def validate(self, ctx: "ValidationContext"):
if not self.assign:
self.errors += ("FOR loop has no variables.",)
if not self.flavor:
self.errors += ("FOR loop has no 'IN' or other valid separator.",)
else:
for var in self.assign:
match = search_variable(var, ignore_errors=True, parse_type=True)
if not match.is_scalar_assign():
self.errors += (f"Invalid FOR loop variable '{var}'.",)
elif match.type:
try:
TypeInfo.from_variable(match)
except DataError as err:
self.errors += (f"Invalid FOR loop variable '{var}': {err}",)
if not self.values:
self.errors += ("FOR loop has no values.",)
self._validate_options()
[docs]
class IfElseHeader(Statement, ABC):
@property
def condition(self) -> "str|None":
values = self.get_values(Token.ARGUMENT)
return ", ".join(values) if values else None
@property
def assign(self) -> "tuple[str, ...]":
return self.get_values(Token.ASSIGN)
[docs]
def validate(self, ctx: "ValidationContext"):
conditions = self.get_tokens(Token.ARGUMENT)
if not conditions:
self.errors += (f"{self.type} must have a condition.",)
if len(conditions) > 1:
self.errors += (
f"{self.type} cannot have more than one condition, "
f"got {seq2str(c.value for c in conditions)}.",
)
[docs]
@Statement.register
class IfHeader(IfElseHeader):
type = Token.IF
[docs]
@classmethod
def from_params(
cls,
condition: str,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "IfHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(cls.type),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, condition),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class InlineIfHeader(IfElseHeader):
type = Token.INLINE_IF
[docs]
@classmethod
def from_params(
cls,
condition: str,
assign: "Sequence[str]" = (),
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
) -> "InlineIfHeader":
tokens = [Token(Token.SEPARATOR, indent)]
for assignment in assign:
tokens += [
Token(Token.ASSIGN, assignment),
Token(Token.SEPARATOR, separator),
]
tokens += [
Token(Token.INLINE_IF),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, condition),
]
return cls(tokens)
[docs]
def validate(self, ctx: "ValidationContext"):
super().validate(ctx)
AssignmentValidator().validate(self)
[docs]
@Statement.register
class ElseIfHeader(IfElseHeader):
type = Token.ELSE_IF
[docs]
@classmethod
def from_params(
cls,
condition: str,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "ElseIfHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.ELSE_IF),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, condition),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class ElseHeader(IfElseHeader):
type = Token.ELSE
[docs]
@classmethod
def from_params(cls, indent: str = FOUR_SPACES, eol: str = EOL) -> "ElseHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.ELSE),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
def validate(self, ctx: "ValidationContext"):
if self.get_tokens(Token.ARGUMENT):
values = self.get_values(Token.ARGUMENT)
self.errors += (f"ELSE does not accept arguments, got {seq2str(values)}.",)
[docs]
class NoArgumentHeader(Statement, ABC):
[docs]
@classmethod
def from_params(cls, indent: str = FOUR_SPACES, eol: str = EOL):
tokens = [
Token(Token.SEPARATOR, indent),
Token(cls.type),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
def validate(self, ctx: "ValidationContext"):
if self.get_tokens(Token.ARGUMENT):
self.errors += (
f"{self.type} does not accept arguments, got {seq2str(self.values)}.",
)
@property
def values(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
[docs]
@Statement.register
class ExceptHeader(Statement):
type = Token.EXCEPT
options = {"type": ("GLOB", "REGEXP", "START", "LITERAL")}
[docs]
@classmethod
def from_params(
cls,
patterns: "Sequence[str]" = (),
type: "str|None" = None,
assign: "str|None" = None,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "ExceptHeader":
tokens = [Token(Token.SEPARATOR, indent), Token(Token.EXCEPT)]
for pattern in patterns:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, pattern),
]
if type:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"type={type}"),
]
if assign:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.AS),
Token(Token.SEPARATOR, separator),
Token(Token.VARIABLE, assign),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def patterns(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def pattern_type(self) -> "str|None":
return self.get_option("type")
@property
def assign(self) -> "str|None":
return self.get_value(Token.VARIABLE)
@property
def variable(self) -> "str|None": # TODO: Remove in RF 8.0.
warnings.warn(
"'ExceptHeader.variable' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'ExceptHeader.assigns' instead."
)
return self.assign
[docs]
def validate(self, ctx: "ValidationContext"):
as_token = self.get_token(Token.AS)
if as_token:
assign = self.get_tokens(Token.VARIABLE)
if not assign:
self.errors += ("EXCEPT AS requires a value.",)
elif len(assign) > 1:
self.errors += ("EXCEPT AS accepts only one value.",)
elif not is_scalar_assign(assign[0].value):
self.errors += (f"EXCEPT AS variable '{assign[0].value}' is invalid.",)
self._validate_options()
[docs]
@Statement.register
class WhileHeader(Statement):
type = Token.WHILE
options = {
"limit": None,
"on_limit": ("PASS", "FAIL"),
"on_limit_message": None,
}
[docs]
@classmethod
def from_params(
cls,
condition: str,
limit: "str|None" = None,
on_limit: "str|None " = None,
on_limit_message: "str|None" = None,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "WhileHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.WHILE),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, condition),
]
if limit:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"limit={limit}"),
]
if on_limit:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"on_limit={on_limit}"),
]
if on_limit_message:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"on_limit_message={on_limit_message}"),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def condition(self) -> str:
return ", ".join(self.get_values(Token.ARGUMENT))
@property
def limit(self) -> "str|None":
return self.get_option("limit")
@property
def on_limit(self) -> "str|None":
return self.get_option("on_limit")
@property
def on_limit_message(self) -> "str|None":
return self.get_option("on_limit_message")
[docs]
def validate(self, ctx: "ValidationContext"):
conditions = self.get_values(Token.ARGUMENT)
if len(conditions) > 1:
self.errors += (
f"WHILE accepts only one condition, got {len(conditions)} "
f"conditions {seq2str(conditions)}.",
)
if self.on_limit and not self.limit:
self.errors += ("WHILE option 'on_limit' cannot be used without 'limit'.",)
self._validate_options()
[docs]
@Statement.register
class GroupHeader(Statement):
type = Token.GROUP
[docs]
@classmethod
def from_params(
cls,
name: str = "",
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "GroupHeader":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.GROUP),
]
if name:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, name),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
return ", ".join(self.get_values(Token.ARGUMENT))
[docs]
def validate(self, ctx: "ValidationContext"):
names = self.get_values(Token.ARGUMENT)
if len(names) > 1:
self.errors += (
f"GROUP accepts only one argument as name, got {len(names)} "
f"arguments {seq2str(names)}.",
)
[docs]
@Statement.register
class Var(Statement):
type = Token.VAR
options = {
"scope": ("LOCAL", "TEST", "TASK", "SUITE", "SUITES", "GLOBAL"),
"separator": None,
}
[docs]
@classmethod
def from_params(
cls,
name: str,
value: "str|Sequence[str]",
scope: "str|None" = None,
value_separator: "str|None" = None,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Var":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.VAR),
Token(Token.SEPARATOR, separator),
Token(Token.VARIABLE, name),
]
values = [value] if isinstance(value, str) else value
for value in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
]
if scope:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"scope={scope}"),
]
if value_separator:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f"separator={value_separator}"),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def name(self) -> str:
name = self.get_value(Token.VARIABLE, "")
if name.endswith("="):
return name[:-1].rstrip()
return name
@property
def value(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
@property
def scope(self) -> "str|None":
return self.get_option("scope")
@property
def separator(self) -> "str|None":
return self.get_option("separator")
[docs]
def validate(self, ctx: "ValidationContext"):
VariableValidator().validate(self)
self._validate_options()
[docs]
@Statement.register
class Return(Statement):
"""Represents the RETURN statement.
This class named ``ReturnStatement`` prior to Robot Framework 7.0.
The old name still exists as a backwards compatible alias.
"""
type = Token.RETURN_STATEMENT
[docs]
@classmethod
def from_params(
cls,
values: "Sequence[str]" = (),
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL,
) -> "Return":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.RETURN_STATEMENT),
]
for value in values:
tokens += [
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
]
tokens += [Token(Token.EOL, eol)]
return cls(tokens)
@property
def values(self) -> "tuple[str, ...]":
return self.get_values(Token.ARGUMENT)
[docs]
def validate(self, ctx: "ValidationContext"):
if not ctx.in_keyword:
self.errors += ("RETURN can only be used inside a user keyword.",)
if ctx.in_finally:
self.errors += ("RETURN cannot be used in FINALLY branch.",)
# Backwards compatibility with RF < 7.
ReturnStatement = Return
[docs]
class LoopControl(NoArgumentHeader, ABC):
[docs]
def validate(self, ctx: "ValidationContext"):
super().validate(ctx)
if not ctx.in_loop:
self.errors += (f"{self.type} can only be used inside a loop.",)
if ctx.in_finally:
self.errors += (f"{self.type} cannot be used in FINALLY branch.",)
[docs]
@Statement.register
class Comment(Statement):
type = Token.COMMENT
[docs]
@classmethod
def from_params(
cls,
comment: str,
indent: str = FOUR_SPACES,
eol: str = EOL,
) -> "Comment":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.COMMENT, comment),
Token(Token.EOL, eol),
]
return cls(tokens)
[docs]
@Statement.register
class Config(Statement):
type = Token.CONFIG
[docs]
@classmethod
def from_params(cls, config: str, eol: str = EOL) -> "Config":
tokens = [
Token(Token.CONFIG, config),
Token(Token.EOL, eol),
]
return cls(tokens)
@property
def language(self) -> "Language|None":
value = " ".join(self.get_values(Token.CONFIG))
lang = value.split(":", 1)[1].strip()
return Language.from_name(lang) if lang else None
[docs]
@Statement.register
class Error(Statement):
type = Token.ERROR
_errors: "tuple[str, ...]" = ()
[docs]
@classmethod
def from_params(
cls,
error: str,
value: str = "",
indent: str = FOUR_SPACES,
eol: str = EOL,
) -> "Error":
tokens = [
Token(Token.SEPARATOR, indent),
Token(Token.ERROR, value, error=error),
Token(Token.EOL, eol),
]
return cls(tokens)
@property
def values(self) -> "list[str]":
return [token.value for token in self.data_tokens]
@property
def errors(self) -> "tuple[str, ...]":
"""Errors got from the underlying ``ERROR``token.
Errors can be set also explicitly. When accessing errors, they are returned
along with errors got from tokens.
"""
tokens = self.get_tokens(Token.ERROR)
return tuple(t.error or "" for t in tokens) + self._errors
@errors.setter
def errors(self, errors: "Sequence[str]"):
self._errors = tuple(errors)
[docs]
class VariableValidator:
[docs]
def validate(self, statement: Statement):
name = statement.get_value(Token.VARIABLE, "")
match = search_variable(name, ignore_errors=True, parse_type=True)
if not match.is_assign(allow_assign_mark=True, allow_nested=True):
statement.errors += (f"Invalid variable name '{name}'.",)
return
if match.identifier == "&":
self._validate_dict_items(statement)
try:
TypeInfo.from_variable(match)
except DataError as err:
statement.errors += (f"Invalid variable '{name}': {err}",)
def _validate_dict_items(self, statement: Statement):
for item in statement.get_values(Token.ARGUMENT):
if not self._is_valid_dict_item(item):
statement.errors += (
f"Invalid dictionary variable item '{item}'. Items must use "
f"'name=value' syntax or be dictionary variables themselves.",
)
def _is_valid_dict_item(self, item: str) -> bool:
name, value = split_from_equals(item)
return value is not None or is_dict_variable(item)
[docs]
class AssignmentValidator:
[docs]
def validate(self, statement: Statement):
assignment = statement.get_values(Token.ASSIGN)
if assignment:
assignment = VariableAssignment(assignment)
statement.errors += assignment.errors
for variable in assignment:
try:
TypeInfo.from_variable(variable)
except DataError as err:
statement.errors += (f"Invalid variable '{variable}': {err}",)