robotframework-pythonlibcore
Advanced tools
| # Copyright 2017- 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. | ||
| """Generic test library core for Robot Framework. | ||
| Main usage is easing creating larger test libraries. For more information and | ||
| examples see the project pages at | ||
| https://github.com/robotframework/PythonLibCore | ||
| """ | ||
| from robot.api.deco import keyword | ||
| from robotlibcore.core import DynamicCore, HybridCore | ||
| from robotlibcore.keywords import KeywordBuilder, KeywordSpecification | ||
| from robotlibcore.plugin import PluginParser | ||
| from robotlibcore.utils import Module, NoKeywordFound, PluginError, PythonLibCoreException | ||
| __version__ = "4.5.0" | ||
| __all__ = [ | ||
| "DynamicCore", | ||
| "HybridCore", | ||
| "KeywordBuilder", | ||
| "KeywordSpecification", | ||
| "Module", | ||
| "NoKeywordFound", | ||
| "PluginError", | ||
| "PluginParser", | ||
| "PythonLibCoreException", | ||
| "keyword", | ||
| ] |
| # Copyright 2017- 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 .dynamic import DynamicCore | ||
| from .hybrid import HybridCore | ||
| __all__ = ["DynamicCore", "HybridCore"] |
| # Copyright 2017- 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 | ||
| import os | ||
| from robotlibcore.utils import NoKeywordFound | ||
| from .hybrid import HybridCore | ||
| class DynamicCore(HybridCore): | ||
| def run_keyword(self, name, args, kwargs=None): | ||
| return self.keywords[name](*args, **(kwargs or {})) | ||
| def get_keyword_arguments(self, name): | ||
| spec = self.keywords_spec.get(name) | ||
| if not spec: | ||
| msg = f"Could not find keyword: {name}" | ||
| raise NoKeywordFound(msg) | ||
| return spec.argument_specification | ||
| def get_keyword_tags(self, name): | ||
| return self.keywords[name].robot_tags | ||
| def get_keyword_documentation(self, name): | ||
| if name == "__intro__": | ||
| return inspect.getdoc(self) or "" | ||
| spec = self.keywords_spec.get(name) | ||
| if not spec: | ||
| msg = f"Could not find keyword: {name}" | ||
| raise NoKeywordFound(msg) | ||
| return spec.documentation | ||
| def get_keyword_types(self, name): | ||
| spec = self.keywords_spec.get(name) | ||
| if spec is None: | ||
| raise ValueError('Keyword "%s" not found.' % name) | ||
| return spec.argument_types | ||
| def __get_keyword(self, keyword_name): | ||
| if keyword_name == "__init__": | ||
| return self.__init__ # type: ignore | ||
| if keyword_name.startswith("__") and keyword_name.endswith("__"): | ||
| return None | ||
| method = self.keywords.get(keyword_name) | ||
| if not method: | ||
| raise ValueError('Keyword "%s" not found.' % keyword_name) | ||
| return method | ||
| def get_keyword_source(self, keyword_name): | ||
| method = self.__get_keyword(keyword_name) | ||
| path = self.__get_keyword_path(method) | ||
| line_number = self.__get_keyword_line(method) | ||
| if path and line_number: | ||
| return "{}:{}".format(path, line_number) | ||
| if path: | ||
| return path | ||
| if line_number: | ||
| return ":%s" % line_number | ||
| return None | ||
| def __get_keyword_line(self, method): | ||
| try: | ||
| lines, line_number = inspect.getsourcelines(method) | ||
| except (OSError, TypeError): | ||
| return None | ||
| for increment, line in enumerate(lines): | ||
| if line.strip().startswith("def "): | ||
| return line_number + increment | ||
| return line_number | ||
| def __get_keyword_path(self, method): | ||
| try: | ||
| return os.path.normpath(inspect.getfile(inspect.unwrap(method))) | ||
| except TypeError: | ||
| return None |
| # Copyright 2017- 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 pathlib import Path | ||
| from typing import Callable, List, Optional | ||
| from robotlibcore.keywords import KeywordBuilder | ||
| from robotlibcore.utils import _translated_keywords, _translation | ||
| class HybridCore: | ||
| def __init__(self, library_components: List, translation: Optional[Path] = None) -> None: | ||
| self.keywords = {} | ||
| self.keywords_spec = {} | ||
| self.attributes = {} | ||
| translation_data = _translation(translation) | ||
| translated_kw_names = _translated_keywords(translation_data) | ||
| self.add_library_components(library_components, translation_data, translated_kw_names) | ||
| self.add_library_components([self], translation_data, translated_kw_names) | ||
| self.__set_library_listeners(library_components) | ||
| def add_library_components( | ||
| self, | ||
| library_components: List, | ||
| translation: Optional[dict] = None, | ||
| translated_kw_names: Optional[list] = None, | ||
| ): | ||
| translation = translation if translation else {} | ||
| translated_kw_names = translated_kw_names if translated_kw_names else [] | ||
| self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__, translation) # type: ignore | ||
| self.__replace_intro_doc(translation) | ||
| for component in library_components: | ||
| for name, func in self.__get_members(component): | ||
| if callable(func) and hasattr(func, "robot_name"): | ||
| kw = getattr(component, name) | ||
| kw_name = self.__get_keyword_name(func, name, translation, translated_kw_names) | ||
| self.keywords[kw_name] = kw | ||
| self.keywords_spec[kw_name] = KeywordBuilder.build(kw, translation) | ||
| # Expose keywords as attributes both using original | ||
| # method names as well as possible custom names. | ||
| self.attributes[name] = self.attributes[kw_name] = kw | ||
| def __get_keyword_name(self, func: Callable, name: str, translation: dict, translated_kw_names: list): | ||
| if name in translated_kw_names: | ||
| return name | ||
| if name in translation and translation[name].get("name"): | ||
| return translation[name].get("name") | ||
| return func.robot_name or name | ||
| def __replace_intro_doc(self, translation: dict): | ||
| if "__intro__" in translation: | ||
| self.__doc__ = translation["__intro__"].get("doc", "") | ||
| def __set_library_listeners(self, library_components: list): | ||
| listeners = self.__get_manually_registered_listeners() | ||
| listeners.extend(self.__get_component_listeners([self, *library_components])) | ||
| if listeners: | ||
| self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners)) | ||
| def __get_manually_registered_listeners(self) -> list: | ||
| manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER", []) | ||
| try: | ||
| return [*manually_registered_listener] | ||
| except TypeError: | ||
| return [manually_registered_listener] | ||
| def __get_component_listeners(self, library_listeners: list) -> list: | ||
| return [component for component in library_listeners if hasattr(component, "ROBOT_LISTENER_API_VERSION")] | ||
| def __get_members(self, component): | ||
| if inspect.ismodule(component): | ||
| return inspect.getmembers(component) | ||
| if inspect.isclass(component): | ||
| msg = f"Libraries must be modules or instances, got class '{component.__name__}' instead." | ||
| raise TypeError( | ||
| msg, | ||
| ) | ||
| if type(component) != component.__class__: # noqa: E721 | ||
| msg = ( | ||
| "Libraries must be modules or new-style class instances, " | ||
| f"got old-style class {component.__class__.__name__} instead." | ||
| ) | ||
| raise TypeError( | ||
| msg, | ||
| ) | ||
| return self.__get_members_from_instance(component) | ||
| def __get_members_from_instance(self, instance): | ||
| # Avoid calling properties by getting members from class, not instance. | ||
| cls = type(instance) | ||
| for name in dir(instance): | ||
| owner = cls if hasattr(cls, name) else instance | ||
| yield name, getattr(owner, name) | ||
| def __getattr__(self, name): | ||
| if name == "attributes": | ||
| return super().__getattribute__(name) | ||
| if name in self.attributes: | ||
| return self.attributes[name] | ||
| msg = "{!r} object has no attribute {!r}".format(type(self).__name__, name) | ||
| raise AttributeError( | ||
| msg, | ||
| ) | ||
| def __dir__(self): | ||
| my_attrs = super().__dir__() | ||
| return sorted(set(my_attrs) | set(self.attributes)) | ||
| def get_keyword_names(self): | ||
| return sorted(self.keywords) |
| # Copyright 2017- 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 .builder import KeywordBuilder | ||
| from .specification import KeywordSpecification | ||
| __all__ = ["KeywordBuilder", "KeywordSpecification"] |
| # Copyright 2017- 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 typing import Callable, Optional, get_type_hints | ||
| from .specification import KeywordSpecification | ||
| class KeywordBuilder: | ||
| @classmethod | ||
| def build(cls, function, translation: Optional[dict] = None): | ||
| translation = translation if translation else {} | ||
| return KeywordSpecification( | ||
| argument_specification=cls._get_arguments(function), | ||
| documentation=cls.get_doc(function, translation), | ||
| argument_types=cls._get_types(function), | ||
| ) | ||
| @classmethod | ||
| def get_doc(cls, function, translation: dict): | ||
| if kw := cls._get_kw_transtation(function, translation): # noqa: SIM102 | ||
| if "doc" in kw: | ||
| return kw["doc"] | ||
| return inspect.getdoc(function) or "" | ||
| @classmethod | ||
| def _get_kw_transtation(cls, function, translation: dict): | ||
| return translation.get(function.__name__, {}) | ||
| @classmethod | ||
| def unwrap(cls, function): | ||
| return inspect.unwrap(function) | ||
| @classmethod | ||
| def _get_arguments(cls, function): | ||
| unwrap_function = cls.unwrap(function) | ||
| arg_spec = cls._get_arg_spec(unwrap_function) | ||
| argument_specification = cls._get_args(arg_spec, function) | ||
| argument_specification.extend(cls._get_varargs(arg_spec)) | ||
| argument_specification.extend(cls._get_named_only_args(arg_spec)) | ||
| argument_specification.extend(cls._get_kwargs(arg_spec)) | ||
| return argument_specification | ||
| @classmethod | ||
| def _get_arg_spec(cls, function: Callable) -> inspect.FullArgSpec: | ||
| return inspect.getfullargspec(function) | ||
| @classmethod | ||
| def _get_type_hint(cls, function: Callable): | ||
| try: | ||
| hints = get_type_hints(function) | ||
| except Exception: # noqa: BLE001 | ||
| hints = function.__annotations__ | ||
| return hints | ||
| @classmethod | ||
| def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable) -> list: | ||
| args = cls._drop_self_from_args(function, arg_spec) | ||
| args.reverse() | ||
| defaults = list(arg_spec.defaults) if arg_spec.defaults else [] | ||
| formated_args = [] | ||
| for arg in args: | ||
| if defaults: | ||
| formated_args.append((arg, defaults.pop())) | ||
| else: | ||
| formated_args.append(arg) | ||
| formated_args.reverse() | ||
| return formated_args | ||
| @classmethod | ||
| def _drop_self_from_args( | ||
| cls, | ||
| function: Callable, | ||
| arg_spec: inspect.FullArgSpec, | ||
| ) -> list: | ||
| return arg_spec.args[1:] if inspect.ismethod(function) else arg_spec.args | ||
| @classmethod | ||
| def _get_varargs(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| return [f"*{arg_spec.varargs}"] if arg_spec.varargs else [] | ||
| @classmethod | ||
| def _get_kwargs(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| return [f"**{arg_spec.varkw}"] if arg_spec.varkw else [] | ||
| @classmethod | ||
| def _get_named_only_args(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| rf_spec: list = [] | ||
| kw_only_args = arg_spec.kwonlyargs if arg_spec.kwonlyargs else [] | ||
| if not arg_spec.varargs and kw_only_args: | ||
| rf_spec.append("*") | ||
| kw_only_defaults = arg_spec.kwonlydefaults if arg_spec.kwonlydefaults else {} | ||
| for kw_only_arg in kw_only_args: | ||
| if kw_only_arg in kw_only_defaults: | ||
| rf_spec.append((kw_only_arg, kw_only_defaults[kw_only_arg])) | ||
| else: | ||
| rf_spec.append(kw_only_arg) | ||
| return rf_spec | ||
| @classmethod | ||
| def _get_types(cls, function): | ||
| if function is None: | ||
| return function | ||
| types = getattr(function, "robot_types", ()) | ||
| if types is None or types: | ||
| return types | ||
| return cls._get_typing_hints(function) | ||
| @classmethod | ||
| def _get_typing_hints(cls, function): | ||
| function = cls.unwrap(function) | ||
| hints = cls._get_type_hint(function) | ||
| arg_spec = cls._get_arg_spec(function) | ||
| all_args = cls._args_as_list(function, arg_spec) | ||
| for arg_with_hint in list(hints): | ||
| # remove self statements | ||
| if arg_with_hint not in [*all_args, "return"]: | ||
| hints.pop(arg_with_hint) | ||
| return hints | ||
| @classmethod | ||
| def _args_as_list(cls, function, arg_spec) -> list: | ||
| function_args = cls._drop_self_from_args(function, arg_spec) | ||
| if arg_spec.varargs: | ||
| function_args.append(arg_spec.varargs) | ||
| function_args.extend(arg_spec.kwonlyargs or []) | ||
| if arg_spec.varkw: | ||
| function_args.append(arg_spec.varkw) | ||
| return function_args | ||
| @classmethod | ||
| def _get_defaults(cls, arg_spec): | ||
| if not arg_spec.defaults: | ||
| return {} | ||
| names = arg_spec.args[-len(arg_spec.defaults) :] | ||
| return zip(names, arg_spec.defaults) |
| # Copyright 2017- 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. | ||
| class KeywordSpecification: | ||
| def __init__( | ||
| self, | ||
| argument_specification=None, | ||
| documentation=None, | ||
| argument_types=None, | ||
| ) -> None: | ||
| self.argument_specification = argument_specification | ||
| self.documentation = documentation | ||
| self.argument_types = argument_types |
| # Copyright 2017- 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 .parser import PluginParser | ||
| __all__ = ["PluginParser"] |
| # Copyright 2017- 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 typing import Any, List, Optional, Union | ||
| from robot.errors import DataError | ||
| from robot.utils import Importer | ||
| from robotlibcore.core import DynamicCore | ||
| from robotlibcore.utils import Module, PluginError | ||
| class PluginParser: | ||
| def __init__(self, base_class: Optional[Any] = None, python_object=None) -> None: | ||
| self._base_class = base_class | ||
| self._python_object = python_object if python_object else [] | ||
| def parse_plugins(self, plugins: Union[str, List[str]]) -> List: | ||
| imported_plugins = [] | ||
| importer = Importer("test library") | ||
| for parsed_plugin in self._string_to_modules(plugins): | ||
| plugin = importer.import_class_or_module(parsed_plugin.module) | ||
| if not inspect.isclass(plugin): | ||
| message = f"Importing test library: '{parsed_plugin.module}' failed." | ||
| raise DataError(message) | ||
| args = self._python_object + parsed_plugin.args | ||
| plugin = plugin(*args, **parsed_plugin.kw_args) | ||
| if self._base_class and not isinstance(plugin, self._base_class): | ||
| message = f"Plugin does not inherit {self._base_class}" | ||
| raise PluginError(message) | ||
| imported_plugins.append(plugin) | ||
| return imported_plugins | ||
| def get_plugin_keywords(self, plugins: List): | ||
| return DynamicCore(plugins).get_keyword_names() | ||
| def _string_to_modules(self, modules: Union[str, List[str]]): | ||
| parsed_modules: list = [] | ||
| if not modules: | ||
| return parsed_modules | ||
| for module in self._modules_splitter(modules): | ||
| module_and_args = module.strip().split(";") | ||
| module_name = module_and_args.pop(0) | ||
| kw_args = {} | ||
| args = [] | ||
| for argument in module_and_args: | ||
| if "=" in argument: | ||
| key, value = argument.split("=") | ||
| kw_args[key] = value | ||
| else: | ||
| args.append(argument) | ||
| parsed_modules.append(Module(module=module_name, args=args, kw_args=kw_args)) | ||
| return parsed_modules | ||
| def _modules_splitter(self, modules: Union[str, List[str]]): | ||
| if isinstance(modules, str): | ||
| for module in modules.split(","): | ||
| yield module | ||
| else: | ||
| for module in modules: | ||
| yield module |
| # Copyright 2017- 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 dataclasses import dataclass | ||
| from .exceptions import NoKeywordFound, PluginError, PythonLibCoreException | ||
| from .translations import _translated_keywords, _translation | ||
| @dataclass | ||
| class Module: | ||
| module: str | ||
| args: list | ||
| kw_args: dict | ||
| __all__ = ["Module", "NoKeywordFound", "PluginError", "PythonLibCoreException", "_translated_keywords", "_translation"] |
| # Copyright 2017- 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. | ||
| class PythonLibCoreException(Exception): # noqa: N818 | ||
| pass | ||
| class PluginError(PythonLibCoreException): | ||
| pass | ||
| class NoKeywordFound(PythonLibCoreException): | ||
| pass |
| # Copyright 2017- 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 json | ||
| from pathlib import Path | ||
| from typing import Optional | ||
| from robot.api import logger | ||
| def _translation(translation: Optional[Path] = None): | ||
| if translation and isinstance(translation, Path) and translation.is_file(): | ||
| with translation.open("r") as file: | ||
| try: | ||
| return json.load(file) | ||
| except json.decoder.JSONDecodeError: | ||
| logger.warn(f"Could not convert json file {translation} to dictionary.") | ||
| return {} | ||
| else: | ||
| return {} | ||
| def _translated_keywords(translation_data: dict) -> list: | ||
| return [item.get("name") for item in translation_data.values() if item.get("name")] |
+21
-2
@@ -1,4 +0,4 @@ | ||
| Metadata-Version: 2.1 | ||
| Metadata-Version: 2.4 | ||
| Name: robotframework-pythonlibcore | ||
| Version: 4.4.1 | ||
| Version: 4.5.0 | ||
| Summary: Tools to ease creating larger test libraries for Robot Framework using Python. | ||
@@ -27,2 +27,14 @@ Home-page: https://github.com/robotframework/PythonLibCore | ||
| License-File: LICENSE.txt | ||
| Dynamic: author | ||
| Dynamic: author-email | ||
| Dynamic: classifier | ||
| Dynamic: description | ||
| Dynamic: description-content-type | ||
| Dynamic: home-page | ||
| Dynamic: keywords | ||
| Dynamic: license | ||
| Dynamic: license-file | ||
| Dynamic: platform | ||
| Dynamic: requires-python | ||
| Dynamic: summary | ||
@@ -82,2 +94,9 @@ # Python Library Core | ||
| ## Installation | ||
| To install this library, run the following command in your terminal: | ||
| ``` bash | ||
| pip install robotframework-pythonlibcore | ||
| ``` | ||
| This command installs the latest version of `robotframework-pythonlibcore`, ensuring you have all the current features and updates. | ||
| # Example | ||
@@ -84,0 +103,0 @@ |
+7
-0
@@ -54,2 +54,9 @@ # Python Library Core | ||
| ## Installation | ||
| To install this library, run the following command in your terminal: | ||
| ``` bash | ||
| pip install robotframework-pythonlibcore | ||
| ``` | ||
| This command installs the latest version of `robotframework-pythonlibcore`, ensuring you have all the current features and updates. | ||
| # Example | ||
@@ -56,0 +63,0 @@ |
+9
-8
| #!/usr/bin/env python | ||
| import re | ||
| from os.path import abspath, dirname, join | ||
| from pathlib import Path | ||
| from os.path import join | ||
| from setuptools import find_packages, setup | ||
| CURDIR = dirname(abspath(__file__)) | ||
| CURDIR = Path(__file__).parent | ||
@@ -24,7 +25,8 @@ CLASSIFIERS = """ | ||
| """.strip().splitlines() | ||
| with open(join(CURDIR, 'src', 'robotlibcore.py')) as f: | ||
| VERSION = re.search('\n__version__ = "(.*)"', f.read()).group(1) | ||
| with open(join(CURDIR, 'README.md')) as f: | ||
| LONG_DESCRIPTION = f.read() | ||
| version_file = Path(CURDIR / 'src' / 'robotlibcore' / '__init__.py') | ||
| VERSION = re.search('\n__version__ = "(.*)"', version_file.read_text()).group(1) | ||
| LONG_DESCRIPTION = Path(CURDIR / 'README.md').read_text() | ||
| DESCRIPTION = ('Tools to ease creating larger test libraries for ' | ||
@@ -47,4 +49,3 @@ 'Robot Framework using Python.') | ||
| package_dir = {'': 'src'}, | ||
| packages = find_packages('src'), | ||
| py_modules = ['robotlibcore'], | ||
| packages = ["robotlibcore","robotlibcore.core", "robotlibcore.keywords", "robotlibcore.plugin", "robotlibcore.utils"] | ||
| ) |
@@ -1,4 +0,4 @@ | ||
| Metadata-Version: 2.1 | ||
| Metadata-Version: 2.4 | ||
| Name: robotframework-pythonlibcore | ||
| Version: 4.4.1 | ||
| Version: 4.5.0 | ||
| Summary: Tools to ease creating larger test libraries for Robot Framework using Python. | ||
@@ -27,2 +27,14 @@ Home-page: https://github.com/robotframework/PythonLibCore | ||
| License-File: LICENSE.txt | ||
| Dynamic: author | ||
| Dynamic: author-email | ||
| Dynamic: classifier | ||
| Dynamic: description | ||
| Dynamic: description-content-type | ||
| Dynamic: home-page | ||
| Dynamic: keywords | ||
| Dynamic: license | ||
| Dynamic: license-file | ||
| Dynamic: platform | ||
| Dynamic: requires-python | ||
| Dynamic: summary | ||
@@ -82,2 +94,9 @@ # Python Library Core | ||
| ## Installation | ||
| To install this library, run the following command in your terminal: | ||
| ``` bash | ||
| pip install robotframework-pythonlibcore | ||
| ``` | ||
| This command installs the latest version of `robotframework-pythonlibcore`, ensuring you have all the current features and updates. | ||
| # Example | ||
@@ -84,0 +103,0 @@ |
@@ -7,6 +7,17 @@ COPYRIGHT.txt | ||
| setup.py | ||
| src/robotlibcore.py | ||
| src/robotframework_pythonlibcore.egg-info/PKG-INFO | ||
| src/robotframework_pythonlibcore.egg-info/SOURCES.txt | ||
| src/robotframework_pythonlibcore.egg-info/dependency_links.txt | ||
| src/robotframework_pythonlibcore.egg-info/top_level.txt | ||
| src/robotframework_pythonlibcore.egg-info/top_level.txt | ||
| src/robotlibcore/__init__.py | ||
| src/robotlibcore/core/__init__.py | ||
| src/robotlibcore/core/dynamic.py | ||
| src/robotlibcore/core/hybrid.py | ||
| src/robotlibcore/keywords/__init__.py | ||
| src/robotlibcore/keywords/builder.py | ||
| src/robotlibcore/keywords/specification.py | ||
| src/robotlibcore/plugin/__init__.py | ||
| src/robotlibcore/plugin/parser.py | ||
| src/robotlibcore/utils/__init__.py | ||
| src/robotlibcore/utils/exceptions.py | ||
| src/robotlibcore/utils/translations.py |
| # Copyright 2017- 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. | ||
| """Generic test library core for Robot Framework. | ||
| Main usage is easing creating larger test libraries. For more information and | ||
| examples see the project pages at | ||
| https://github.com/robotframework/PythonLibCore | ||
| """ | ||
| import inspect | ||
| import json | ||
| import os | ||
| from dataclasses import dataclass | ||
| from pathlib import Path | ||
| from typing import Any, Callable, List, Optional, Union, get_type_hints | ||
| from robot.api import logger | ||
| from robot.api.deco import keyword # noqa: F401 | ||
| from robot.errors import DataError | ||
| from robot.utils import Importer | ||
| __version__ = "4.4.1" | ||
| class PythonLibCoreException(Exception): # noqa: N818 | ||
| pass | ||
| class PluginError(PythonLibCoreException): | ||
| pass | ||
| class NoKeywordFound(PythonLibCoreException): | ||
| pass | ||
| def _translation(translation: Optional[Path] = None): | ||
| if translation and isinstance(translation, Path) and translation.is_file(): | ||
| with translation.open("r") as file: | ||
| try: | ||
| return json.load(file) | ||
| except json.decoder.JSONDecodeError: | ||
| logger.warn(f"Could not convert json file {translation} to dictionary.") | ||
| return {} | ||
| else: | ||
| return {} | ||
| def _translated_keywords(translation_data: dict) -> list: | ||
| return [item.get("name") for item in translation_data.values() if item.get("name")] | ||
| class HybridCore: | ||
| def __init__(self, library_components: List, translation: Optional[Path] = None) -> None: | ||
| self.keywords = {} | ||
| self.keywords_spec = {} | ||
| self.attributes = {} | ||
| translation_data = _translation(translation) | ||
| translated_kw_names = _translated_keywords(translation_data) | ||
| self.add_library_components(library_components, translation_data, translated_kw_names) | ||
| self.add_library_components([self], translation_data, translated_kw_names) | ||
| self.__set_library_listeners(library_components) | ||
| def add_library_components( | ||
| self, | ||
| library_components: List, | ||
| translation: Optional[dict] = None, | ||
| translated_kw_names: Optional[list] = None, | ||
| ): | ||
| translation = translation if translation else {} | ||
| translated_kw_names = translated_kw_names if translated_kw_names else [] | ||
| self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__, translation) # type: ignore | ||
| self.__replace_intro_doc(translation) | ||
| for component in library_components: | ||
| for name, func in self.__get_members(component): | ||
| if callable(func) and hasattr(func, "robot_name"): | ||
| kw = getattr(component, name) | ||
| kw_name = self.__get_keyword_name(func, name, translation, translated_kw_names) | ||
| self.keywords[kw_name] = kw | ||
| self.keywords_spec[kw_name] = KeywordBuilder.build(kw, translation) | ||
| # Expose keywords as attributes both using original | ||
| # method names as well as possible custom names. | ||
| self.attributes[name] = self.attributes[kw_name] = kw | ||
| def __get_keyword_name(self, func: Callable, name: str, translation: dict, translated_kw_names: list): | ||
| if name in translated_kw_names: | ||
| return name | ||
| if name in translation and translation[name].get("name"): | ||
| return translation[name].get("name") | ||
| return func.robot_name or name | ||
| def __replace_intro_doc(self, translation: dict): | ||
| if "__intro__" in translation: | ||
| self.__doc__ = translation["__intro__"].get("doc", "") | ||
| def __set_library_listeners(self, library_components: list): | ||
| listeners = self.__get_manually_registered_listeners() | ||
| listeners.extend(self.__get_component_listeners([self, *library_components])) | ||
| if listeners: | ||
| self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners)) | ||
| def __get_manually_registered_listeners(self) -> list: | ||
| manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER", []) | ||
| try: | ||
| return [*manually_registered_listener] | ||
| except TypeError: | ||
| return [manually_registered_listener] | ||
| def __get_component_listeners(self, library_listeners: list) -> list: | ||
| return [component for component in library_listeners if hasattr(component, "ROBOT_LISTENER_API_VERSION")] | ||
| def __get_members(self, component): | ||
| if inspect.ismodule(component): | ||
| return inspect.getmembers(component) | ||
| if inspect.isclass(component): | ||
| msg = f"Libraries must be modules or instances, got class '{component.__name__}' instead." | ||
| raise TypeError( | ||
| msg, | ||
| ) | ||
| if type(component) != component.__class__: | ||
| msg = ( | ||
| "Libraries must be modules or new-style class instances, " | ||
| f"got old-style class {component.__class__.__name__} instead." | ||
| ) | ||
| raise TypeError( | ||
| msg, | ||
| ) | ||
| return self.__get_members_from_instance(component) | ||
| def __get_members_from_instance(self, instance): | ||
| # Avoid calling properties by getting members from class, not instance. | ||
| cls = type(instance) | ||
| for name in dir(instance): | ||
| owner = cls if hasattr(cls, name) else instance | ||
| yield name, getattr(owner, name) | ||
| def __getattr__(self, name): | ||
| if name in self.attributes: | ||
| return self.attributes[name] | ||
| msg = "{!r} object has no attribute {!r}".format(type(self).__name__, name) | ||
| raise AttributeError( | ||
| msg, | ||
| ) | ||
| def __dir__(self): | ||
| my_attrs = super().__dir__() | ||
| return sorted(set(my_attrs) | set(self.attributes)) | ||
| def get_keyword_names(self): | ||
| return sorted(self.keywords) | ||
| @dataclass | ||
| class Module: | ||
| module: str | ||
| args: list | ||
| kw_args: dict | ||
| class DynamicCore(HybridCore): | ||
| def run_keyword(self, name, args, kwargs=None): | ||
| return self.keywords[name](*args, **(kwargs or {})) | ||
| def get_keyword_arguments(self, name): | ||
| spec = self.keywords_spec.get(name) | ||
| if not spec: | ||
| msg = f"Could not find keyword: {name}" | ||
| raise NoKeywordFound(msg) | ||
| return spec.argument_specification | ||
| def get_keyword_tags(self, name): | ||
| return self.keywords[name].robot_tags | ||
| def get_keyword_documentation(self, name): | ||
| if name == "__intro__": | ||
| return inspect.getdoc(self) or "" | ||
| spec = self.keywords_spec.get(name) | ||
| if not spec: | ||
| msg = f"Could not find keyword: {name}" | ||
| raise NoKeywordFound(msg) | ||
| return spec.documentation | ||
| def get_keyword_types(self, name): | ||
| spec = self.keywords_spec.get(name) | ||
| if spec is None: | ||
| raise ValueError('Keyword "%s" not found.' % name) | ||
| return spec.argument_types | ||
| def __get_keyword(self, keyword_name): | ||
| if keyword_name == "__init__": | ||
| return self.__init__ # type: ignore | ||
| if keyword_name.startswith("__") and keyword_name.endswith("__"): | ||
| return None | ||
| method = self.keywords.get(keyword_name) | ||
| if not method: | ||
| raise ValueError('Keyword "%s" not found.' % keyword_name) | ||
| return method | ||
| def get_keyword_source(self, keyword_name): | ||
| method = self.__get_keyword(keyword_name) | ||
| path = self.__get_keyword_path(method) | ||
| line_number = self.__get_keyword_line(method) | ||
| if path and line_number: | ||
| return "{}:{}".format(path, line_number) | ||
| if path: | ||
| return path | ||
| if line_number: | ||
| return ":%s" % line_number | ||
| return None | ||
| def __get_keyword_line(self, method): | ||
| try: | ||
| lines, line_number = inspect.getsourcelines(method) | ||
| except (OSError, TypeError): | ||
| return None | ||
| for increment, line in enumerate(lines): | ||
| if line.strip().startswith("def "): | ||
| return line_number + increment | ||
| return line_number | ||
| def __get_keyword_path(self, method): | ||
| try: | ||
| return os.path.normpath(inspect.getfile(inspect.unwrap(method))) | ||
| except TypeError: | ||
| return None | ||
| class KeywordBuilder: | ||
| @classmethod | ||
| def build(cls, function, translation: Optional[dict] = None): | ||
| translation = translation if translation else {} | ||
| return KeywordSpecification( | ||
| argument_specification=cls._get_arguments(function), | ||
| documentation=cls.get_doc(function, translation), | ||
| argument_types=cls._get_types(function), | ||
| ) | ||
| @classmethod | ||
| def get_doc(cls, function, translation: dict): | ||
| if kw := cls._get_kw_transtation(function, translation): # noqa: SIM102 | ||
| if "doc" in kw: | ||
| return kw["doc"] | ||
| return inspect.getdoc(function) or "" | ||
| @classmethod | ||
| def _get_kw_transtation(cls, function, translation: dict): | ||
| return translation.get(function.__name__, {}) | ||
| @classmethod | ||
| def unwrap(cls, function): | ||
| return inspect.unwrap(function) | ||
| @classmethod | ||
| def _get_arguments(cls, function): | ||
| unwrap_function = cls.unwrap(function) | ||
| arg_spec = cls._get_arg_spec(unwrap_function) | ||
| argument_specification = cls._get_args(arg_spec, function) | ||
| argument_specification.extend(cls._get_varargs(arg_spec)) | ||
| argument_specification.extend(cls._get_named_only_args(arg_spec)) | ||
| argument_specification.extend(cls._get_kwargs(arg_spec)) | ||
| return argument_specification | ||
| @classmethod | ||
| def _get_arg_spec(cls, function: Callable) -> inspect.FullArgSpec: | ||
| return inspect.getfullargspec(function) | ||
| @classmethod | ||
| def _get_type_hint(cls, function: Callable): | ||
| try: | ||
| hints = get_type_hints(function) | ||
| except Exception: # noqa: BLE001 | ||
| hints = function.__annotations__ | ||
| return hints | ||
| @classmethod | ||
| def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable) -> list: | ||
| args = cls._drop_self_from_args(function, arg_spec) | ||
| args.reverse() | ||
| defaults = list(arg_spec.defaults) if arg_spec.defaults else [] | ||
| formated_args = [] | ||
| for arg in args: | ||
| if defaults: | ||
| formated_args.append((arg, defaults.pop())) | ||
| else: | ||
| formated_args.append(arg) | ||
| formated_args.reverse() | ||
| return formated_args | ||
| @classmethod | ||
| def _drop_self_from_args( | ||
| cls, | ||
| function: Callable, | ||
| arg_spec: inspect.FullArgSpec, | ||
| ) -> list: | ||
| return arg_spec.args[1:] if inspect.ismethod(function) else arg_spec.args | ||
| @classmethod | ||
| def _get_varargs(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| return [f"*{arg_spec.varargs}"] if arg_spec.varargs else [] | ||
| @classmethod | ||
| def _get_kwargs(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| return [f"**{arg_spec.varkw}"] if arg_spec.varkw else [] | ||
| @classmethod | ||
| def _get_named_only_args(cls, arg_spec: inspect.FullArgSpec) -> list: | ||
| rf_spec: list = [] | ||
| kw_only_args = arg_spec.kwonlyargs if arg_spec.kwonlyargs else [] | ||
| if not arg_spec.varargs and kw_only_args: | ||
| rf_spec.append("*") | ||
| kw_only_defaults = arg_spec.kwonlydefaults if arg_spec.kwonlydefaults else {} | ||
| for kw_only_arg in kw_only_args: | ||
| if kw_only_arg in kw_only_defaults: | ||
| rf_spec.append((kw_only_arg, kw_only_defaults[kw_only_arg])) | ||
| else: | ||
| rf_spec.append(kw_only_arg) | ||
| return rf_spec | ||
| @classmethod | ||
| def _get_types(cls, function): | ||
| if function is None: | ||
| return function | ||
| types = getattr(function, "robot_types", ()) | ||
| if types is None or types: | ||
| return types | ||
| return cls._get_typing_hints(function) | ||
| @classmethod | ||
| def _get_typing_hints(cls, function): | ||
| function = cls.unwrap(function) | ||
| hints = cls._get_type_hint(function) | ||
| arg_spec = cls._get_arg_spec(function) | ||
| all_args = cls._args_as_list(function, arg_spec) | ||
| for arg_with_hint in list(hints): | ||
| # remove self statements | ||
| if arg_with_hint not in [*all_args, "return"]: | ||
| hints.pop(arg_with_hint) | ||
| return hints | ||
| @classmethod | ||
| def _args_as_list(cls, function, arg_spec) -> list: | ||
| function_args = cls._drop_self_from_args(function, arg_spec) | ||
| if arg_spec.varargs: | ||
| function_args.append(arg_spec.varargs) | ||
| function_args.extend(arg_spec.kwonlyargs or []) | ||
| if arg_spec.varkw: | ||
| function_args.append(arg_spec.varkw) | ||
| return function_args | ||
| @classmethod | ||
| def _get_defaults(cls, arg_spec): | ||
| if not arg_spec.defaults: | ||
| return {} | ||
| names = arg_spec.args[-len(arg_spec.defaults) :] | ||
| return zip(names, arg_spec.defaults) | ||
| class KeywordSpecification: | ||
| def __init__( | ||
| self, | ||
| argument_specification=None, | ||
| documentation=None, | ||
| argument_types=None, | ||
| ) -> None: | ||
| self.argument_specification = argument_specification | ||
| self.documentation = documentation | ||
| self.argument_types = argument_types | ||
| class PluginParser: | ||
| def __init__(self, base_class: Optional[Any] = None, python_object=None) -> None: | ||
| self._base_class = base_class | ||
| self._python_object = python_object if python_object else [] | ||
| def parse_plugins(self, plugins: Union[str, List[str]]) -> List: | ||
| imported_plugins = [] | ||
| importer = Importer("test library") | ||
| for parsed_plugin in self._string_to_modules(plugins): | ||
| plugin = importer.import_class_or_module(parsed_plugin.module) | ||
| if not inspect.isclass(plugin): | ||
| message = f"Importing test library: '{parsed_plugin.module}' failed." | ||
| raise DataError(message) | ||
| args = self._python_object + parsed_plugin.args | ||
| plugin = plugin(*args, **parsed_plugin.kw_args) | ||
| if self._base_class and not isinstance(plugin, self._base_class): | ||
| message = f"Plugin does not inherit {self._base_class}" | ||
| raise PluginError(message) | ||
| imported_plugins.append(plugin) | ||
| return imported_plugins | ||
| def get_plugin_keywords(self, plugins: List): | ||
| return DynamicCore(plugins).get_keyword_names() | ||
| def _string_to_modules(self, modules: Union[str, List[str]]): | ||
| parsed_modules: list = [] | ||
| if not modules: | ||
| return parsed_modules | ||
| for module in self._modules_splitter(modules): | ||
| module_and_args = module.strip().split(";") | ||
| module_name = module_and_args.pop(0) | ||
| kw_args = {} | ||
| args = [] | ||
| for argument in module_and_args: | ||
| if "=" in argument: | ||
| key, value = argument.split("=") | ||
| kw_args[key] = value | ||
| else: | ||
| args.append(argument) | ||
| parsed_modules.append(Module(module=module_name, args=args, kw_args=kw_args)) | ||
| return parsed_modules | ||
| def _modules_splitter(self, modules: Union[str, List[str]]): | ||
| if isinstance(modules, str): | ||
| for module in modules.split(","): | ||
| yield module | ||
| else: | ||
| for module in modules: | ||
| yield module |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
66363
17.51%24
84.62%594
45.23%