# 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 inspect
from os.path import normpath
from pathlib import Path
from typing import Any, Callable, Generic, Mapping, Sequence, TypeVar, TYPE_CHECKING
from robot.model import Tags
from robot.errors import DataError
from robot.utils import (is_init, is_list_like, printable_name, split_tags_from_doc,
type_name)
from .arguments import ArgumentSpec, DynamicArgumentParser, PythonArgumentParser
from .dynamicmethods import (GetKeywordArguments, GetKeywordDocumentation,
GetKeywordTags, GetKeywordTypes, GetKeywordSource,
RunKeyword)
from .model import BodyItemParent, Keyword
from .keywordimplementation import KeywordImplementation
from .librarykeywordrunner import (EmbeddedArgumentsRunner, LibraryKeywordRunner,
RunKeywordRunner)
from .runkwregister import RUN_KW_REGISTER
if TYPE_CHECKING:
from robot.conf import LanguagesLike
from .testlibraries import DynamicLibrary, TestLibrary
Self = TypeVar('Self', bound='LibraryKeyword')
K = TypeVar('K', bound='LibraryKeyword')
[docs]
class LibraryKeyword(KeywordImplementation):
"""Base class for different library keywords."""
type = KeywordImplementation.LIBRARY_KEYWORD
owner: 'TestLibrary'
__slots__ = ['_resolve_args_until']
def __init__(self, owner: 'TestLibrary',
name: str = '',
args: 'ArgumentSpec|None' = None,
doc: str = '',
tags: 'Tags|Sequence[str]' = (),
resolve_args_until: 'int|None' = None,
parent: 'BodyItemParent|None' = None,
error: 'str|None' = None):
super().__init__(name, args, doc, tags, owner=owner, parent=parent, error=error)
self._resolve_args_until = resolve_args_until
@property
def method(self) -> Callable[..., Any]:
raise NotImplementedError
@property
def lineno(self) -> 'int|None':
method = self.method
try:
lines, start_lineno = inspect.getsourcelines(inspect.unwrap(method))
except (TypeError, OSError, IOError):
return None
for increment, line in enumerate(lines):
if line.strip().startswith('def '):
return start_lineno + increment
return start_lineno
[docs]
def create_runner(self, name: 'str|None',
languages: 'LanguagesLike' = None) -> LibraryKeywordRunner:
if self.embedded:
return EmbeddedArgumentsRunner(self, name)
if self._resolve_args_until is not None:
dry_run = RUN_KW_REGISTER.get_dry_run(self.owner.real_name, self.name)
return RunKeywordRunner(self, dry_run_children=dry_run)
return LibraryKeywordRunner(self, languages=languages)
[docs]
def resolve_arguments(self, args: 'Sequence[str|Any]',
named_args: 'Mapping[str, Any]|None' = None,
variables=None,
languages: 'LanguagesLike' = None) -> 'tuple[list, list]':
resolve_args_until = self._resolve_args_until
positional, named = self.args.resolve(args, named_args, variables,
self.owner.converters,
resolve_named=resolve_args_until is None,
resolve_args_until=resolve_args_until,
languages=languages)
if self.embedded:
self.embedded.validate(positional)
return positional, named
[docs]
def bind(self: Self, data: Keyword) -> Self:
return self.copy(parent=data.parent)
[docs]
def copy(self: Self, **attributes) -> Self:
raise NotImplementedError
[docs]
class StaticKeyword(LibraryKeyword):
"""Represents a keyword in a static library."""
__slots__ = ['method_name']
def __init__(self, method_name: str,
owner: 'TestLibrary',
name: str = '',
args: 'ArgumentSpec|None' = None,
doc: str = '',
tags: 'Tags|Sequence[str]' = (),
resolve_args_until: 'int|None' = None,
parent: 'BodyItemParent|None' = None,
error: 'str|None' = None):
super().__init__(owner, name, args, doc, tags, resolve_args_until, parent, error)
self.method_name = method_name
@property
def method(self) -> Callable[..., Any]:
"""Keyword method."""
return getattr(self.owner.instance, self.method_name)
@property
def source(self) -> 'Path|None':
# `getsourcefile` can return None and raise TypeError.
try:
if self.method is None:
raise TypeError
source = inspect.getsourcefile(inspect.unwrap(self.method))
except TypeError:
source = None
return Path(normpath(source)) if source else super().source
[docs]
@classmethod
def from_name(cls, name: str, owner: 'TestLibrary') -> 'StaticKeyword':
return StaticKeywordCreator(name, owner).create(method_name=name)
[docs]
def copy(self, **attributes) -> 'StaticKeyword':
return StaticKeyword(self.method_name, self.owner, self.name, self.args,
self._doc, self.tags, self._resolve_args_until,
self.parent, self.error).config(**attributes)
[docs]
class DynamicKeyword(LibraryKeyword):
"""Represents a keyword in a dynamic library."""
owner: 'DynamicLibrary'
__slots__ = ['run_keyword', '_orig_name', '__source_info']
def __init__(self, owner: 'DynamicLibrary',
name: str = '',
args: 'ArgumentSpec|None' = None,
doc: str = '',
tags: 'Tags|Sequence[str]' = (),
resolve_args_until: 'int|None' = None,
parent: 'BodyItemParent|None' = None,
error: 'str|None' = None):
# TODO: It would probably be better not to convert name we got from
# `get_keyword_names`. That would have some backwards incompatibility
# effects, but we can consider it in RF 8.0.
super().__init__(owner, printable_name(name, code_style=True), args, doc,
tags, resolve_args_until, parent, error)
self._orig_name = name
self.__source_info = None
@property
def method(self) -> Callable[..., Any]:
"""Dynamic ``run_keyword`` method."""
return RunKeyword(self.owner.instance, self._orig_name,
self.owner.supports_named_args)
@property
def source(self) -> 'Path|None':
return self._source_info[0] or super().source
@property
def lineno(self) -> 'int|None':
return self._source_info[1]
@property
def _source_info(self) -> 'tuple[Path|None, int]':
if not self.__source_info:
get_keyword_source = GetKeywordSource(self.owner.instance)
try:
source = get_keyword_source(self._orig_name)
except DataError as err:
source = None
self.owner.report_error(f"Getting source information for keyword "
f"'{self.name}' failed: {err}", err.details)
if source and ':' in source and source.rsplit(':', 1)[1].isdigit():
source, lineno = source.rsplit(':', 1)
lineno = int(lineno)
else:
lineno = None
self.__source_info = Path(normpath(source)) if source else None, lineno
return self.__source_info
[docs]
@classmethod
def from_name(cls, name: str, owner: 'DynamicLibrary') -> 'DynamicKeyword':
return DynamicKeywordCreator(name, owner).create()
[docs]
def resolve_arguments(self, args: 'Sequence[str|Any]',
named_args: 'Mapping[str, Any]|None' = None,
variables=None,
languages: 'LanguagesLike' = None) -> 'tuple[list, list]':
positional, named = super().resolve_arguments(args, named_args, variables,
languages)
if not self.owner.supports_named_args:
positional, named = self.args.map(positional, named)
return positional, named
[docs]
def copy(self, **attributes) -> 'DynamicKeyword':
return DynamicKeyword(self.owner, self._orig_name, self.args, self._doc,
self.tags, self._resolve_args_until, self.parent,
self.error).config(**attributes)
[docs]
class LibraryInit(LibraryKeyword):
"""Represents a library initializer.
:attr:`positional` and :attr:`named` contain arguments used for initializing
the library.
"""
def __init__(self, owner: 'TestLibrary',
name: str = '',
args: 'ArgumentSpec|None' = None,
doc: str = '',
tags: 'Tags|Sequence[str]' = (),
positional: 'list|None' = None,
named: 'dict|None' = None):
super().__init__(owner, name, args, doc, tags)
self.positional = positional or []
self.named = named or {}
@property
def doc(self) -> str:
from .testlibraries import DynamicLibrary
if isinstance(self.owner, DynamicLibrary):
doc = GetKeywordDocumentation(self.owner.instance)('__init__')
if doc:
return doc
return self._doc
@doc.setter
def doc(self, doc: str):
self._doc = doc
@property
def method(self) -> 'Callable[..., None]|None':
"""Initializer method.
``None`` with module based libraries and when class based libraries
do not have ``__init__``.
"""
return getattr(self.owner.instance, '__init__', None)
[docs]
@classmethod
def from_class(cls, klass) -> 'LibraryInit':
method = getattr(klass, '__init__', None)
return LibraryInitCreator(method).create()
[docs]
@classmethod
def null(cls) -> 'LibraryInit':
return LibraryInitCreator(None).create()
[docs]
def copy(self, **attributes) -> 'LibraryInit':
return LibraryInit(self.owner, self.name, self.args, self._doc, self.tags,
self.positional, self.named).config(**attributes)
[docs]
class KeywordCreator(Generic[K]):
keyword_class: 'type[K]'
def __init__(self, name: str, library: 'TestLibrary|None' = None):
self.name = name
self.library = library
self.extra = {}
if library and RUN_KW_REGISTER.is_run_keyword(library.real_name, name):
resolve_until = RUN_KW_REGISTER.get_args_to_process(library.real_name, name)
self.extra['resolve_args_until'] = resolve_until
@property
def instance(self) -> Any:
return self.library.instance
[docs]
def create(self, **extra) -> K:
tags = self.get_tags()
doc, doc_tags = split_tags_from_doc(self.get_doc())
kw = self.keyword_class(
owner=self.library,
name=self.get_name(),
args=self.get_args(),
doc=doc,
tags=tags + doc_tags,
**self.extra,
**extra
)
kw.args.name = lambda: kw.full_name
return kw
[docs]
def get_name(self) -> str:
raise NotImplementedError
[docs]
def get_args(self) -> ArgumentSpec:
raise NotImplementedError
[docs]
def get_doc(self) -> str:
raise NotImplementedError
[docs]
class StaticKeywordCreator(KeywordCreator[StaticKeyword]):
keyword_class = StaticKeyword
def __init__(self, name: str, library: 'TestLibrary'):
super().__init__(name, library)
self.method = getattr(library.instance, name)
[docs]
def get_name(self) -> str:
robot_name = getattr(self.method, 'robot_name', None)
name = robot_name or printable_name(self.name, code_style=True)
if not name:
raise DataError('Keyword name cannot be empty.')
return name
[docs]
def get_args(self) -> ArgumentSpec:
return PythonArgumentParser().parse(self.method)
[docs]
def get_doc(self) -> str:
return inspect.getdoc(self.method) or ''
[docs]
class DynamicKeywordCreator(KeywordCreator[DynamicKeyword]):
keyword_class = DynamicKeyword
library: 'DynamicLibrary'
[docs]
def get_name(self) -> str:
return self.name
[docs]
def get_args(self) -> ArgumentSpec:
supports_named_args = self.library.supports_named_args
get_keyword_arguments = GetKeywordArguments(self.instance, supports_named_args)
spec = DynamicArgumentParser().parse(get_keyword_arguments(self.name))
if not supports_named_args:
name = RunKeyword(self.instance).name
if spec.named_only:
raise DataError(f"Too few '{name}' method parameters to support "
f"named-only arguments.")
if spec.var_named:
raise DataError(f"Too few '{name}' method parameters to support "
f"free named arguments.")
types = GetKeywordTypes(self.instance)(self.name)
if isinstance(types, dict) and 'return' in types:
spec.return_type = types.pop('return')
spec.types = types
return spec
[docs]
def get_doc(self) -> str:
return GetKeywordDocumentation(self.instance)(self.name)
[docs]
class LibraryInitCreator(KeywordCreator[LibraryInit]):
keyword_class = LibraryInit
def __init__(self, method: 'Callable[..., None]|None'):
super().__init__('__init__')
self.method = method if is_init(method) else lambda: None
[docs]
def create(self, **extra) -> LibraryInit:
init = super().create(**extra)
init.args.name = lambda: init.owner.name
return init
[docs]
def get_name(self) -> str:
return self.name
[docs]
def get_args(self) -> ArgumentSpec:
return PythonArgumentParser('Library').parse(self.method)
[docs]
def get_doc(self) -> str:
return inspect.getdoc(self.method) or ''