# 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 warnings
from collections import OrderedDict
from typing import Any, cast, Literal, Mapping, Sequence, TYPE_CHECKING, TypeVar
from robot.utils import setter
from .body import BaseBranches, BaseIterations, Body, BodyItem, BodyItemParent
from .modelobject import DataDict
from .visitor import SuiteVisitor
if TYPE_CHECKING:
from .keyword import Keyword
from .message import Message
IT = TypeVar("IT", bound="IfBranch|TryBranch")
FW = TypeVar("FW", bound="ForIteration|WhileIteration")
[docs]
class Branches(BaseBranches[
"Keyword", "For", "While", "Group", "If", "Try", "Var", "Return", "Continue",
"Break", "Message", "Error", IT
]): # fmt: skip
__slots__ = ()
[docs]
class Iterations(BaseIterations[
"Keyword", "For", "While", "Group", "If", "Try", "Var", "Return", "Continue",
"Break", "Message", "Error", FW
]): # fmt: skip
__slots__ = ()
[docs]
class ForIteration(BodyItem):
"""Represents one FOR loop iteration."""
type = BodyItem.ITERATION
body_class = Body
repr_args = ("assign",)
__slots__ = ("assign", "message", "status")
def __init__(
self,
assign: "Mapping[str, str]|None" = None,
parent: BodyItemParent = None,
):
self.assign = OrderedDict(assign or ())
self.parent = parent
self.body = ()
@property
def variables(self) -> "Mapping[str, str]": # TODO: Remove in RF 8.0.
"""Deprecated since Robot Framework 7.0. Use :attr:`assign` instead."""
warnings.warn(
"'ForIteration.variables' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'ForIteration.assign' instead."
)
return self.assign
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_for_iteration(self)
@property
def _log_name(self):
return ", ".join(f"{name} = {value}" for name, value in self.assign.items())
[docs]
def to_dict(self) -> DataDict:
return {
"type": self.type,
"assign": dict(self.assign),
"body": self.body.to_dicts(),
}
[docs]
@Body.register
class For(BodyItem):
"""Represents ``FOR`` loops."""
type = BodyItem.FOR
body_class = Body
repr_args = ("assign", "flavor", "values", "start", "mode", "fill")
__slots__ = ("assign", "flavor", "values", "start", "mode", "fill")
def __init__(
self,
assign: Sequence[str] = (),
flavor: Literal["IN", "IN RANGE", "IN ENUMERATE", "IN ZIP"] = "IN",
values: Sequence[str] = (),
start: "str|None" = None,
mode: "str|None" = None,
fill: "str|None" = None,
parent: BodyItemParent = None,
):
self.assign = tuple(assign)
self.flavor = flavor
self.values = tuple(values)
self.start = start
self.mode = mode
self.fill = fill
self.parent = parent
self.body = ()
@property
def variables(self) -> "tuple[str, ...]": # TODO: Remove in RF 8.0.
"""Deprecated since Robot Framework 7.0. Use :attr:`assign` instead."""
warnings.warn(
"'For.variables' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'For.assign' instead."
)
return self.assign
@variables.setter
def variables(self, assign: "tuple[str, ...]"):
warnings.warn(
"'For.variables' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'For.assign' instead."
)
self.assign = assign
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_for(self)
[docs]
def to_dict(self) -> DataDict:
data = {
"type": self.type,
"assign": self.assign,
"flavor": self.flavor,
"values": self.values,
}
for name, value in [
("start", self.start),
("mode", self.mode),
("fill", self.fill),
]:
if value is not None:
data[name] = value
data["body"] = self.body.to_dicts()
return data
def __str__(self):
parts = ["FOR", *self.assign, self.flavor, *self.values]
for name, value in [
("start", self.start),
("mode", self.mode),
("fill", self.fill),
]:
if value is not None:
parts.append(f"{name}={value}")
return " ".join(parts)
def _include_in_repr(self, name: str, value: Any) -> bool:
return value is not None or name in ("assign", "flavor", "values")
[docs]
class WhileIteration(BodyItem):
"""Represents one WHILE loop iteration."""
type = BodyItem.ITERATION
body_class = Body
__slots__ = ()
def __init__(self, parent: BodyItemParent = None):
self.parent = parent
self.body = ()
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_while_iteration(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type, "body": self.body.to_dicts()}
[docs]
@Body.register
class While(BodyItem):
"""Represents ``WHILE`` loops."""
type = BodyItem.WHILE
body_class = Body
repr_args = ("condition", "limit", "on_limit", "on_limit_message")
__slots__ = ("condition", "limit", "on_limit", "on_limit_message")
def __init__(
self,
condition: "str|None" = None,
limit: "str|None" = None,
on_limit: "str|None" = None,
on_limit_message: "str|None" = None,
parent: BodyItemParent = None,
):
self.condition = condition
self.on_limit = on_limit
self.limit = limit
self.on_limit_message = on_limit_message
self.parent = parent
self.body = ()
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_while(self)
def _include_in_repr(self, name: str, value: Any) -> bool:
return name == "condition" or value is not None
[docs]
def to_dict(self) -> DataDict:
data: DataDict = {"type": self.type}
for name, value in [
("condition", self.condition),
("limit", self.limit),
("on_limit", self.on_limit),
("on_limit_message", self.on_limit_message),
]:
if value is not None:
data[name] = value
data["body"] = self.body.to_dicts()
return data
def __str__(self) -> str:
parts = ["WHILE"]
if self.condition is not None:
parts.append(self.condition)
if self.limit is not None:
parts.append(f"limit={self.limit}")
if self.on_limit is not None:
parts.append(f"on_limit={self.on_limit}")
if self.on_limit_message is not None:
parts.append(f"on_limit_message={self.on_limit_message}")
return " ".join(parts)
[docs]
@Body.register
class Group(BodyItem):
"""Represents ``GROUP``."""
type = BodyItem.GROUP
body_class = Body
repr_args = ("name",)
__slots__ = ("name",)
def __init__(self, name: str = "", parent: BodyItemParent = None):
self.name = name
self.parent = parent
self.body = ()
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_group(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type, "name": self.name, "body": self.body.to_dicts()}
def __str__(self) -> str:
parts = ["GROUP"]
if self.name:
parts.append(self.name)
return " ".join(parts)
[docs]
class IfBranch(BodyItem):
"""Represents individual ``IF``, ``ELSE IF`` or ``ELSE`` branch."""
body_class = Body
repr_args = ("type", "condition")
__slots__ = ("type", "condition")
def __init__(
self,
type: str = BodyItem.IF,
condition: "str|None" = None,
parent: BodyItemParent = None,
):
self.type = type
self.condition = condition
self.parent = parent
self.body = ()
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
@property
def id(self) -> str:
"""Branch id omits IF/ELSE root from the parent id part."""
if not self.parent:
return "k1"
if not self.parent.parent:
return self._get_id(self.parent)
return self._get_id(self.parent.parent)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_if_branch(self)
[docs]
def to_dict(self) -> DataDict:
data = {"type": self.type}
if self.condition:
data["condition"] = self.condition
data["body"] = self.body.to_dicts()
return data
def __str__(self) -> str:
if self.type == self.IF:
return f"IF {self.condition}"
if self.type == self.ELSE_IF:
return f"ELSE IF {self.condition}"
return "ELSE"
[docs]
@Body.register
class If(BodyItem):
"""IF/ELSE structure root. Branches are stored in :attr:`body`."""
type = BodyItem.IF_ELSE_ROOT
branch_class = IfBranch
branches_class = Branches[branch_class]
__slots__ = ()
def __init__(self, parent: BodyItemParent = None):
self.parent = parent
self.body = ()
@setter
def body(self, branches: "Sequence[BodyItem|DataDict]") -> branches_class:
return self.branches_class(self.branch_class, self, branches)
@property
def id(self) -> None:
"""Root IF/ELSE id is always ``None``."""
return None
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_if(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type, "body": self.body.to_dicts()}
[docs]
class TryBranch(BodyItem):
"""Represents individual ``TRY``, ``EXCEPT``, ``ELSE`` or ``FINALLY`` branch."""
body_class = Body
repr_args = ("type", "patterns", "pattern_type", "assign")
__slots__ = ("type", "patterns", "pattern_type", "assign")
def __init__(
self,
type: str = BodyItem.TRY,
patterns: Sequence[str] = (),
pattern_type: "str|None" = None,
assign: "str|None" = None,
parent: BodyItemParent = None,
):
if (patterns or pattern_type or assign) and type != BodyItem.EXCEPT:
raise TypeError(f"'{type}' branches do not accept patterns or assignment.")
self.type = type
self.patterns = tuple(patterns)
self.pattern_type = pattern_type
self.assign = assign
self.parent = parent
self.body = ()
@property
def variable(self) -> "str|None": # TODO: Remove in RF 8.0.
"""Deprecated since Robot Framework 7.0. Use :attr:`assign` instead."""
warnings.warn(
"'TryBranch.variable' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'TryBranch.assign' instead."
)
return self.assign
@variable.setter
def variable(self, assign: "str|None"):
warnings.warn(
"'TryBranch.variable' is deprecated and will be removed in "
"Robot Framework 8.0. Use 'TryBranch.assign' instead."
)
self.assign = assign
@setter
def body(self, body: "Sequence[BodyItem|DataDict]") -> Body:
return self.body_class(self, body)
@property
def id(self) -> str:
"""Branch id omits TRY/EXCEPT root from the parent id part."""
if not self.parent:
return "k1"
if not self.parent.parent:
return self._get_id(self.parent)
return self._get_id(self.parent.parent)
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_try_branch(self)
[docs]
def to_dict(self) -> DataDict:
data: DataDict = {"type": self.type}
if self.type == self.EXCEPT:
data["patterns"] = self.patterns
if self.pattern_type:
data["pattern_type"] = self.pattern_type
if self.assign:
data["assign"] = self.assign
data["body"] = self.body.to_dicts()
return data
def __str__(self) -> str:
if self.type != BodyItem.EXCEPT:
return self.type
parts = ["EXCEPT", *self.patterns]
if self.pattern_type:
parts.append(f"type={self.pattern_type}")
if self.assign:
parts.extend(["AS", self.assign])
return " ".join(parts)
def _include_in_repr(self, name: str, value: Any) -> bool:
return bool(value)
[docs]
@Body.register
class Try(BodyItem):
"""TRY/EXCEPT structure root. Branches are stored in :attr:`body`."""
type = BodyItem.TRY_EXCEPT_ROOT
branch_class = TryBranch
branches_class = Branches[branch_class]
__slots__ = ()
def __init__(self, parent: BodyItemParent = None):
self.parent = parent
self.body = ()
@setter
def body(self, branches: "Sequence[TryBranch|DataDict]") -> branches_class:
return self.branches_class(self.branch_class, self, branches)
@property
def try_branch(self) -> TryBranch:
if self.body and self.body[0].type == BodyItem.TRY:
return cast(TryBranch, self.body[0])
raise TypeError("No 'TRY' branch or 'TRY' branch is not first.")
@property
def except_branches(self) -> "list[TryBranch]":
return [
cast(TryBranch, branch)
for branch in self.body
if branch.type == BodyItem.EXCEPT
]
@property
def else_branch(self) -> "TryBranch|None":
for branch in self.body:
if branch.type == BodyItem.ELSE:
return cast(TryBranch, branch)
return None
@property
def finally_branch(self) -> "TryBranch|None":
if self.body and self.body[-1].type == BodyItem.FINALLY:
return cast(TryBranch, self.body[-1])
return None
@property
def id(self) -> None:
"""Root TRY/EXCEPT id is always ``None``."""
return None
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_try(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type, "body": self.body.to_dicts()}
[docs]
@Body.register
class Var(BodyItem):
"""Represents ``VAR``."""
type = BodyItem.VAR
repr_args = ("name", "value", "scope", "separator")
__slots__ = ("name", "value", "scope", "separator")
def __init__(
self,
name: str = "",
value: "str|Sequence[str]" = (),
scope: "str|None" = None,
separator: "str|None" = None,
parent: BodyItemParent = None,
):
self.name = name
self.value = (value,) if isinstance(value, str) else tuple(value)
self.scope = scope
self.separator = separator
self.parent = parent
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_var(self)
[docs]
def to_dict(self) -> DataDict:
data = {"type": self.type, "name": self.name, "value": self.value}
if self.scope is not None:
data["scope"] = self.scope
if self.separator is not None:
data["separator"] = self.separator
return data
def __str__(self):
parts = ["VAR", self.name, *self.value]
if self.separator is not None:
parts.append(f"separator={self.separator}")
if self.scope is not None:
parts.append(f"scope={self.scope}")
return " ".join(parts)
def _include_in_repr(self, name: str, value: Any) -> bool:
return value is not None or name in ("name", "value")
[docs]
@Body.register
class Return(BodyItem):
"""Represents ``RETURN``."""
type = BodyItem.RETURN
repr_args = ("values",)
__slots__ = ("values",)
def __init__(self, values: Sequence[str] = (), parent: BodyItemParent = None):
self.values = tuple(values)
self.parent = parent
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_return(self)
[docs]
def to_dict(self) -> DataDict:
data = {"type": self.type}
if self.values:
data["values"] = self.values
return data
def __str__(self):
return " ".join(["RETURN", *self.values])
def _include_in_repr(self, name: str, value: Any) -> bool:
return bool(value)
[docs]
@Body.register
class Continue(BodyItem):
"""Represents ``CONTINUE``."""
type = BodyItem.CONTINUE
__slots__ = ()
def __init__(self, parent: BodyItemParent = None):
self.parent = parent
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_continue(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type}
def __str__(self):
return "CONTINUE"
[docs]
@Body.register
class Break(BodyItem):
"""Represents ``BREAK``."""
type = BodyItem.BREAK
__slots__ = ()
def __init__(self, parent: BodyItemParent = None):
self.parent = parent
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_break(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type}
def __str__(self):
return "BREAK"
[docs]
@Body.register
class Error(BodyItem):
"""Represents syntax error in data.
For example, an invalid setting like ``[Setpu]`` or ``END`` in wrong place.
"""
type = BodyItem.ERROR
repr_args = ("values",)
__slots__ = ("values",)
def __init__(self, values: Sequence[str] = (), parent: BodyItemParent = None):
self.values = tuple(values)
self.parent = parent
[docs]
def visit(self, visitor: SuiteVisitor):
visitor.visit_error(self)
[docs]
def to_dict(self) -> DataDict:
return {"type": self.type, "values": self.values}
def __str__(self):
return " ".join(["ERROR", *self.values])