Source code for robot.running.librarykeyword

#  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, 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 .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=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, execute_in_dry_run=dry_run) return LibraryKeywordRunner(self, languages=languages)
[docs] def resolve_arguments(self, args: Sequence[str], variables=None, languages=None) -> 'tuple[list, list]': resolve_args_until = self._resolve_args_until positional, named = self.args.resolve(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, arguments, variables=None, languages=None) -> 'tuple[list, list]': positional, named = super().resolve_arguments(arguments, 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] def get_tags(self) -> 'list[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] def get_tags(self) -> 'list[str]': tags = getattr(self.method, 'robot_tags', ()) if not is_list_like(tags): raise DataError(f"Expected tags to be list-like, got {type_name(tags)}.") return list(tags)
[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] def get_tags(self) -> 'list[str]': return GetKeywordTags(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 ''
[docs] def get_tags(self) -> 'list[str]': return []