# 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 pathlib import Path
from typing import Any, Iterable, Literal, overload, Sequence, TYPE_CHECKING
from robot import model
from robot.model import BodyItem, create_fixture, DataDict, ModelObject, Tags
from robot.output import LOGGER
from robot.utils import NOT_SET, setter
from .arguments import ArgInfo, ArgumentSpec, UserKeywordArgumentParser
from .keywordimplementation import KeywordImplementation
from .keywordfinder import KeywordFinder
from .model import Body, BodyItemParent, Keyword, TestSuite
from .userkeywordrunner import UserKeywordRunner, EmbeddedArgumentsRunner
if TYPE_CHECKING:
from robot.conf import LanguagesLike
from robot.parsing import File
[docs]
class ResourceFile(ModelObject):
"""Represents a resource file."""
repr_args = ('source',)
__slots__ = ('_source', 'owner', 'doc', 'keyword_finder')
def __init__(self, source: 'Path|str|None' = None,
owner: 'TestSuite|None' = None,
doc: str = ''):
self.source = source
self.owner = owner
self.doc = doc
self.keyword_finder = KeywordFinder['UserKeyword'](self)
self.imports = []
self.variables = []
self.keywords = []
@property
def source(self) -> 'Path|None':
if self._source:
return self._source
if self.owner:
return self.owner.source
return None
@source.setter
def source(self, source: 'Path|str|None'):
if isinstance(source, str):
source = Path(source)
self._source = source
@property
def name(self) -> 'str|None':
"""Resource file name.
``None`` if resource file is part of a suite or if it does not have
:attr:`source`, name of the source file without the extension otherwise.
"""
if self.owner or not self.source:
return None
return self.source.stem
@setter
def imports(self, imports: Sequence['Import']) -> 'Imports':
return Imports(self, imports)
@setter
def variables(self, variables: Sequence['Variable']) -> 'Variables':
return Variables(self, variables)
@setter
def keywords(self, keywords: Sequence['UserKeyword']) -> 'UserKeywords':
return UserKeywords(self, keywords)
[docs]
@classmethod
def from_file_system(cls, path: 'Path|str', **config) -> 'ResourceFile':
"""Create a :class:`ResourceFile` object based on the give ``path``.
:param path: File path where to read the data from.
:param config: Configuration parameters for :class:`~.builders.ResourceFileBuilder`
class that is used internally for building the suite.
New in Robot Framework 6.1. See also :meth:`from_string` and :meth:`from_model`.
"""
from .builder import ResourceFileBuilder
return ResourceFileBuilder(**config).build(path)
[docs]
@classmethod
def from_string(cls, string: str, **config) -> 'ResourceFile':
"""Create a :class:`ResourceFile` object based on the given ``string``.
:param string: String to create the resource file from.
:param config: Configuration parameters for
:func:`~robot.parsing.parser.parser.get_resource_model` used internally.
New in Robot Framework 6.1. See also :meth:`from_file_system` and
:meth:`from_model`.
"""
from robot.parsing import get_resource_model
model = get_resource_model(string, data_only=True, **config)
return cls.from_model(model)
[docs]
@classmethod
def from_model(cls, model: 'File') -> 'ResourceFile':
"""Create a :class:`ResourceFile` object based on the given ``model``.
:param model: Model to create the suite from.
The model can be created by using the
:func:`~robot.parsing.parser.parser.get_resource_model` function and possibly
modified by other tooling in the :mod:`robot.parsing` module.
New in Robot Framework 6.1. See also :meth:`from_file_system` and
:meth:`from_string`.
"""
from .builder import RobotParser
return RobotParser().parse_resource_model(model)
@overload
def find_keywords(self, name: str, count: Literal[1]) -> 'UserKeyword':
...
@overload
def find_keywords(self, name: str, count: 'int|None' = None) -> 'list[UserKeyword]':
...
[docs]
def find_keywords(self, name: str, count: 'int|None' = None) \
-> 'list[UserKeyword]|UserKeyword':
return self.keyword_finder.find(name, count)
[docs]
def to_dict(self) -> DataDict:
data = {}
if self._source:
data['source'] = str(self._source)
if self.doc:
data['doc'] = self.doc
if self.imports:
data['imports'] = self.imports.to_dicts()
if self.variables:
data['variables'] = self.variables.to_dicts()
if self.keywords:
data['keywords'] = self.keywords.to_dicts()
return data
[docs]
class UserKeyword(KeywordImplementation):
"""Represents a user keyword."""
type = KeywordImplementation.USER_KEYWORD
fixture_class = Keyword
__slots__ = ['timeout', '_setup', '_teardown']
def __init__(self, name: str = '',
args: 'ArgumentSpec|Sequence[str]|None' = (),
doc: str = '',
tags: 'Tags|Sequence[str]' = (),
timeout: 'str|None' = None,
lineno: 'int|None' = None,
owner: 'ResourceFile|None' = None,
parent: 'BodyItemParent|None' = None,
error: 'str|None' = None):
super().__init__(name, args, doc, tags, lineno, owner, parent, error)
self.timeout = timeout
self._setup = None
self._teardown = None
self.body = []
@setter
def args(self, spec: 'ArgumentSpec|Sequence[str]|None') -> ArgumentSpec:
if not spec:
spec = ArgumentSpec()
elif not isinstance(spec, ArgumentSpec):
spec = UserKeywordArgumentParser().parse(spec)
spec.name = lambda: self.full_name
return spec
@setter
def body(self, body: 'Sequence[BodyItem|DataDict]') -> Body:
return Body(self, body)
@property
def setup(self) -> Keyword:
"""User keyword setup as a :class:`Keyword` object.
New in Robot Framework 7.0.
"""
if self._setup is None:
self.setup = None
return self._setup
@setup.setter
def setup(self, setup: 'Keyword|DataDict|None'):
self._setup = create_fixture(self.fixture_class, setup, self, Keyword.SETUP)
@property
def has_setup(self) -> bool:
"""Check does a keyword have a setup without creating a setup object.
See :attr:`has_teardown` for more information. New in Robot Framework 7.0.
"""
return bool(self._setup)
@property
def teardown(self) -> Keyword:
"""User keyword teardown as a :class:`Keyword` object."""
if self._teardown is None:
self.teardown = None
return self._teardown
@teardown.setter
def teardown(self, teardown: 'Keyword|DataDict|None'):
self._teardown = create_fixture(self.fixture_class, teardown, self, Keyword.TEARDOWN)
@property
def has_teardown(self) -> bool:
"""Check does a keyword have a teardown without creating a teardown object.
A difference between using ``if kw.has_teardown:`` and ``if kw.teardown:``
is that accessing the :attr:`teardown` attribute creates a :class:`Keyword`
object representing the teardown even when the user keyword actually does
not have one. This can have an effect on memory usage.
New in Robot Framework 6.1.
"""
return bool(self._teardown)
[docs]
def create_runner(self, name: 'str|None',
languages: 'LanguagesLike' = None) \
-> 'UserKeywordRunner|EmbeddedArgumentsRunner':
if self.embedded:
return EmbeddedArgumentsRunner(self, name)
return UserKeywordRunner(self)
[docs]
def bind(self, data: Keyword) -> 'UserKeyword':
kw = UserKeyword('', self.args.copy(), self.doc, self.tags, self.timeout,
self.lineno, self.owner, data.parent, self.error)
# Avoid possible errors setting name with invalid embedded args.
kw._name = self._name
kw.embedded = self.embedded
if self.has_setup:
kw.setup = self.setup.to_dict()
if self.has_teardown:
kw.teardown = self.teardown.to_dict()
kw.body = self.body.to_dicts()
return kw
[docs]
def to_dict(self) -> DataDict:
data: DataDict = {'name': self.name}
for name, value in [('args', tuple(self._decorate_arg(a) for a in self.args)),
('doc', self.doc),
('tags', tuple(self.tags)),
('timeout', self.timeout),
('lineno', self.lineno),
('error', self.error)]:
if value:
data[name] = value
if self.has_setup:
data['setup'] = self.setup.to_dict()
data['body'] = self.body.to_dicts()
if self.has_teardown:
data['teardown'] = self.teardown.to_dict()
return data
def _decorate_arg(self, arg: ArgInfo) -> str:
if arg.kind == arg.VAR_NAMED:
deco = '&'
elif arg.kind in (arg.VAR_POSITIONAL, arg.NAMED_ONLY_MARKER):
deco = '@'
else:
deco = '$'
result = f'{deco}{{{arg.name}}}'
if arg.default is not NOT_SET:
result += f'={arg.default}'
return result
[docs]
class Variable(ModelObject):
repr_args = ('name', 'value', 'separator')
def __init__(self, name: str = '',
value: Sequence[str] = (),
separator: 'str|None' = None,
owner: 'ResourceFile|None' = None,
lineno: 'int|None' = None,
error: 'str|None' = None):
self.name = name
self.value = tuple(value)
self.separator = separator
self.owner = owner
self.lineno = lineno
self.error = error
@property
def source(self) -> 'Path|None':
return self.owner.source if self.owner is not None else None
[docs]
def report_error(self, message: str, level: str = 'ERROR'):
source = self.source or '<unknown>'
line = f' on line {self.lineno}' if self.lineno else ''
LOGGER.write(f"Error in file '{source}'{line}: "
f"Setting variable '{self.name}' failed: {message}", level)
[docs]
def to_dict(self) -> DataDict:
data = {'name': self.name, 'value': self.value}
if self.lineno:
data['lineno'] = self.lineno
if self.error:
data['error'] = self.error
return data
def _include_in_repr(self, name: str, value: Any) -> bool:
return not (name == 'separator' and value is None)
[docs]
class Import(ModelObject):
"""Represents library, resource file or variable file import."""
repr_args = ('type', 'name', 'args', 'alias')
LIBRARY = 'LIBRARY'
RESOURCE = 'RESOURCE'
VARIABLES = 'VARIABLES'
def __init__(self, type: Literal['LIBRARY', 'RESOURCE', 'VARIABLES'],
name: str,
args: Sequence[str] = (),
alias: 'str|None' = None,
owner: 'ResourceFile|None' = None,
lineno: 'int|None' = None):
if type not in (self.LIBRARY, self.RESOURCE, self.VARIABLES):
raise ValueError(f"Invalid import type: Expected '{self.LIBRARY}', "
f"'{self.RESOURCE}' or '{self.VARIABLES}', got '{type}'.")
self.type = type
self.name = name
self.args = tuple(args)
self.alias = alias
self.owner = owner
self.lineno = lineno
@property
def source(self) -> 'Path|None':
return self.owner.source if self.owner is not None else None
@property
def directory(self) -> 'Path|None':
source = self.source
return source.parent if source and not source.is_dir() else source
@property
def setting_name(self) -> str:
return self.type.title()
[docs]
def select(self, library: Any, resource: Any, variables: Any) -> Any:
return {self.LIBRARY: library,
self.RESOURCE: resource,
self.VARIABLES: variables}[self.type]
[docs]
def report_error(self, message: str, level: str = 'ERROR'):
source = self.source or '<unknown>'
line = f' on line {self.lineno}' if self.lineno else ''
LOGGER.write(f"Error in file '{source}'{line}: {message}", level)
[docs]
@classmethod
def from_dict(cls, data) -> 'Import':
return cls(**data)
[docs]
def to_dict(self) -> DataDict:
data: DataDict = {'type': self.type, 'name': self.name}
if self.args:
data['args'] = self.args
if self.alias:
data['alias'] = self.alias
if self.lineno:
data['lineno'] = self.lineno
return data
def _include_in_repr(self, name: str, value: Any) -> bool:
return name in ('type', 'name') or value
[docs]
class Imports(model.ItemList):
def __init__(self, owner: ResourceFile, imports: Sequence[Import] = ()):
super().__init__(Import, {'owner': owner}, items=imports)
[docs]
def library(self, name: str, args: Sequence[str] = (), alias: 'str|None' = None,
lineno: 'int|None' = None) -> Import:
"""Create library import."""
return self.create(Import.LIBRARY, name, args, alias, lineno=lineno)
[docs]
def resource(self, name: str, lineno: 'int|None' = None) -> Import:
"""Create resource import."""
return self.create(Import.RESOURCE, name, lineno=lineno)
[docs]
def variables(self, name: str, args: Sequence[str] = (),
lineno: 'int|None' = None) -> Import:
"""Create variables import."""
return self.create(Import.VARIABLES, name, args, lineno=lineno)
[docs]
def create(self, *args, **kwargs) -> Import:
"""Generic method for creating imports.
Import type specific methods :meth:`library`, :meth:`resource` and
:meth:`variables` are recommended over this method.
"""
# RF 6.1 changed types to upper case. Code below adds backwards compatibility.
if args:
args = (args[0].upper(),) + args[1:]
elif 'type' in kwargs:
kwargs['type'] = kwargs['type'].upper()
return super().create(*args, **kwargs)
[docs]
class Variables(model.ItemList[Variable]):
def __init__(self, owner: ResourceFile, variables: Sequence[Variable] = ()):
super().__init__(Variable, {'owner': owner}, items=variables)
[docs]
class UserKeywords(model.ItemList[UserKeyword]):
def __init__(self, owner: ResourceFile, keywords: Sequence[UserKeyword] = ()):
self.invalidate_keyword_cache = owner.keyword_finder.invalidate_cache
self.invalidate_keyword_cache()
super().__init__(UserKeyword, {'owner': owner}, items=keywords)
[docs]
def append(self, item: 'UserKeyword|DataDict') -> UserKeyword:
self.invalidate_keyword_cache()
return super().append(item)
[docs]
def extend(self, items: 'Iterable[UserKeyword|DataDict]'):
self.invalidate_keyword_cache()
return super().extend(items)
def __setitem__(self, index: 'int|slice', item: 'Iterable[UserKeyword|DataDict]'):
self.invalidate_keyword_cache()
return super().__setitem__(index, item)
[docs]
def insert(self, index: int, item: 'UserKeyword|DataDict'):
self.invalidate_keyword_cache()
super().insert(index, item)
[docs]
def clear(self):
self.invalidate_keyword_cache()
super().clear()