# 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, TYPE_CHECKING, TypeVar
from robot.errors import DataError
from robot.model import Tags
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, GetKeywordSource, GetKeywordTags,
GetKeywordTypes, RunKeyword
)
from .keywordimplementation import KeywordImplementation
from .librarykeywordrunner import (
EmbeddedArgumentsRunner, LibraryKeywordRunner, RunKeywordRunner
)
from .model import BodyItemParent, Keyword
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 '{self.name}' "
f"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
prefix = f"Too few '{name}' method parameters to support "
if spec.named_only:
raise DataError(prefix + "named-only arguments.")
if spec.var_named:
raise DataError(prefix + "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 ""