# 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.
from abc import ABC, abstractmethod
from robot.conf import Languages
from robot.utils import normalize, normalize_whitespace, RecommendationFinder
from .tokens import StatementTokens, Token
[docs]
class Settings(ABC):
names: "tuple[str, ...]" = ()
aliases: "dict[str, str]" = {}
multi_use = (
"Metadata",
"Library",
"Resource",
"Variables",
)
single_value = (
"Resource",
"Test Timeout",
"Test Template",
"Timeout",
"Template",
"Name",
)
name_and_arguments = (
"Metadata",
"Suite Setup",
"Suite Teardown",
"Test Setup",
"Test Teardown",
"Test Template",
"Setup",
"Teardown",
"Template",
"Resource",
"Variables",
)
name_arguments_and_with_name = (
"Library",
) # fmt: skip
def __init__(self, languages: Languages):
self.settings: "dict[str, list[Token]|None]" = dict.fromkeys(self.names)
self.languages = languages
[docs]
def lex(self, statement: StatementTokens):
orig = self._format_name(statement[0].value)
name = normalize_whitespace(orig).title()
name = self.languages.settings.get(name, name)
if name in self.aliases:
name = self.aliases[name]
try:
self._validate(orig, name, statement)
except ValueError as err:
self._lex_error(statement, err.args[0])
else:
self._lex_setting(statement, name)
def _format_name(self, name: str) -> str:
return name
def _validate(self, orig: str, name: str, statement: StatementTokens):
if name not in self.settings:
message = self._get_non_existing_setting_message(orig, name)
raise ValueError(message)
if self.settings[name] is not None and name not in self.multi_use:
raise ValueError(
f"Setting '{orig}' is allowed only once. Only the first value is used."
)
if name in self.single_value and len(statement) > 2:
raise ValueError(
f"Setting '{orig}' accepts only one value, got {len(statement) - 1}."
)
def _get_non_existing_setting_message(self, name: str, normalized: str) -> str:
if self._is_valid_somewhere(normalized, Settings.__subclasses__()):
return self._not_valid_here(name)
return RecommendationFinder(normalize).find_and_format(
name=normalized,
candidates=tuple(self.settings) + tuple(self.aliases),
message=f"Non-existing setting '{name}'.",
)
def _is_valid_somewhere(self, name: str, classes: "list[type[Settings]]") -> bool:
for cls in classes:
if (
name in cls.names
or name in cls.aliases
or self._is_valid_somewhere(name, cls.__subclasses__())
):
return True
return False
@abstractmethod
def _not_valid_here(self, name: str) -> str:
raise NotImplementedError
def _lex_error(self, statement: StatementTokens, error: str):
statement[0].set_error(error)
for token in statement[1:]:
token.type = Token.COMMENT
def _lex_setting(self, statement: StatementTokens, name: str):
statement[0].type = {
"Test Tags": Token.TEST_TAGS,
"Name": Token.SUITE_NAME,
}.get(name, name.upper())
self.settings[name] = values = statement[1:]
if name in self.name_and_arguments:
self._lex_name_and_arguments(values)
elif name in self.name_arguments_and_with_name:
self._lex_name_arguments_and_with_name(values)
else:
self._lex_arguments(values)
if name == "Return":
statement[0].error = (
"The '[Return]' setting is deprecated. "
"Use the 'RETURN' statement instead."
)
def _lex_name_and_arguments(self, tokens: StatementTokens):
if tokens:
tokens[0].type = Token.NAME
self._lex_arguments(tokens[1:])
def _lex_name_arguments_and_with_name(self, tokens: StatementTokens):
self._lex_name_and_arguments(tokens)
marker = tokens[-2].value if len(tokens) > 1 else None
if marker and normalize_whitespace(marker) in ("WITH NAME", "AS"):
tokens[-2].type = Token.AS
tokens[-1].type = Token.NAME
def _lex_arguments(self, tokens: StatementTokens):
for token in tokens:
token.type = Token.ARGUMENT
[docs]
class FileSettings(Settings, ABC):
pass
[docs]
class SuiteFileSettings(FileSettings):
names = (
"Documentation",
"Metadata",
"Name",
"Suite Setup",
"Suite Teardown",
"Test Setup",
"Test Teardown",
"Test Template",
"Test Timeout",
"Test Tags",
"Default Tags",
"Keyword Tags",
"Library",
"Resource",
"Variables",
)
aliases = {
"Force Tags": "Test Tags",
"Task Tags": "Test Tags",
"Task Setup": "Test Setup",
"Task Teardown": "Test Teardown",
"Task Template": "Test Template",
"Task Timeout": "Test Timeout",
}
def _not_valid_here(self, name: str) -> str:
return f"Setting '{name}' is not allowed in suite file."
[docs]
class InitFileSettings(FileSettings):
names = (
"Documentation",
"Metadata",
"Name",
"Suite Setup",
"Suite Teardown",
"Test Setup",
"Test Teardown",
"Test Timeout",
"Test Tags",
"Keyword Tags",
"Library",
"Resource",
"Variables",
)
aliases = {
"Force Tags": "Test Tags",
"Task Tags": "Test Tags",
"Task Setup": "Test Setup",
"Task Teardown": "Test Teardown",
"Task Timeout": "Test Timeout",
}
def _not_valid_here(self, name: str) -> str:
return f"Setting '{name}' is not allowed in suite initialization file."
[docs]
class ResourceFileSettings(FileSettings):
names = (
"Documentation",
"Keyword Tags",
"Library",
"Resource",
"Variables",
)
def _not_valid_here(self, name: str) -> str:
return f"Setting '{name}' is not allowed in resource file."
[docs]
class TestCaseSettings(Settings):
names = (
"Documentation",
"Tags",
"Setup",
"Teardown",
"Template",
"Timeout",
)
def __init__(self, parent: SuiteFileSettings):
super().__init__(parent.languages)
self.parent = parent
def _format_name(self, name: str) -> str:
return name[1:-1].strip()
@property
def template_set(self) -> bool:
template = self.settings["Template"]
if self._has_disabling_value(template):
return False
parent_template = self.parent.settings["Test Template"]
return self._has_value(template) or self._has_value(parent_template)
def _has_disabling_value(self, setting: "StatementTokens|None") -> bool:
if setting is None:
return False
return setting == [] or setting[0].value.upper() == "NONE"
def _has_value(self, setting: "StatementTokens|None") -> bool:
return bool(setting and setting[0].value)
def _not_valid_here(self, name: str) -> str:
return f"Setting '{name}' is not allowed with tests or tasks."
[docs]
class KeywordSettings(Settings):
names = (
"Documentation",
"Arguments",
"Setup",
"Teardown",
"Timeout",
"Tags",
"Return",
)
def __init__(self, parent: FileSettings):
super().__init__(parent.languages)
self.parent = parent
def _format_name(self, name: str) -> str:
return name[1:-1].strip()
def _not_valid_here(self, name: str) -> str:
return f"Setting '{name}' is not allowed with user keywords."