Skip to content

Translation support for PLC #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ ENV/
# PyCharm project settings
.idea

# VSCode project settings
.vscode

# Robot Ouput files
log.html
output.xml
Expand Down
39 changes: 39 additions & 0 deletions atest/SmallLibrary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from pathlib import Path
from typing import Optional

from robot.api import logger
from robotlibcore import DynamicCore, keyword

class SmallLibrary(DynamicCore):
"""Library documentation."""

def __init__(self, translation: Optional[Path] = None):
"""__init__ documentation."""
if not isinstance(translation, Path):
logger.warn("Convert to Path")
translation = Path(translation)
logger.warn(translation.absolute())
logger.warn(type(translation))

DynamicCore.__init__(self, [], translation.absolute())

@keyword(tags=["tag1", "tag2"])
def normal_keyword(self, arg: int, other: str) -> str:
"""I have doc

Multiple lines.
Other line.
"""
data = f"{arg} {other}"
print(data)
return data

def not_keyword(self, data: str) -> str:
print(data)
return data

@keyword(name="This Is New Name", tags=["tag1", "tag2"])
def name_changed(self, some: int, other: int) -> int:
"""This one too"""
print(f"{some} {type(some)}, {other} {type(other)}")
return some + other
6 changes: 6 additions & 0 deletions atest/tests_types.robot
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*** Settings ***
Library DynamicTypesLibrary.py
Library DynamicTypesAnnotationsLibrary.py xxx
Library SmallLibrary.py ${CURDIR}/translation.json


*** Variables ***
Expand Down Expand Up @@ -115,6 +116,11 @@ Python 3.10 New Type Hints
Keyword With Named Only Arguments
Kw With Named Arguments arg=1

SmallLibray With New Name
${data} = SmallLibrary.Other Name 123 abc
Should Be Equal ${data} 123 abc
${data} = SmallLibrary.name_changed_again 1 2
Should Be Equal As Integers ${data} 3

*** Keywords ***
Import DynamicTypesAnnotationsLibrary In Python 3.10 Only
Expand Down
11 changes: 11 additions & 0 deletions atest/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"normal_keyword": {
"name": "other_name",
"doc": "This is new doc"
},
"name_changed": {
"name": "name_changed_again",
"doc": "This is also replaced.\n\nnew line."
}

}
16 changes: 12 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ line-length = 120

[tool.ruff]
line-length = 120
fixable = ["ALL"]
lint.fixable = ["ALL"]
target-version = "py38"
select = [
lint.select = [
"F",
"E",
"W",
Expand Down Expand Up @@ -46,8 +46,16 @@ select = [
"RUF"
]

[tool.ruff.mccabe]
[tool.ruff.lint.extend-per-file-ignores]
"utest/*" = [
"S",
"SLF",
"PLR",
"B018"
]

[tool.ruff.lint.mccabe]
max-complexity = 9

[tool.ruff.flake8-quotes]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath = ./atest ./src ./utest
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ rellu >= 0.7
twine
wheel
typing-extensions >= 4.5.0
approvaltests >= 11.1.1
51 changes: 42 additions & 9 deletions src/robotlibcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
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
Expand All @@ -42,28 +45,47 @@ 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 {}


class HybridCore:
def __init__(self, library_components: List) -> None:
def __init__(self, library_components: List, translation: Optional[Path] = None) -> None:
self.keywords = {}
self.keywords_spec = {}
self.attributes = {}
self.add_library_components(library_components)
self.add_library_components([self])
translation_data = _translation(translation)
self.add_library_components(library_components, translation_data)
self.add_library_components([self], translation_data)
self.__set_library_listeners(library_components)

def add_library_components(self, library_components: List):
self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) # type: ignore
def add_library_components(self, library_components: List, translation: Optional[dict] = None):
translation = translation if translation else {}
self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__, translation) # type: ignore
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 = func.robot_name or name
kw_name = self.__get_keyword_name(func, name, translation)
self.keywords[kw_name] = kw
self.keywords_spec[kw_name] = KeywordBuilder.build(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):
if name in translation:
return translation[name]["name"]
return func.robot_name or name

def __set_library_listeners(self, library_components: list):
listeners = self.__get_manually_registered_listeners()
listeners.extend(self.__get_component_listeners([self, *library_components]))
Expand Down Expand Up @@ -198,13 +220,24 @@ def __get_keyword_path(self, method):

class KeywordBuilder:
@classmethod
def build(cls, function):
def build(cls, function, translation: Optional[dict] = None):
translation = translation if translation else {}
return KeywordSpecification(
argument_specification=cls._get_arguments(function),
documentation=inspect.getdoc(function) or "",
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):
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)
Expand Down
3 changes: 2 additions & 1 deletion tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,10 @@ def lint(ctx):
ruff_cmd.append("--fix")
ruff_cmd.append("./src")
ruff_cmd.append("./tasks.py")
ruff_cmd.append("./utest")
ctx.run(" ".join(ruff_cmd))
print("Run black")
ctx.run("black src/ tasks.py utest/run.py atest/run.py")
ctx.run("black src/ tasks.py utest atest/run.py")
print("Run tidy")
print(f"Lint Robot files {'in ci' if in_ci else ''}")
command = [
Expand Down
16 changes: 6 additions & 10 deletions utest/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,19 @@
import argparse
import platform
import sys
from os.path import abspath, dirname, join
from pathlib import Path

import pytest
from robot.version import VERSION as RF_VERSION

curdir = dirname(abspath(__file__))
atest_dir = join(curdir, "..", "atest")
curdir = Path(__file__).parent
atest_dir = curdir / ".." / "atest"
python_version = platform.python_version()
xunit_report = join(
atest_dir,
"results",
"xunit-python-{}-robot{}.xml".format(python_version, RF_VERSION),
)
src = join(curdir, "..", "src")
xunit_report = atest_dir / "results" / f"xunit-python-{python_version}-robot{RF_VERSION}.xml"
src = curdir / ".." / "src"
sys.path.insert(0, src)
sys.path.insert(0, atest_dir)
helpers = join(curdir, "helpers")
helpers = curdir / "helpers"
sys.path.append(helpers)

parser = argparse.ArgumentParser()
Expand Down
25 changes: 14 additions & 11 deletions utest/test_get_keyword_source.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import inspect
from os import path
from pathlib import Path

import pytest
from DynamicLibrary import DynamicLibrary
Expand All @@ -18,23 +18,26 @@ def lib_types():


@pytest.fixture(scope="module")
def cur_dir():
return path.dirname(__file__)
def cur_dir() -> Path:
return Path(__file__).parent


@pytest.fixture(scope="module")
def lib_path(cur_dir):
return path.normpath(path.join(cur_dir, "..", "atest", "DynamicLibrary.py"))
def lib_path(cur_dir) -> Path:
path = cur_dir / ".." / "atest" / "DynamicLibrary.py"
return path.resolve()


@pytest.fixture(scope="module")
def lib_path_components(cur_dir):
return path.normpath(path.join(cur_dir, "..", "atest", "librarycomponents.py"))
def lib_path_components(cur_dir) -> Path:
path = cur_dir / ".." / "atest" / "librarycomponents.py"
return path.resolve()


@pytest.fixture(scope="module")
def lib_path_types(cur_dir):
return path.normpath(path.join(cur_dir, "..", "atest", "DynamicTypesLibrary.py"))
def lib_path_types(cur_dir) -> Path:
path = cur_dir / ".." / "atest" / "DynamicTypesLibrary.py"
return path.resolve()


def test_location_in_main(lib, lib_path):
Expand All @@ -60,7 +63,7 @@ def test_location_in_class_custom_keyword_name(lib, lib_path_components):
def test_no_line_number(lib, lib_path, when):
when(lib)._DynamicCore__get_keyword_line(Any()).thenReturn(None)
source = lib.get_keyword_source("keyword_in_main")
assert source == lib_path
assert Path(source) == lib_path


def test_no_path(lib, when):
Expand Down Expand Up @@ -90,4 +93,4 @@ def test_error_in_getfile(lib, when):
def test_error_in_line_number(lib, when, lib_path):
when(inspect).getsourcelines(Any()).thenRaise(IOError("Some message"))
source = lib.get_keyword_source("keyword_in_main")
assert source == lib_path
assert Path(source) == lib_path
8 changes: 4 additions & 4 deletions utest/test_get_keyword_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from DynamicTypesAnnotationsLibrary import CustomObject, DynamicTypesAnnotationsLibrary
from DynamicTypesLibrary import DynamicTypesLibrary
from lib_future_annotation import lib_future_annotation, Location
from lib_future_annotation import Location, lib_future_annotation


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_keyword_new_type(lib_types):

def test_keyword_return_type(lib_types):
types = lib_types.get_keyword_types("keyword_define_return_type")
assert types == {"arg": str, 'return': Union[List[str], str]}
assert types == {"arg": str, "return": Union[List[str], str]}


def test_keyword_forward_references(lib_types):
Expand All @@ -105,7 +105,7 @@ def test_keyword_with_annotation_external_class(lib_types):

def test_keyword_with_annotation_and_default_part2(lib_types):
types = lib_types.get_keyword_types("keyword_default_and_annotation")
assert types == {"arg1": int, "arg2": Union[bool, str], 'return': str}
assert types == {"arg1": int, "arg2": Union[bool, str], "return": str}


def test_keyword_with_robot_types_and_annotations(lib_types):
Expand Down Expand Up @@ -205,7 +205,7 @@ def test_kw_with_named_arguments(lib_types: DynamicTypesAnnotationsLibrary):

def test_kw_with_many_named_arguments_with_default(lib_types: DynamicTypesAnnotationsLibrary):
types = lib_types.get_keyword_types("kw_with_many_named_arguments_with_default")
assert types == {'arg2': int}
assert types == {"arg2": int}
types = lib_types.get_keyword_types("kw_with_positional_and_named_arguments_with_defaults")
assert types == {"arg1": int, "arg2": str}
types = lib_types.get_keyword_types("kw_with_positional_and_named_arguments")
Expand Down
Loading