From 8d1e08fda6f759c429643e4e1e4158bab6c14d31 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 4 Jan 2021 23:02:33 +0200 Subject: [PATCH 001/148] Drop support for EOL Python 2.7 and 3.5 --- .github/workflows/CI.yml | 5 ++--- setup.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4ec7873..bf54ae1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,13 +8,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.6, 3.9, pypy3] + python-version: [3.6, 3.9, pypy3] rf-version: [3.1.2, 3.2.2] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} with Robot Framework ${{ matrix.rf-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -27,7 +27,6 @@ jobs: - name: Run flake8 run: | flake8 --max-line-length=110 src/ - if: matrix.python-version != '2.7' - name: Run unit tests run: | python utest/run.py diff --git a/setup.py b/setup.py index e9d80e4..5997895 100644 --- a/setup.py +++ b/setup.py @@ -10,13 +10,12 @@ Development Status :: 5 - Production/Stable License :: OSI Approved :: Apache Software License Operating System :: OS Independent -Programming Language :: Python :: 2 -Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 +Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing @@ -41,7 +40,7 @@ keywords = 'robotframework testing testautomation library development', platforms = 'any', classifiers = CLASSIFIERS, - python_requires = '>=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4', + python_requires = '>=3.6, <4', package_dir = {'': 'src'}, packages = find_packages('src'), py_modules = ['robotlibcore'], From bd21af1a414b7ea632bec80f4df6555851204867 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Jan 2021 09:52:00 +0200 Subject: [PATCH 002/148] Drop support for EOL Python 2.7 --- atest/run.py | 3 +-- src/robotlibcore.py | 17 +-------------- utest/test_get_keyword_source.py | 2 -- utest/test_get_keyword_types.py | 36 ++++---------------------------- utest/test_keyword_builder.py | 15 +++---------- utest/test_robotlibcore.py | 19 ++--------------- 6 files changed, 11 insertions(+), 81 deletions(-) diff --git a/atest/run.py b/atest/run.py index bab2983..4d7a29b 100755 --- a/atest/run.py +++ b/atest/run.py @@ -26,8 +26,7 @@ sys.exit(rc) process_output(output, verbose=False) output = join(outdir, 'lib-DynamicTypesLibrary-python-%s-robot-%s.xml' % (python_version, rf_version)) -exclude = 'py3' if sys.version_info < (3,) else '' -rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug', exclude=exclude) +rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug') if rc > 250: sys.exit(rc) process_output(output, verbose=False) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index ad87634..7056ccc 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,7 +21,6 @@ import inspect import os -import sys from robot.utils import PY_VERSION @@ -33,7 +32,6 @@ from robot.api.deco import keyword # noqa F401 from robot import __version__ as robot_version -PY2 = sys.version_info < (3,) RF31 = robot_version < '3.2' __version__ = '2.2.2.dev1' @@ -87,10 +85,7 @@ def __getattr__(self, name): .format(type(self).__name__, name)) def __dir__(self): - if PY2: - my_attrs = dir(type(self)) + list(self.__dict__) - else: - my_attrs = super().__dir__() + my_attrs = super().__dir__() return sorted(set(my_attrs) | set(self.attributes)) def get_keyword_names(self): @@ -172,8 +167,6 @@ def build(cls, function): @classmethod def unwrap(cls, function): - if PY2: - return function return inspect.unwrap(function) @classmethod @@ -192,8 +185,6 @@ def _get_arguments(cls, function): @classmethod def _get_arg_spec(cls, function): - if PY2: - return inspect.getargspec(function) return inspect.getfullargspec(function) @classmethod @@ -224,15 +215,11 @@ def _get_var_args(cls, arg_spec): @classmethod def _get_kwargs(cls, arg_spec): - if PY2: - return ['**%s' % arg_spec.keywords] if arg_spec.keywords else [] return ['**%s' % arg_spec.varkw] if arg_spec.varkw else [] @classmethod def _get_kw_only(cls, arg_spec): kw_only_args = [] - if PY2: - return kw_only_args for arg in arg_spec.kwonlyargs: if not arg_spec.kwonlydefaults or arg not in arg_spec.kwonlydefaults: kw_only_args.append(arg) @@ -258,8 +245,6 @@ def _get_types(cls, function): @classmethod def _get_typing_hints(cls, function): - if PY2: - return {} function = cls.unwrap(function) try: hints = typing.get_type_hints(function) diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index eab55a6..8956bda 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -6,7 +6,6 @@ from DynamicLibrary import DynamicLibrary from DynamicTypesLibrary import DynamicTypesLibrary -from robotlibcore import PY2 @pytest.fixture(scope='module') @@ -49,7 +48,6 @@ def test_location_in_class(lib, lib_path_components): assert source == '%s:15' % lib_path_components -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_decorator_wrapper(lib_types, lib_path_types): source = lib_types.get_keyword_source('keyword_wrapped') assert source == '%s:72' % lib_path_types diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 1105813..d84be44 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,13 +1,11 @@ import pytest -from robotlibcore import PY2, RF31 - -if not PY2: - from typing import List, Union, Dict - from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary - from DynamicTypesAnnotationsLibrary import CustomObject +from robotlibcore import RF31 +from typing import List, Union +from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary +from DynamicTypesAnnotationsLibrary import CustomObject from DynamicTypesLibrary import DynamicTypesLibrary @@ -70,44 +68,37 @@ def test_keyword_none_rf31(lib): assert types == {} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_single_annotation(lib_types): types = lib_types.get_keyword_types('keyword_with_one_annotation') assert types == {'arg': str} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_multiple_annotations(lib_types): types = lib_types.get_keyword_types('keyword_with_multiple_annotations') assert types == {'arg1': str, 'arg2': List} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_multiple_types(lib_types): types = lib_types.get_keyword_types('keyword_multiple_types') assert types == {'arg': Union[List, None]} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_new_type(lib_types): types = lib_types.get_keyword_types('keyword_new_type') assert len(types) == 1 assert types['arg'] -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_return_type(lib_types): types = lib_types.get_keyword_types('keyword_define_return_type') assert types == {'arg': str} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_forward_references(lib_types): types = lib_types.get_keyword_types('keyword_forward_references') assert types == {'arg': CustomObject} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_annotation_and_default(lib_types): types = lib_types.get_keyword_types('keyword_with_annotations_and_default') assert types == {'arg': str} @@ -118,36 +109,30 @@ def test_keyword_with_many_defaults(lib): assert types == {} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_annotation_external_class(lib_types): types = lib_types.get_keyword_types('keyword_with_webdriver') assert types == {'arg': CustomObject} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_annotation_and_default(lib_types): types = lib_types.get_keyword_types('keyword_default_and_annotation') assert types == {'arg1': int, 'arg2': Union[bool, str]} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_robot_types_and_annotations(lib_types): types = lib_types.get_keyword_types('keyword_robot_types_and_annotations') assert types == {'arg': str} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_robot_types_disbaled_and_annotations(lib_types): types = lib_types.get_keyword_types('keyword_robot_types_disabled_and_annotations') assert types is None -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_robot_types_and_bool_annotations(lib_types): types = lib_types.get_keyword_types('keyword_robot_types_and_bool_hint') assert types == {'arg1': str} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_init_args(lib_types): types = lib_types.get_keyword_types('__init__') assert types == {'arg': str} @@ -163,85 +148,72 @@ def test_varargs(lib): assert types == {} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_init_args_with_annotation(lib_types): types = lib_types.get_keyword_types('__init__') assert types == {'arg': str} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_exception_in_annotations(lib_types): types = lib_types.get_keyword_types('keyword_exception_annotations') assert types == {'arg': 'NotHere'} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_only_arguments(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments') assert types == {} @pytest.mark.skipif(RF31, reason='Only for RF3.2+') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_only_arguments_many(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many') assert types == {} @pytest.mark.skipif(not RF31, reason='Only for RF3.1') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_only_arguments_many(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many') assert types == {} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_mandatory_and_keyword_only_arguments(lib_types): types = lib_types.get_keyword_types('keyword_mandatory_and_keyword_only_arguments') assert types == {'arg': int, 'some': bool} @pytest.mark.skipif(RF31, reason='Only for RF3.2+') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_only_arguments_many_positional_and_default_rf32(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') assert types == {'four': Union[int, str], 'six': Union[bool, str]} @pytest.mark.skipif(not RF31, reason='Only for RF3.1') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_only_arguments_many_positional_and_default_rf31(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') assert types == {'four': Union[int, str], 'six': Union[bool, str]} @pytest.mark.skipif(RF31, reason='Only for RF3.2+') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_all_args_rf32(lib_types): types = lib_types.get_keyword_types('keyword_all_args') assert types == {} @pytest.mark.skipif(not RF31, reason='Only for RF3.1') -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_all_args_rf31(lib_types): types = lib_types.get_keyword_types('keyword_all_args') assert types == {} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_self_and_types(lib_types): types = lib_types.get_keyword_types('keyword_self_and_types') assert types == {'mandatory': str, 'other': bool} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_self_and_keyword_only_types(lib_types): types = lib_types.get_keyword_types('keyword_self_and_keyword_only_types') assert types == {'varargs': int, 'other': bool, 'kwargs': int} -@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_keyword_with_decorator_arguments(lib_types): types = lib_types.get_keyword_types('keyword_with_deco_and_signature') assert types == {'arg1': bool, 'arg2': bool} diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 2cdd94c..523ea61 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -1,10 +1,9 @@ import pytest -from robotlibcore import PY2, RF31, KeywordBuilder +from robotlibcore import RF31, KeywordBuilder from moc_library import MockLibrary -if not PY2: - from moc_library_py3 import MockLibraryPy3 - from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary +from moc_library_py3 import MockLibraryPy3 +from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @pytest.fixture @@ -68,20 +67,17 @@ def test_varargs_and_kwargs(lib): assert spec.argument_specification == ['*vargs', '**kwargs'] -@pytest.mark.skipif(PY2, reason='Only for Python 3') def test_named_only(lib_py3): spec = KeywordBuilder.build(lib_py3.named_only) assert spec.argument_specification == ['*varargs', 'key1', 'key2'] -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(RF31, reason='Only for RF3.2+') def test_named_only_rf32(lib_py3): spec = KeywordBuilder.build(lib_py3.named_only_with_defaults) assert spec.argument_specification == ['*varargs', 'key1', 'key2', ('key3', 'default1'), ('key4', True)] -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(not RF31, reason='Only for RF3.1') def test_named_only_rf31(lib_py3): spec = KeywordBuilder.build(lib_py3.named_only_with_defaults) @@ -98,25 +94,21 @@ def test_types_disabled_in_keyword_deco(lib): assert spec.argument_types is None -@pytest.mark.skipif(PY2, reason='Only for Python 3') def test_types_(lib_py3): spec = KeywordBuilder.build(lib_py3.args_with_type_hints) assert spec.argument_types == {'arg3': str, 'arg4': type(None)} -@pytest.mark.skipif(PY2, reason='Only for Python 3') def test_types(lib_py3): spec = KeywordBuilder.build(lib_py3.self_and_keyword_only_types) assert spec.argument_types == {'varargs': int, 'other': bool, 'kwargs': int} -@pytest.mark.skipif(PY2, reason='Only for Python 3') def test_optional_none(lib_py3): spec = KeywordBuilder.build(lib_py3.optional_none) assert spec.argument_types == {'arg1': str, 'arg2': str} -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(RF31, reason='For RF 3.2') def test_complex_deco_rf32(dyn_types): spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) @@ -125,7 +117,6 @@ def test_complex_deco_rf32(dyn_types): assert spec.documentation == "Test me doc here" -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(not RF31, reason='For RF 3.2') def test_complex_deco_rf31(dyn_types): spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 2a9e059..b0f32e9 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,13 +1,10 @@ -import sys - import pytest from robot import __version__ as robot_version -from robotlibcore import HybridCore, PY2 +from robotlibcore import HybridCore from HybridLibrary import HybridLibrary from DynamicLibrary import DynamicLibrary -if not PY2: - from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary +from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @pytest.fixture(scope='module') @@ -125,7 +122,6 @@ def test_get_keyword_arguments_rf32(): args('__foobar__') -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(robot_version < '3.2', reason='For RF 3.2 or greater') def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments @@ -138,7 +134,6 @@ def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): assert args('keyword_with_deco_and_signature') == [('arg1', False), ('arg2', False)] -@pytest.mark.skipif(PY2, reason='Only for Python 3') @pytest.mark.skipif(robot_version >= '3.2', reason='For RF 3.1') def test_keyword_only_arguments_for_get_keyword_arguments_rf31(): args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments @@ -176,13 +171,3 @@ def test_library_cannot_be_class(): HybridCore([HybridLibrary]) assert str(exc_info.value) == \ "Libraries must be modules or instances, got class 'HybridLibrary' instead." - - -@pytest.mark.skipif(sys.version_info[0] > 2, reason='Only applicable on Py 2') -def test_library_cannot_be_old_style_class_instance(): - class OldStyle: - pass - with pytest.raises(TypeError) as exc_info: - HybridCore([OldStyle()]) - assert str(exc_info.value) == \ - "Libraries must be modules or new-style class instances, got old-style class 'OldStyle' instead." From f3048abe2d5d05c011925f4844886180c7b3d9ef Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Jan 2021 11:10:44 +0200 Subject: [PATCH 003/148] Upgrade Python syntax with pyupgrade --py36-plus --- atest/DynamicTypesAnnotationsLibrary.py | 10 +++++----- atest/DynamicTypesLibrary.py | 6 +++--- atest/ExtendExistingLibrary.py | 2 +- atest/librarycomponents.py | 10 ++++------ atest/moc_library.py | 2 +- atest/run.py | 11 +++++------ src/robotlibcore.py | 12 ++++++------ utest/run.py | 2 +- 8 files changed, 26 insertions(+), 29 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 35c10b0..a46056b 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -26,7 +26,7 @@ def wrapper(*args, **kwargs): return actual_decorator -class CustomObject(object): +class CustomObject: def __init__(self, x, y): self.x = x @@ -75,19 +75,19 @@ def keyword_with_webdriver(self, arg: CustomObject): @keyword def keyword_default_and_annotation(self: 'DynamicTypesAnnotationsLibrary', arg1: int, arg2: Union[bool, str] = False) -> str: - return '%s: %s, %s: %s' % (arg1, type(arg1), arg2, type(arg2)) + return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) @keyword(types={'arg': str}) def keyword_robot_types_and_annotations(self: 'DynamicTypesAnnotationsLibrary', arg: int): - return '%s: %s' % (arg, type(arg)) + return '{}: {}'.format(arg, type(arg)) @keyword(types=None) def keyword_robot_types_disabled_and_annotations(self, arg: int): - return '%s: %s' % (arg, type(arg)) + return '{}: {}'.format(arg, type(arg)) @keyword(types={'arg1': str}) def keyword_robot_types_and_bool_hint(self, arg1, arg2: bool): - return '%s: %s, %s: %s' % (arg1, type(arg1), arg2, type(arg2)) + return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) @keyword def keyword_exception_annotations(self: 'DynamicTypesAnnotationsLibrary', arg: 'NotHere'): diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index 409fde1..3626fe7 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -55,7 +55,7 @@ def keyword_many_default_types(self, arg1=1, arg2='Foobar'): @keyword def keyword_none(self, arg=None): - return '%s: %s' % (arg, type(arg)) + return '{}: {}'.format(arg, type(arg)) @keyword def is_python_3(self): @@ -74,8 +74,8 @@ def keyword_wrapped(self, number=1, arg=''): @keyword def varargs_and_kwargs(self, *args, **kwargs): - return '%s, %s' % (args, kwargs) + return '{}, {}'.format(args, kwargs) @keyword def keyword_booleans(self, arg1=True, arg2=False): - return '%s: %s, %s: %s' % (arg1, type(arg1), arg2, type(arg2)) + return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) diff --git a/atest/ExtendExistingLibrary.py b/atest/ExtendExistingLibrary.py index de67fbb..a1abcff 100644 --- a/atest/ExtendExistingLibrary.py +++ b/atest/ExtendExistingLibrary.py @@ -8,7 +8,7 @@ def __init__(self): self.add_library_components([ExtendingComponent()]) -class ExtendingComponent(object): +class ExtendingComponent: @keyword def keyword_in_extending_library(self): diff --git a/atest/librarycomponents.py b/atest/librarycomponents.py index 2815142..6859098 100644 --- a/atest/librarycomponents.py +++ b/atest/librarycomponents.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from robotlibcore import keyword @@ -8,7 +6,7 @@ def function(): return 1 -class Names(object): +class Names: attribute = 'not keyword' @keyword @@ -31,7 +29,7 @@ def dont_touch_property(self): raise RuntimeError('Should not touch property!!') -class Arguments(object): +class Arguments: @keyword def mandatory(self, arg1, arg2): @@ -61,11 +59,11 @@ def format_args(self, *args, **kwargs): def ru(item): return repr(item).lstrip('u') args = [ru(a) for a in args] - kwargs = ['%s=%s' % (k, ru(kwargs[k])) for k in sorted(kwargs)] + kwargs = ['{}={}'.format(k, ru(kwargs[k])) for k in sorted(kwargs)] return ', '.join(args + kwargs) -class DocsAndTags(object): +class DocsAndTags: @keyword def one_line_doc(self): diff --git a/atest/moc_library.py b/atest/moc_library.py index 7dfca37..eec3c18 100644 --- a/atest/moc_library.py +++ b/atest/moc_library.py @@ -1,7 +1,7 @@ from robot.api.deco import keyword -class MockLibrary(object): +class MockLibrary: def no_args(self): pass diff --git a/atest/run.py b/atest/run.py index 4d7a29b..c4e4e30 100755 --- a/atest/run.py +++ b/atest/run.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function import platform from os.path import abspath, dirname, join @@ -19,22 +18,22 @@ sys.path.insert(0, join(curdir, '..', 'src')) python_version = platform.python_version() for variant in library_variants: - output = join(outdir, 'lib-%s-python-%s-robot-%s.xml' % (variant, python_version, rf_version)) + output = join(outdir, 'lib-{}-python-{}-robot-{}.xml'.format(variant, python_version, rf_version)) rc = run(tests, name=variant, variable='LIBRARY:%sLibrary' % variant, output=output, report=None, log=None, loglevel='debug') if rc > 250: sys.exit(rc) process_output(output, verbose=False) -output = join(outdir, 'lib-DynamicTypesLibrary-python-%s-robot-%s.xml' % (python_version, rf_version)) +output = join(outdir, 'lib-DynamicTypesLibrary-python-{}-robot-{}.xml'.format(python_version, rf_version)) rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug') if rc > 250: sys.exit(rc) process_output(output, verbose=False) print('\nCombining results.') library_variants.append('DynamicTypesLibrary') -rc = rebot(*(join(outdir, 'lib-%s-python-%s-robot-%s.xml' % (variant, python_version, rf_version)) for variant in library_variants), - **dict(name='Acceptance Tests', outputdir=outdir, log='log-python-%s-robot-%s.html' % (python_version, rf_version), - report='report-python-%s-robot-%s.html' % (python_version, rf_version))) +rc = rebot(*(join(outdir, 'lib-{}-python-{}-robot-{}.xml'.format(variant, python_version, rf_version)) for variant in library_variants), + **dict(name='Acceptance Tests', outputdir=outdir, log='log-python-{}-robot-{}.html'.format(python_version, rf_version), + report='report-python-{}-robot-{}.html'.format(python_version, rf_version))) if rc == 0: print('\nAll tests passed/failed as expected.') else: diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 7056ccc..4bd41fb 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -37,7 +37,7 @@ __version__ = '2.2.2.dev1' -class HybridCore(object): +class HybridCore: def __init__(self, library_components): self.keywords = {} @@ -131,7 +131,7 @@ def get_keyword_source(self, keyword_name): path = self.__get_keyword_path(method) line_number = self.__get_keyword_line(method) if path and line_number: - return '%s:%s' % (path, line_number) + return '{}:{}'.format(path, line_number) if path: return path if line_number: @@ -141,7 +141,7 @@ def get_keyword_source(self, keyword_name): def __get_keyword_line(self, method): try: lines, line_number = inspect.getsourcelines(method) - except (OSError, IOError, TypeError): + except (OSError, TypeError): return None for increment, line in enumerate(lines): if line.strip().startswith('def '): @@ -155,7 +155,7 @@ def __get_keyword_path(self, method): return None -class KeywordBuilder(object): +class KeywordBuilder: @classmethod def build(cls, function): @@ -231,7 +231,7 @@ def _get_kw_only(cls, arg_spec): @classmethod def _format_defaults(cls, arg, value): if RF31: - return '%s=%s' % (arg, value) + return '{}={}'.format(arg, value) return arg, value @classmethod @@ -299,7 +299,7 @@ def _get_defaults(cls, arg_spec): return zip(names, arg_spec.defaults) -class KeywordSpecification(object): +class KeywordSpecification: def __init__(self, argument_specification=None, documentation=None, argument_types=None): self.argument_specification = argument_specification diff --git a/utest/run.py b/utest/run.py index 9c65721..f5a8c26 100755 --- a/utest/run.py +++ b/utest/run.py @@ -10,7 +10,7 @@ curdir = dirname(abspath(__file__)) atest_dir = join(curdir, '..', 'atest') python_version = platform.python_version() -xunit_report = join(atest_dir, 'results', 'xunit-python-%s-robot%s.xml' % (python_version, rf_version)) +xunit_report = join(atest_dir, 'results', 'xunit-python-{}-robot{}.xml'.format(python_version, rf_version)) src = join(curdir, '..', 'src') sys.path.insert(0, src) sys.path.insert(0, atest_dir) From 873b38179883be8f11676f6ac2d4d0b420032aca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Jan 2021 11:48:43 +0200 Subject: [PATCH 004/148] Fix tests --- utest/test_get_keyword_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 8956bda..95d65a0 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -45,7 +45,7 @@ def test_location_in_main(lib, lib_path): def test_location_in_class(lib, lib_path_components): source = lib.get_keyword_source('method') - assert source == '%s:15' % lib_path_components + assert source == '%s:13' % lib_path_components def test_decorator_wrapper(lib_types, lib_path_types): @@ -55,7 +55,7 @@ def test_decorator_wrapper(lib_types, lib_path_types): def test_location_in_class_custom_keyword_name(lib, lib_path_components): source = lib.get_keyword_source('Custom name') - assert source == '%s:19' % lib_path_components + assert source == '%s:17' % lib_path_components def test_no_line_number(lib, lib_path, when): From 510da003ae52851f99abf98e1266c592727caac0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Jan 2021 11:50:17 +0200 Subject: [PATCH 005/148] Refactor moc_library_py3 into moc_library --- atest/moc_library.py | 17 +++++++++++++++++ atest/moc_library_py3.py | 19 ------------------- utest/test_keyword_builder.py | 30 ++++++++++++------------------ 3 files changed, 29 insertions(+), 37 deletions(-) delete mode 100644 atest/moc_library_py3.py diff --git a/atest/moc_library.py b/atest/moc_library.py index eec3c18..88377d1 100644 --- a/atest/moc_library.py +++ b/atest/moc_library.py @@ -1,3 +1,5 @@ +from typing import Optional + from robot.api.deco import keyword @@ -27,3 +29,18 @@ def default_only(self, named1='string1', named2=123): def varargs_kwargs(self, *vargs, **kwargs): pass + + def named_only(self, *varargs, key1, key2): + pass + + def named_only_with_defaults(self, *varargs, key1, key2, key3='default1', key4=True): + pass + + def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool: + pass + + def self_and_keyword_only_types(x: 'MockLibrary', mandatory, *varargs: int, other: bool, **kwargs: int): + pass + + def optional_none(self, xxx, arg1: Optional[str] = None, arg2: Optional[str] = None, arg3=False): + pass diff --git a/atest/moc_library_py3.py b/atest/moc_library_py3.py deleted file mode 100644 index c9444ed..0000000 --- a/atest/moc_library_py3.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Optional - - -class MockLibraryPy3: - - def named_only(self, *varargs, key1, key2): - pass - - def named_only_with_defaults(self, *varargs, key1, key2, key3='default1', key4=True): - pass - - def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool: - pass - - def self_and_keyword_only_types(x: 'MockLibraryPy3', mandatory, *varargs: int, other: bool, **kwargs: int): - pass - - def optional_none(self, xxx, arg1: Optional[str] = None, arg2: Optional[str] = None, arg3=False): - pass diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 523ea61..e224acc 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -2,7 +2,6 @@ from robotlibcore import RF31, KeywordBuilder from moc_library import MockLibrary -from moc_library_py3 import MockLibraryPy3 from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -11,11 +10,6 @@ def lib(): return MockLibrary() -@pytest.fixture -def lib_py3(): - return MockLibraryPy3() - - @pytest.fixture def dyn_types(): return DynamicTypesAnnotationsLibrary(1) @@ -67,20 +61,20 @@ def test_varargs_and_kwargs(lib): assert spec.argument_specification == ['*vargs', '**kwargs'] -def test_named_only(lib_py3): - spec = KeywordBuilder.build(lib_py3.named_only) +def test_named_only(lib): + spec = KeywordBuilder.build(lib.named_only) assert spec.argument_specification == ['*varargs', 'key1', 'key2'] @pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_named_only_rf32(lib_py3): - spec = KeywordBuilder.build(lib_py3.named_only_with_defaults) +def test_named_only_rf32(lib): + spec = KeywordBuilder.build(lib.named_only_with_defaults) assert spec.argument_specification == ['*varargs', 'key1', 'key2', ('key3', 'default1'), ('key4', True)] @pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_named_only_rf31(lib_py3): - spec = KeywordBuilder.build(lib_py3.named_only_with_defaults) +def test_named_only_rf31(lib): + spec = KeywordBuilder.build(lib.named_only_with_defaults) assert spec.argument_specification == ['*varargs', 'key1', 'key2', 'key3=default1', 'key4=True'] @@ -94,18 +88,18 @@ def test_types_disabled_in_keyword_deco(lib): assert spec.argument_types is None -def test_types_(lib_py3): - spec = KeywordBuilder.build(lib_py3.args_with_type_hints) +def test_types_(lib): + spec = KeywordBuilder.build(lib.args_with_type_hints) assert spec.argument_types == {'arg3': str, 'arg4': type(None)} -def test_types(lib_py3): - spec = KeywordBuilder.build(lib_py3.self_and_keyword_only_types) +def test_types(lib): + spec = KeywordBuilder.build(lib.self_and_keyword_only_types) assert spec.argument_types == {'varargs': int, 'other': bool, 'kwargs': int} -def test_optional_none(lib_py3): - spec = KeywordBuilder.build(lib_py3.optional_none) +def test_optional_none(lib): + spec = KeywordBuilder.build(lib.optional_none) assert spec.argument_types == {'arg1': str, 'arg2': str} From 38ef017423f0904d2b2d9f0ca67fb26610075454 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 7 Feb 2021 01:04:43 +0200 Subject: [PATCH 006/148] Static library example --- docs/example/run.py | 14 +++++++++++ docs/example/static/StaticLibrary.py | 36 ++++++++++++++++++++++++++++ docs/example/static/test.robot | 21 ++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 docs/example/run.py create mode 100644 docs/example/static/StaticLibrary.py create mode 100644 docs/example/static/test.robot diff --git a/docs/example/run.py b/docs/example/run.py new file mode 100644 index 0000000..57331a6 --- /dev/null +++ b/docs/example/run.py @@ -0,0 +1,14 @@ +import argparse + +from robot import run_cli + +parser = argparse.ArgumentParser("Runner for examples") +parser.add_argument("type", help="Which example is run.") +args = parser.parse_args() +if args.type not in ["static", "dynamic"]: + raise ValueError("Invalid value for library type.") +run_cli([ + "--pythonpath", + args.type, + args.type +]) diff --git a/docs/example/static/StaticLibrary.py b/docs/example/static/StaticLibrary.py new file mode 100644 index 0000000..35d462c --- /dev/null +++ b/docs/example/static/StaticLibrary.py @@ -0,0 +1,36 @@ +import time +from typing import Optional + +from robot.api import logger + + +class StaticLibrary: + def __init__(self): + self.separator = ";" + + def join_strings(self, *strings: str) -> str: + """Joins args strings.""" + logger.info("Joining.") + return " ".join(strings) + + def sum(self, value1: int, value2: int) -> int: + """Do other thing.""" + logger.info(f"Calculating hard.") + return value1 + value2 + + def wait_something_to_happen(self, arg1: str, arg2: int) -> str: + self._waiter(0.3) + arg1 = self.join_strings(arg1, arg1) + self._waiter(0.2) + arg2 = self.sum(arg2, arg2) + self._waiter() + logger.info("Waiting done") + return f"{arg1} and {arg2}" + + def join_string_with_separator(self, *strings, separator: Optional[str] =None): + """Joins strings with separator""" + return f"{separator if separator else self.separator}".join(strings) + + def _waiter(self, timeout: float = 0.1): + logger.info(f"Waiting {timeout}") + time.sleep(timeout) diff --git a/docs/example/static/test.robot b/docs/example/static/test.robot new file mode 100644 index 0000000..ebaa7ce --- /dev/null +++ b/docs/example/static/test.robot @@ -0,0 +1,21 @@ +*** Settings *** +Library StaticLibrary + +*** Test Cases *** +Join Stings + ${data} = Join Strings kala is big + Should Be Equal ${data} kala is big + +Sum Values + ${data} = Sum 1 2 + Should Be Equal As Numbers ${data} 3 + +Wait Something To Happen + ${data} = Wait Something To Happen tidii 3 + Should Be Equal ${data} tidii tidii and 6 + +Join Strings With Separator + ${data} = Join String With Separator Foo Bar Tidii separator=|-| + Should Be Equal ${data} Foo|-|Bar|-|Tidii + ${data} = Join String With Separator Foo Bar Tidii + Should Be Equal ${data} Foo;Bar;Tidii From e91e368d26b44c4a64635b5b0f24a5695d74cb16 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 7 Feb 2021 01:07:54 +0200 Subject: [PATCH 007/148] Add RF output files to ignore list --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 9ec1898..5682d75 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,8 @@ ENV/ # PyCharm project settings .idea + +# Robot Ouput files +log.html +output.xml +report.html \ No newline at end of file From d01dd80e918fa4fafeffcc6b5f6732a1ca1d30dd Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 7 Feb 2021 01:12:08 +0200 Subject: [PATCH 008/148] Improved examples --- docs/example/static/StaticLibrary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/example/static/StaticLibrary.py b/docs/example/static/StaticLibrary.py index 35d462c..e51978b 100644 --- a/docs/example/static/StaticLibrary.py +++ b/docs/example/static/StaticLibrary.py @@ -5,8 +5,8 @@ class StaticLibrary: - def __init__(self): - self.separator = ";" + def __init__(self, separator: str = ";"): + self.separator = separator def join_strings(self, *strings: str) -> str: """Joins args strings.""" From ab3ba40ddde60cdf67ed3b90036a8f0f33af83fc Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 7 Feb 2021 01:18:30 +0200 Subject: [PATCH 009/148] Inprove runner --- docs/example/{static => 01-static}/StaticLibrary.py | 0 docs/example/{static => 01-static}/test.robot | 0 docs/example/run.py | 10 +++++++--- 3 files changed, 7 insertions(+), 3 deletions(-) rename docs/example/{static => 01-static}/StaticLibrary.py (100%) rename docs/example/{static => 01-static}/test.robot (100%) diff --git a/docs/example/static/StaticLibrary.py b/docs/example/01-static/StaticLibrary.py similarity index 100% rename from docs/example/static/StaticLibrary.py rename to docs/example/01-static/StaticLibrary.py diff --git a/docs/example/static/test.robot b/docs/example/01-static/test.robot similarity index 100% rename from docs/example/static/test.robot rename to docs/example/01-static/test.robot diff --git a/docs/example/run.py b/docs/example/run.py index 57331a6..146dccc 100644 --- a/docs/example/run.py +++ b/docs/example/run.py @@ -5,10 +5,14 @@ parser = argparse.ArgumentParser("Runner for examples") parser.add_argument("type", help="Which example is run.") args = parser.parse_args() -if args.type not in ["static", "dynamic"]: +if args.type == "static": + folder = f"01-{args.type}" +elif args.type == "hybrid": + folder = f"02-{args.type}" +else: raise ValueError("Invalid value for library type.") run_cli([ "--pythonpath", - args.type, - args.type + folder, + folder ]) From 542aaa9e3f004a59dc0967f50931823c2ee2f95d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 7 Feb 2021 01:41:31 +0200 Subject: [PATCH 010/148] Hybrid example --- docs/example/01-static/StaticLibrary.py | 2 +- docs/example/02-hybrid/HybridLibrary.py | 18 +++++++++++++++++ docs/example/02-hybrid/calculator.py | 10 ++++++++++ docs/example/02-hybrid/stringtools.py | 17 ++++++++++++++++ docs/example/02-hybrid/test.robot | 26 +++++++++++++++++++++++++ docs/example/02-hybrid/waiter.py | 20 +++++++++++++++++++ docs/example/run.py | 6 +----- 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 docs/example/02-hybrid/HybridLibrary.py create mode 100644 docs/example/02-hybrid/calculator.py create mode 100644 docs/example/02-hybrid/stringtools.py create mode 100644 docs/example/02-hybrid/test.robot create mode 100644 docs/example/02-hybrid/waiter.py diff --git a/docs/example/01-static/StaticLibrary.py b/docs/example/01-static/StaticLibrary.py index e51978b..0236ecf 100644 --- a/docs/example/01-static/StaticLibrary.py +++ b/docs/example/01-static/StaticLibrary.py @@ -27,7 +27,7 @@ def wait_something_to_happen(self, arg1: str, arg2: int) -> str: logger.info("Waiting done") return f"{arg1} and {arg2}" - def join_string_with_separator(self, *strings, separator: Optional[str] =None): + def join_string_with_separator(self, *strings, separator: Optional[str] = None): """Joins strings with separator""" return f"{separator if separator else self.separator}".join(strings) diff --git a/docs/example/02-hybrid/HybridLibrary.py b/docs/example/02-hybrid/HybridLibrary.py new file mode 100644 index 0000000..c402ae1 --- /dev/null +++ b/docs/example/02-hybrid/HybridLibrary.py @@ -0,0 +1,18 @@ +from robot.api import logger + +from calculator import Calculator +from stringtools import StringTools +from waiter import Waiter + + +class HybridLibrary(Calculator, StringTools, Waiter): + def __init__(self, separator: str = ";"): + self.separator = separator + + def get_keyword_names(self): + keywords = [] + for name in dir(self): + method = getattr(self, name) + if hasattr(method, "robot_name"): + keywords.append(name) + return keywords diff --git a/docs/example/02-hybrid/calculator.py b/docs/example/02-hybrid/calculator.py new file mode 100644 index 0000000..2dc2e54 --- /dev/null +++ b/docs/example/02-hybrid/calculator.py @@ -0,0 +1,10 @@ +from robot.api import logger +from robot.api.deco import keyword + + +class Calculator: + @keyword + def sum(self, value1: int, value2: int) -> int: + """Do other thing.""" + logger.info(f"Calculating hard.") + return value1 + value2 diff --git a/docs/example/02-hybrid/stringtools.py b/docs/example/02-hybrid/stringtools.py new file mode 100644 index 0000000..8e9cb3f --- /dev/null +++ b/docs/example/02-hybrid/stringtools.py @@ -0,0 +1,17 @@ +from typing import Optional + +from robot.api import logger +from robot.api.deco import keyword + + +class StringTools: + @keyword + def join_strings(self, *strings: str) -> str: + """Joins args strings.""" + logger.info("Joining.") + return " ".join(strings) + + @keyword + def join_string_with_separator(self, *strings, separator: Optional[str] = None): + """Joins strings with separator""" + return f"{separator if separator else self.separator}".join(strings) diff --git a/docs/example/02-hybrid/test.robot b/docs/example/02-hybrid/test.robot new file mode 100644 index 0000000..ad0f6bf --- /dev/null +++ b/docs/example/02-hybrid/test.robot @@ -0,0 +1,26 @@ +*** Settings *** +Library HybridLibrary + +*** Test Cases *** +Join Stings + ${data} = Join Strings kala is big + Should Be Equal ${data} kala is big + +Sum Values + ${data} = Sum 1 2 + Should Be Equal As Numbers ${data} 3 + +Wait Something To Happen + ${data} = Wait Something To Happen tidii 3 + Should Be Equal ${data} tidii tidii and 6 + +Join Strings With Separator + ${data} = Join String With Separator Foo Bar Tidii separator=|-| + Should Be Equal ${data} Foo|-|Bar|-|Tidii + ${data} = Join String With Separator Foo Bar Tidii + Should Be Equal ${data} Foo;Bar;Tidii + +Waiter Is Not Keyword + Run Keyword And Expect Error + ... No keyword with name 'Waiter' found. + ... Waiter 1.0 \ No newline at end of file diff --git a/docs/example/02-hybrid/waiter.py b/docs/example/02-hybrid/waiter.py new file mode 100644 index 0000000..2692441 --- /dev/null +++ b/docs/example/02-hybrid/waiter.py @@ -0,0 +1,20 @@ +import time + +from robot.api import logger +from robot.api.deco import keyword + + +class Waiter: + @keyword + def wait_something_to_happen(self, arg1: str, arg2: int) -> str: + self.waiter(0.3) + arg1 = self.join_strings(arg1, arg1) + self.waiter(0.2) + arg2 = self.sum(arg2, arg2) + self.waiter() + logger.info("Waiting done") + return f"{arg1} and {arg2}" + + def waiter(self, timeout: float = 0.1): + logger.info(f"Waiting {timeout}") + time.sleep(timeout) diff --git a/docs/example/run.py b/docs/example/run.py index 146dccc..c53eebc 100644 --- a/docs/example/run.py +++ b/docs/example/run.py @@ -11,8 +11,4 @@ folder = f"02-{args.type}" else: raise ValueError("Invalid value for library type.") -run_cli([ - "--pythonpath", - folder, - folder -]) +run_cli(["--loglevel", "trace", "--pythonpath", folder, folder]) From b7a68ff08856eb3c291a2b8996873258748203a4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Tue, 13 Apr 2021 09:35:00 +0300 Subject: [PATCH 011/148] Use RF 4.0.1 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bf54ae1..8910347 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.6, 3.9, pypy3] - rf-version: [3.1.2, 3.2.2] + rf-version: [3.2.2, 4.0.1] steps: - uses: actions/checkout@v2 From 44d4cda3b88214da42c7a5475e3c05b27532ea36 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 10 Jun 2021 23:47:43 +0300 Subject: [PATCH 012/148] Drop RF 3.1 support Fixes #80 --- src/robotlibcore.py | 14 ++------- utest/test_get_keyword_types.py | 51 +++++---------------------------- utest/test_keyword_builder.py | 42 ++++----------------------- utest/test_robotlibcore.py | 32 ++------------------- 4 files changed, 17 insertions(+), 122 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 4bd41fb..47d2b06 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -30,9 +30,7 @@ typing = None from robot.api.deco import keyword # noqa F401 -from robot import __version__ as robot_version -RF31 = robot_version < '3.2' __version__ = '2.2.2.dev1' @@ -195,9 +193,7 @@ def _get_default_and_named_args(cls, arg_spec, function): formated_args = [] for arg in args: if defaults: - formated_args.append( - cls._format_defaults(arg, defaults.pop()) - ) + formated_args.append((arg, defaults.pop())) else: formated_args.append(arg) formated_args.reverse() @@ -225,15 +221,9 @@ def _get_kw_only(cls, arg_spec): kw_only_args.append(arg) else: value = arg_spec.kwonlydefaults.get(arg, '') - kw_only_args.append(cls._format_defaults(arg, value)) + kw_only_args.append((arg, value)) return kw_only_args - @classmethod - def _format_defaults(cls, arg, value): - if RF31: - return '{}={}'.format(arg, value) - return arg, value - @classmethod def _get_types(cls, function): if function is None: diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index d84be44..f377d04 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,9 +1,7 @@ -import pytest - +from typing import List, Union -from robotlibcore import RF31 +import pytest -from typing import List, Union from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from DynamicTypesAnnotationsLibrary import CustomObject from DynamicTypesLibrary import DynamicTypesLibrary @@ -29,14 +27,7 @@ def test_types_disabled(lib): assert types is None -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_keyword_types_and_bool_default_rf31(lib): - types = lib.get_keyword_types('keyword_robot_types_and_bool_default') - assert types == {'arg1': str} - - -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_keyword_types_and_bool_default_rf32(lib): +def test_keyword_types_and_bool_default(lib): types = lib.get_keyword_types('keyword_robot_types_and_bool_default') assert types == {'arg1': str} @@ -56,14 +47,7 @@ def test_not_keyword(lib): lib.get_keyword_types('not_keyword') -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_keyword_none_rf32(lib): - types = lib.get_keyword_types('keyword_none') - assert types == {} - - -@pytest.mark.skipif(not RF31, reason='Only for RF3.2+') -def test_keyword_none_rf31(lib): +def test_keyword_none(lib): types = lib.get_keyword_types('keyword_none') assert types == {} @@ -114,7 +98,7 @@ def test_keyword_with_annotation_external_class(lib_types): assert types == {'arg': CustomObject} -def test_keyword_with_annotation_and_default(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]} @@ -163,13 +147,6 @@ def test_keyword_only_arguments(lib_types): assert types == {} -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_keyword_only_arguments_many(lib_types): - types = lib_types.get_keyword_types('keyword_only_arguments_many') - assert types == {} - - -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') def test_keyword_only_arguments_many(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many') assert types == {} @@ -180,26 +157,12 @@ def test_keyword_mandatory_and_keyword_only_arguments(lib_types): assert types == {'arg': int, 'some': bool} -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_keyword_only_arguments_many_positional_and_default_rf32(lib_types): +def test_keyword_only_arguments_many_positional_and_default(lib_types): types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') assert types == {'four': Union[int, str], 'six': Union[bool, str]} -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_keyword_only_arguments_many_positional_and_default_rf31(lib_types): - types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') - assert types == {'four': Union[int, str], 'six': Union[bool, str]} - - -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_keyword_all_args_rf32(lib_types): - types = lib_types.get_keyword_types('keyword_all_args') - assert types == {} - - -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_keyword_all_args_rf31(lib_types): +def test_keyword_all_args(lib_types): types = lib_types.get_keyword_types('keyword_all_args') assert types == {} diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index e224acc..f66e653 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -1,6 +1,6 @@ import pytest -from robotlibcore import RF31, KeywordBuilder +from robotlibcore import KeywordBuilder from moc_library import MockLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -32,52 +32,31 @@ def test_positional_args(lib): assert spec.argument_specification == ['arg1', 'arg2'] -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_positional_and_named_rf32(lib): +def test_positional_and_named(lib): spec = KeywordBuilder.build(lib.positional_and_default) assert spec.argument_specification == ['arg1', 'arg2', ('named1', 'string1'), ('named2', 123)] -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_positional_and_named_rf31(lib): - spec = KeywordBuilder.build(lib.positional_and_default) - assert spec.argument_specification == ['arg1', 'arg2', 'named1=string1', 'named2=123'] - - -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_named_only_rf32(lib): +def test_named_only(lib): spec = KeywordBuilder.build(lib.default_only) assert spec.argument_specification == [('named1', 'string1'), ('named2', 123)] -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_named_only_rf31(lib): - spec = KeywordBuilder.build(lib.default_only) - assert spec.argument_specification == ['named1=string1', 'named2=123'] - - def test_varargs_and_kwargs(lib): spec = KeywordBuilder.build(lib.varargs_kwargs) assert spec.argument_specification == ['*vargs', '**kwargs'] -def test_named_only(lib): +def test_named_only_part2(lib): spec = KeywordBuilder.build(lib.named_only) assert spec.argument_specification == ['*varargs', 'key1', 'key2'] -@pytest.mark.skipif(RF31, reason='Only for RF3.2+') -def test_named_only_rf32(lib): +def test_named_only(lib): spec = KeywordBuilder.build(lib.named_only_with_defaults) assert spec.argument_specification == ['*varargs', 'key1', 'key2', ('key3', 'default1'), ('key4', True)] -@pytest.mark.skipif(not RF31, reason='Only for RF3.1') -def test_named_only_rf31(lib): - spec = KeywordBuilder.build(lib.named_only_with_defaults) - assert spec.argument_specification == ['*varargs', 'key1', 'key2', 'key3=default1', 'key4=True'] - - def test_types_in_keyword_deco(lib): spec = KeywordBuilder.build(lib.positional_args) assert spec.argument_types == {'arg1': str, 'arg2': int} @@ -103,17 +82,8 @@ def test_optional_none(lib): assert spec.argument_types == {'arg1': str, 'arg2': str} -@pytest.mark.skipif(RF31, reason='For RF 3.2') -def test_complex_deco_rf32(dyn_types): +def test_complex_deco(dyn_types): spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) assert spec.argument_types == {'arg1': bool, 'arg2': bool} assert spec.argument_specification == [('arg1', False), ('arg2', False)] assert spec.documentation == "Test me doc here" - - -@pytest.mark.skipif(not RF31, reason='For RF 3.2') -def test_complex_deco_rf31(dyn_types): - spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) - assert spec.argument_types == {'arg1': bool, 'arg2': bool} - assert spec.argument_specification == ['arg1=False', 'arg2=False'] - assert spec.documentation == "Test me doc here" diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index b0f32e9..62f02f6 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,5 +1,4 @@ import pytest -from robot import __version__ as robot_version from robotlibcore import HybridCore from HybridLibrary import HybridLibrary @@ -96,21 +95,7 @@ def test_getattr(): "'%s' object has no attribute 'non_existing'" % type(lib).__name__ -@pytest.mark.skipif(robot_version >= '3.2', reason='For RF 3.1') -def test_get_keyword_arguments_rf31(): - args = DynamicLibrary().get_keyword_arguments - assert args('mandatory') == ['arg1', 'arg2'] - assert args('defaults') == ['arg1', 'arg2=default', 'arg3=3'] - assert args('varargs_and_kwargs') == ['*args', '**kws'] - assert args('kwargs_only') == ['**kws'] - assert args('all_arguments') == ['mandatory', 'default=value', '*varargs', '**kwargs'] - assert args('__init__') == ['arg=None'] - with pytest.raises(AttributeError): - args('__foobar__') - - -@pytest.mark.skipif(robot_version < '3.2', reason='For RF 3.2 or greater') -def test_get_keyword_arguments_rf32(): +def test_get_keyword_arguments(): args = DynamicLibrary().get_keyword_arguments assert args('mandatory') == ['arg1', 'arg2'] assert args('defaults') == ['arg1', ('arg2', 'default'), ('arg3', 3)] @@ -122,8 +107,7 @@ def test_get_keyword_arguments_rf32(): args('__foobar__') -@pytest.mark.skipif(robot_version < '3.2', reason='For RF 3.2 or greater') -def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): +def test_keyword_only_arguments_for_get_keyword_arguments(): args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments assert args('keyword_only_arguments') == ['*varargs', ('some', 111)] assert args('keyword_only_arguments_many') == ['*varargs', ('some', 'value'), ('other', None)] @@ -134,18 +118,6 @@ def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): assert args('keyword_with_deco_and_signature') == [('arg1', False), ('arg2', False)] -@pytest.mark.skipif(robot_version >= '3.2', reason='For RF 3.1') -def test_keyword_only_arguments_for_get_keyword_arguments_rf31(): - args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments - assert args('keyword_only_arguments') == ['*varargs', 'some=111'] - assert args('keyword_only_arguments_many') == ['*varargs', 'some=value', 'other=None'] - assert args('keyword_only_arguments_no_default') == ['*varargs', 'other'] - assert args('keyword_only_arguments_default_and_no_default') == ['*varargs', 'other', 'value=False'] - all_args = ['mandatory', 'positional=1', '*varargs', 'other', 'value=False', '**kwargs'] - assert args('keyword_all_args') == all_args - assert args('keyword_with_deco_and_signature') == ['arg1=False', 'arg2=False'] - - def test_get_keyword_documentation(): doc = DynamicLibrary().get_keyword_documentation assert doc('function') == '' From 3d2dcc5448ef99a978b7ec61302d491df52c6c3e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 10 Jun 2021 21:45:16 +0300 Subject: [PATCH 013/148] Reveals the bug --- atest/DynamicTypesAnnotationsLibrary.py | 4 ++++ atest/tests_types.robot | 11 +++++++++++ utest/test_get_keyword_types.py | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index a46056b..659e262 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -148,3 +148,7 @@ def enum_conversion(self, param: Optional[penum] = None): def keyword_with_deco_and_signature(self, arg1: bool = False, arg2: bool = False): """Test me doc here""" return f"{arg1}: {type(arg1)}, {arg2}: {type(arg2)}" + + @keyword + def keyword_optional_with_none(self, arg: Optional[str] = None): + return f"arg: {arg}, type: {type(arg)}" diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 139d983..436c8c0 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -1,5 +1,6 @@ *** Settings *** Library DynamicTypesLibrary.py +Library DynamicTypesAnnotationsLibrary.py xxx Suite Setup Import DynamicTypesAnnotationsLibrary In Python 3 Only *** Test Cases *** @@ -73,6 +74,16 @@ Enum Conversion To Invalid Value Should Fail Run Keyword And Expect Error ValueError: Argument 'param' got value 'not ok' that* ... Enum Conversion not ok +Type Conversion With Optional And None + ${types} = Keyword Optional With None + Should Contain ${types} arg: None, + Should Contain ${types} + ${types} = Keyword Optional With None None + Should Contain ${types} arg: None, + Should Contain ${types} + ${types} = Keyword Optional With None ${None} + Should Contain ${types} arg: None, + Should Contain ${types} *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3 Only diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index f377d04..929ccbb 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -180,3 +180,8 @@ def test_keyword_self_and_keyword_only_types(lib_types): def test_keyword_with_decorator_arguments(lib_types): types = lib_types.get_keyword_types('keyword_with_deco_and_signature') assert types == {'arg1': bool, 'arg2': bool} + + +def test_keyword_optional_with_none_1(lib_types): + types = lib_types.get_keyword_types('keyword_optional_with_none') + assert types == {'arg': Union[str, type(None)]} From a389b22bb3d838178c5a5126d7e9eb6a0e11162a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 10 Jun 2021 23:37:45 +0300 Subject: [PATCH 014/148] Unit test for Union problem --- src/robotlibcore.py | 16 +++++++++------- utest/test_get_keyword_types.py | 28 ++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 47d2b06..c1995c4 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -18,19 +18,17 @@ examples see the project pages at https://github.com/robotframework/PythonLibCore """ - import inspect import os +import typing +from robot import version as robot_version from robot.utils import PY_VERSION -try: - import typing -except ImportError: - typing = None from robot.api.deco import keyword # noqa F401 +RF32 = robot_version < '4.' __version__ = '2.2.2.dev1' @@ -51,6 +49,8 @@ def add_library_components(self, library_components): if callable(func) and hasattr(func, 'robot_name'): kw = getattr(component, name) kw_name = func.robot_name or name + if kw_name == "keyword_optional_with_none": + print(kw_name) self.keywords[kw_name] = kw self.keywords_spec[kw_name] = KeywordBuilder.build(kw) # Expose keywords as attributes both using original @@ -246,8 +246,10 @@ def _get_typing_hints(cls, function): # remove return and self statements if arg_with_hint not in all_args: hints.pop(arg_with_hint) - default = cls._get_defaults(arg_spec) - return cls._remove_optional_none_type_hints(hints, default) + if RF32: + default = cls._get_defaults(arg_spec) + return cls._remove_optional_none_type_hints(hints, default) + return hints @classmethod def _args_as_list(cls, function, arg_spec): diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 929ccbb..dde4495 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,7 +1,18 @@ -from typing import List, Union +import sys +from os.path import dirname, abspath, join import pytest +import typing + +from robotlibcore import RF32 + +from typing import List, Union +curdir = dirname(abspath(__file__)) +atest_dir = join(curdir, '..', 'atest') +src = join(curdir, '..', 'src') +sys.path.insert(0, src) +sys.path.insert(0, atest_dir) from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from DynamicTypesAnnotationsLibrary import CustomObject from DynamicTypesLibrary import DynamicTypesLibrary @@ -182,6 +193,15 @@ def test_keyword_with_decorator_arguments(lib_types): assert types == {'arg1': bool, 'arg2': bool} -def test_keyword_optional_with_none_1(lib_types): - types = lib_types.get_keyword_types('keyword_optional_with_none') - assert types == {'arg': Union[str, type(None)]} +@pytest.mark.skipif(RF32, reason='Only for RF4+') +def test_keyword_optional_with_none_rf32(lib_types): + lib = DynamicTypesAnnotationsLibrary("111") + types = lib.get_keyword_types('keyword_optional_with_none') + assert types == {'arg': typing.Union[str, type(None)]} + + +@pytest.mark.skipif(not RF32, reason='Only for RF3.2+') +def test_keyword_optional_with_none_rf32(lib_types): + lib = DynamicTypesAnnotationsLibrary("111") + types = lib.get_keyword_types('keyword_optional_with_none') + assert types == {'arg': str} From d4e407dddca2925745a79ed25f80d5ff819b5180 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:12:17 +0300 Subject: [PATCH 015/148] Fix bug with RF 4.0 and working with types hints with Union --- src/robotlibcore.py | 4 +++- utest/test_get_keyword_types.py | 10 +--------- utest/test_keyword_builder.py | 12 ++++++++++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index c1995c4..7890945 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -22,7 +22,7 @@ import os import typing -from robot import version as robot_version +from robot import __version__ as robot_version from robot.utils import PY_VERSION @@ -262,6 +262,7 @@ def _args_as_list(cls, function, arg_spec): function_args.append(arg_spec.varkw) return function_args + # TODO: Remove when support RF 3.2 is dropped # Copied from: robot.running.arguments.argumentparser @classmethod def _remove_optional_none_type_hints(cls, type_hints, defaults): @@ -276,6 +277,7 @@ def _remove_optional_none_type_hints(cls, type_hints, defaults): type_hints[arg] = types[0] return type_hints + # TODO: Remove when support RF 3.2 is dropped # Copied from: robot.running.arguments.argumentparser @classmethod def _is_union(cls, typing_type): diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index dde4495..90876a7 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,6 +1,3 @@ -import sys -from os.path import dirname, abspath, join - import pytest import typing @@ -8,11 +5,6 @@ from typing import List, Union -curdir = dirname(abspath(__file__)) -atest_dir = join(curdir, '..', 'atest') -src = join(curdir, '..', 'src') -sys.path.insert(0, src) -sys.path.insert(0, atest_dir) from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from DynamicTypesAnnotationsLibrary import CustomObject from DynamicTypesLibrary import DynamicTypesLibrary @@ -194,7 +186,7 @@ def test_keyword_with_decorator_arguments(lib_types): @pytest.mark.skipif(RF32, reason='Only for RF4+') -def test_keyword_optional_with_none_rf32(lib_types): +def test_keyword_optional_with_none_rf4(lib_types): lib = DynamicTypesAnnotationsLibrary("111") types = lib.get_keyword_types('keyword_optional_with_none') assert types == {'arg': typing.Union[str, type(None)]} diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index f66e653..eb956ee 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -1,6 +1,7 @@ import pytest +import typing -from robotlibcore import KeywordBuilder +from robotlibcore import KeywordBuilder, RF32 from moc_library import MockLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -77,11 +78,18 @@ def test_types(lib): assert spec.argument_types == {'varargs': int, 'other': bool, 'kwargs': int} -def test_optional_none(lib): +@pytest.mark.skipif(not RF32, reason='Only for RF3.2+') +def test_optional_none_rf32(lib): spec = KeywordBuilder.build(lib.optional_none) assert spec.argument_types == {'arg1': str, 'arg2': str} +@pytest.mark.skipif(RF32, reason='Only for RF4') +def test_optional_none_rf4(lib): + spec = KeywordBuilder.build(lib.optional_none) + assert spec.argument_types == {'arg1': typing.Union[str, None], 'arg2': typing.Union[str, None]} + + def test_complex_deco(dyn_types): spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) assert spec.argument_types == {'arg1': bool, 'arg2': bool} From 33d36aee957ca68bd550c23363148e09c5d90fb1 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:14:22 +0300 Subject: [PATCH 016/148] remove debug help --- src/robotlibcore.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 7890945..bbfeb4d 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -49,8 +49,6 @@ def add_library_components(self, library_components): if callable(func) and hasattr(func, 'robot_name'): kw = getattr(component, name) kw_name = func.robot_name or name - if kw_name == "keyword_optional_with_none": - print(kw_name) self.keywords[kw_name] = kw self.keywords_spec[kw_name] = KeywordBuilder.build(kw) # Expose keywords as attributes both using original From 87588d3a1d11df7842955c181bc7ab13baa89186 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:16:22 +0300 Subject: [PATCH 017/148] Unit test improvements --- utest/test_get_keyword_types.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 90876a7..d5cd917 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -187,13 +187,11 @@ def test_keyword_with_decorator_arguments(lib_types): @pytest.mark.skipif(RF32, reason='Only for RF4+') def test_keyword_optional_with_none_rf4(lib_types): - lib = DynamicTypesAnnotationsLibrary("111") - types = lib.get_keyword_types('keyword_optional_with_none') + types = lib_types.get_keyword_types('keyword_optional_with_none') assert types == {'arg': typing.Union[str, type(None)]} @pytest.mark.skipif(not RF32, reason='Only for RF3.2+') def test_keyword_optional_with_none_rf32(lib_types): - lib = DynamicTypesAnnotationsLibrary("111") - types = lib.get_keyword_types('keyword_optional_with_none') + types = lib_types.get_keyword_types('keyword_optional_with_none') assert types == {'arg': str} From d2427eec0bb5c24b065fd9524028c0b6ed054f06 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:27:39 +0300 Subject: [PATCH 018/148] Release notes for 3.0.0 --- docs/PythonLibCore-3.0.0.rst | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/PythonLibCore-3.0.0.rst diff --git a/docs/PythonLibCore-3.0.0.rst b/docs/PythonLibCore-3.0.0.rst new file mode 100644 index 0000000..4aaa1ee --- /dev/null +++ b/docs/PythonLibCore-3.0.0.rst @@ -0,0 +1,90 @@ +========================= +Python Library Core 3.0.0 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 3.0.0 is +a new release with fixing but with RF 4 and typing.Union resulting incorrect +conversion. Also this release drops support for Rf 3.1 + +All issues targeted for Python Library Core v3.0.0 can be found +from the `issue tracker`_. + +**REMOVE ``--pre`` from the next command with final releases.** +If you have pip_ installed, just run + +:: + + pip install --pre --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==3.0.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 3.0.0 was released on Friday June 11, 2021. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av3.0.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== +${None} type conversion does not work correctly (`#81`_) +--------------------------------------------------------- +When argument contained type hint with typing.Union and None default value, +and keyword was used with ${None} default value, then argument was not converted +correctly with RF 4. + +Drop Python 2 and Python 3.5 support (`#76`_) +--------------------------------------------- +Python 2 and 3.5 support is not anymore supported. Many thanks for Hugo van Kemenade for +providing the PR. + +Support only RF 3.2.2 and 4.0.1 (`#80`_) +---------------------------------------- +Support for RF 3.1 is dropped. Only Rf 3.2 and 4.0 are supported by this release. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#81`_ + - bug + - critical + - ${None} type conversion does not work correctly + * - `#76`_ + - enhancement + - critical + - Drop Python 2 and Python 3.5 support + * - `#80`_ + - enhancement + - critical + - Support only RF 3.2.2 and 4.0.1 + +Altogether 3 issues. View on the `issue tracker `__. + +.. _#81: https://github.com/robotframework/PythonLibCore/issues/81 +.. _#76: https://github.com/robotframework/PythonLibCore/issues/76 +.. _#80: https://github.com/robotframework/PythonLibCore/issues/80 From 57ed65d5f5d993e9267d355b80e5ace8dcec6036 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:27:55 +0300 Subject: [PATCH 019/148] Updated version to 3.0.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index bbfeb4d..5ef76fc 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -30,7 +30,7 @@ RF32 = robot_version < '4.' -__version__ = '2.2.2.dev1' +__version__ = '3.0.0' class HybridCore: From 84571b2f24a9977b411b31f30afe5d8b13fe586a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 11 Jun 2021 00:30:01 +0300 Subject: [PATCH 020/148] Back to dev version --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 5ef76fc..51df561 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -30,7 +30,7 @@ RF32 = robot_version < '4.' -__version__ = '3.0.0' +__version__ = '3.0.1.dev1' class HybridCore: From 4847f87997c7619f2bd483c5a3aebcdfa1f9cd2c Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Wed, 1 Sep 2021 21:54:55 +0300 Subject: [PATCH 021/148] Use Python 3.10 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8910347..b6fb085 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.9, pypy3] + python-version: [3.6, 3.9, pypy3, 3.10.0-rc.1] rf-version: [3.2.2, 4.0.1] steps: From 495a1da8b5f6b0ac0fc98877371d7b2170cd2107 Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Wed, 1 Sep 2021 21:58:51 +0300 Subject: [PATCH 022/148] Drop RF 3.2 support --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b6fb085..125a004 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.6, 3.9, pypy3, 3.10.0-rc.1] - rf-version: [3.2.2, 4.0.1] + rf-version: [4.0.2, 4.1.0] steps: - uses: actions/checkout@v2 From 95d7b376aad6da89c7bf9ba8e6801688f35dfd0a Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Thu, 2 Sep 2021 08:27:36 +0300 Subject: [PATCH 023/148] Try out RF 4.1.1rc1 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 125a004..286bb13 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.6, 3.9, pypy3, 3.10.0-rc.1] - rf-version: [4.0.2, 4.1.0] + rf-version: [4.0.2, 4.1.1rc1] steps: - uses: actions/checkout@v2 From 01c1816ca615eb5c29ebd8c81315dee9cbfab28f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 22:26:28 +0300 Subject: [PATCH 024/148] Drop RF32 support more Fixes #85 --- .github/workflows/CI.yml | 2 +- src/robotlibcore.py | 29 ----------------------------- utest/test_get_keyword_types.py | 11 +---------- utest/test_keyword_builder.py | 11 ++--------- 4 files changed, 4 insertions(+), 49 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 286bb13..1aa1d99 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.6, 3.9, pypy3, 3.10.0-rc.1] - rf-version: [4.0.2, 4.1.1rc1] + rf-version: [4.0.2, 4.1.1] steps: - uses: actions/checkout@v2 diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 51df561..28d24a7 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -22,14 +22,11 @@ import os import typing -from robot import __version__ as robot_version from robot.utils import PY_VERSION from robot.api.deco import keyword # noqa F401 -RF32 = robot_version < '4.' - __version__ = '3.0.1.dev1' @@ -244,9 +241,6 @@ def _get_typing_hints(cls, function): # remove return and self statements if arg_with_hint not in all_args: hints.pop(arg_with_hint) - if RF32: - default = cls._get_defaults(arg_spec) - return cls._remove_optional_none_type_hints(hints, default) return hints @classmethod @@ -260,29 +254,6 @@ def _args_as_list(cls, function, arg_spec): function_args.append(arg_spec.varkw) return function_args - # TODO: Remove when support RF 3.2 is dropped - # Copied from: robot.running.arguments.argumentparser - @classmethod - def _remove_optional_none_type_hints(cls, type_hints, defaults): - # If argument has None as a default, typing.get_type_hints adds - # optional None to the information it returns. We don't want that. - for arg, default in defaults: - if default is None and arg in type_hints: - type_ = type_hints[arg] - if cls._is_union(type_): - types = type_.__args__ - if len(types) == 2 and types[1] is type(None): # noqa - type_hints[arg] = types[0] - return type_hints - - # TODO: Remove when support RF 3.2 is dropped - # Copied from: robot.running.arguments.argumentparser - @classmethod - def _is_union(cls, typing_type): - if PY_VERSION >= (3, 7) and hasattr(typing_type, '__origin__'): - typing_type = typing_type.__origin__ - return isinstance(typing_type, type(typing.Union)) - @classmethod def _get_defaults(cls, arg_spec): if not arg_spec.defaults: diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index d5cd917..47eaae1 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,8 +1,6 @@ import pytest import typing -from robotlibcore import RF32 - from typing import List, Union from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -185,13 +183,6 @@ def test_keyword_with_decorator_arguments(lib_types): assert types == {'arg1': bool, 'arg2': bool} -@pytest.mark.skipif(RF32, reason='Only for RF4+') -def test_keyword_optional_with_none_rf4(lib_types): +def test_keyword_optional_with_none(lib_types): types = lib_types.get_keyword_types('keyword_optional_with_none') assert types == {'arg': typing.Union[str, type(None)]} - - -@pytest.mark.skipif(not RF32, reason='Only for RF3.2+') -def test_keyword_optional_with_none_rf32(lib_types): - types = lib_types.get_keyword_types('keyword_optional_with_none') - assert types == {'arg': str} diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index eb956ee..dc8f3fb 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -1,7 +1,7 @@ import pytest import typing -from robotlibcore import KeywordBuilder, RF32 +from robotlibcore import KeywordBuilder from moc_library import MockLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -78,14 +78,7 @@ def test_types(lib): assert spec.argument_types == {'varargs': int, 'other': bool, 'kwargs': int} -@pytest.mark.skipif(not RF32, reason='Only for RF3.2+') -def test_optional_none_rf32(lib): - spec = KeywordBuilder.build(lib.optional_none) - assert spec.argument_types == {'arg1': str, 'arg2': str} - - -@pytest.mark.skipif(RF32, reason='Only for RF4') -def test_optional_none_rf4(lib): +def test_optional_none(lib): spec = KeywordBuilder.build(lib.optional_none) assert spec.argument_types == {'arg1': typing.Union[str, None], 'arg2': typing.Union[str, None]} From 3338deb9af46ad0e0e2d40c1e60bf30b62b4e550 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 22:32:25 +0300 Subject: [PATCH 025/148] Fix lint error --- src/robotlibcore.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 28d24a7..56ae0b1 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -22,9 +22,6 @@ import os import typing -from robot.utils import PY_VERSION - - from robot.api.deco import keyword # noqa F401 __version__ = '3.0.1.dev1' From a5d75ccb43cad4fa316fad94ee0404337408b059 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 22:41:32 +0300 Subject: [PATCH 026/148] Lint fixes with black and isort --- .flake8 | 5 ++++ .github/workflows/CI.yml | 7 +++-- requirements-dev.txt | 2 ++ src/robotlibcore.py | 57 ++++++++++++++++++---------------------- tasks.py | 9 +++++++ 5 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0014793 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +exclude = + __pycache__, +ignore = E203 +max-line-length = 120 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1aa1d99..74ea4db 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.9, pypy3, 3.10.0-rc.1] + python-version: [3.6, 3.9, 3.10.0-rc.1] rf-version: [4.0.2, 4.1.1] steps: @@ -26,7 +26,10 @@ jobs: pip install -U --pre robotframework==${{ matrix.rf-version }} - name: Run flake8 run: | - flake8 --max-line-length=110 src/ + flake8 --config .flake8 src/ + - name: Run balck + run: | + black --target-version py36 --line-length 120 --check src/ - name: Run unit tests run: | python utest/run.py diff --git a/requirements-dev.txt b/requirements-dev.txt index ca8a225..885b302 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,5 @@ pytest-cov pytest-mockito robotstatuschecker flake8 +black +isort diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 56ae0b1..16ef2c4 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -24,11 +24,10 @@ from robot.api.deco import keyword # noqa F401 -__version__ = '3.0.1.dev1' +__version__ = "3.0.1.dev1" class HybridCore: - def __init__(self, library_components): self.keywords = {} self.keywords_spec = {} @@ -37,10 +36,10 @@ def __init__(self, library_components): self.add_library_components([self]) def add_library_components(self, library_components): - self.keywords_spec['__init__'] = KeywordBuilder.build(self.__init__) + self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) for component in library_components: for name, func in self.__get_members(component): - if callable(func) and hasattr(func, 'robot_name'): + if callable(func) and hasattr(func, "robot_name"): kw = getattr(component, name) kw_name = func.robot_name or name self.keywords[kw_name] = kw @@ -53,12 +52,14 @@ def __get_members(self, component): if inspect.ismodule(component): return inspect.getmembers(component) if inspect.isclass(component): - raise TypeError('Libraries must be modules or instances, got ' - 'class {!r} instead.'.format(component.__name__)) + raise TypeError( + "Libraries must be modules or instances, got " "class {!r} instead.".format(component.__name__) + ) if type(component) != component.__class__: - raise TypeError('Libraries must be modules or new-style class ' - 'instances, got old-style class {!r} instead.' - .format(component.__class__.__name__)) + raise TypeError( + "Libraries must be modules or new-style class " + "instances, got old-style class {!r} instead.".format(component.__class__.__name__) + ) return self.__get_members_from_instance(component) def __get_members_from_instance(self, instance): @@ -71,8 +72,7 @@ def __get_members_from_instance(self, instance): def __getattr__(self, name): if name in self.attributes: return self.attributes[name] - raise AttributeError('{!r} object has no attribute {!r}' - .format(type(self).__name__, name)) + raise AttributeError("{!r} object has no attribute {!r}".format(type(self).__name__, name)) def __dir__(self): my_attrs = super().__dir__() @@ -83,7 +83,6 @@ def get_keyword_names(self): class DynamicCore(HybridCore): - def run_keyword(self, name, args, kwargs=None): return self.keywords[name](*args, **(kwargs or {})) @@ -95,8 +94,8 @@ 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 '' + if name == "__intro__": + return inspect.getdoc(self) or "" spec = self.keywords_spec.get(name) return spec.documentation @@ -107,9 +106,9 @@ def get_keyword_types(self, name): return spec.argument_types def __get_keyword(self, keyword_name): - if keyword_name == '__init__': + if keyword_name == "__init__": return self.__init__ - if keyword_name.startswith('__') and keyword_name.endswith('__'): + if keyword_name.startswith("__") and keyword_name.endswith("__"): return None method = self.keywords.get(keyword_name) if not method: @@ -121,11 +120,11 @@ def get_keyword_source(self, 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) + return "{}:{}".format(path, line_number) if path: return path if line_number: - return ':%s' % line_number + return ":%s" % line_number return None def __get_keyword_line(self, method): @@ -134,7 +133,7 @@ def __get_keyword_line(self, method): except (OSError, TypeError): return None for increment, line in enumerate(lines): - if line.strip().startswith('def '): + if line.strip().startswith("def "): return line_number + increment return line_number @@ -146,13 +145,12 @@ def __get_keyword_path(self, method): class KeywordBuilder: - @classmethod def build(cls, function): return KeywordSpecification( argument_specification=cls._get_arguments(function), - documentation=inspect.getdoc(function) or '', - argument_types=cls._get_types(function) + documentation=inspect.getdoc(function) or "", + argument_types=cls._get_types(function), ) @classmethod @@ -163,9 +161,7 @@ def unwrap(cls, function): def _get_arguments(cls, function): unwrap_function = cls.unwrap(function) arg_spec = cls._get_arg_spec(unwrap_function) - argument_specification = cls._get_default_and_named_args( - arg_spec, function - ) + argument_specification = cls._get_default_and_named_args(arg_spec, function) argument_specification.extend(cls._get_var_args(arg_spec)) kw_only_args = cls._get_kw_only(arg_spec) if kw_only_args: @@ -198,12 +194,12 @@ def _drop_self_from_args(cls, function, arg_spec): @classmethod def _get_var_args(cls, arg_spec): if arg_spec.varargs: - return ['*%s' % arg_spec.varargs] + return ["*%s" % arg_spec.varargs] return [] @classmethod def _get_kwargs(cls, arg_spec): - return ['**%s' % arg_spec.varkw] if arg_spec.varkw else [] + return ["**%s" % arg_spec.varkw] if arg_spec.varkw else [] @classmethod def _get_kw_only(cls, arg_spec): @@ -212,7 +208,7 @@ def _get_kw_only(cls, arg_spec): if not arg_spec.kwonlydefaults or arg not in arg_spec.kwonlydefaults: kw_only_args.append(arg) else: - value = arg_spec.kwonlydefaults.get(arg, '') + value = arg_spec.kwonlydefaults.get(arg, "") kw_only_args.append((arg, value)) return kw_only_args @@ -220,7 +216,7 @@ def _get_kw_only(cls, arg_spec): def _get_types(cls, function): if function is None: return function - types = getattr(function, 'robot_types', ()) + types = getattr(function, "robot_types", ()) if types is None or types: return types return cls._get_typing_hints(function) @@ -255,12 +251,11 @@ def _args_as_list(cls, function, arg_spec): def _get_defaults(cls, arg_spec): if not arg_spec.defaults: return {} - names = arg_spec.args[-len(arg_spec.defaults):] + 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): self.argument_specification = argument_specification self.documentation = documentation diff --git a/tasks.py b/tasks.py index e681d7d..e4f0627 100644 --- a/tasks.py +++ b/tasks.py @@ -118,3 +118,12 @@ def init_labels(ctx, username=None, password=None): when labels it uses have changed. """ initialize_labels(REPOSITORY, username, password) + +@task +def lint(ctx): + print("Run flake8") + ctx.run("flake8 --config .flake8 src/") + print("Run black") + ctx.run("black --target-version py36 --line-length 120 src/") + print("Run isort") + ctx.run("isort src/") From f0347000a101495055d09502a3c55d3452b6c1e8 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 23:03:25 +0300 Subject: [PATCH 027/148] Lint Robot files --- atest/tests.robot | 31 ++++++++++++++++--------------- atest/tests_types.robot | 30 +++++++----------------------- requirements-dev.txt | 1 + tasks.py | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/atest/tests.robot b/atest/tests.robot index b279fe0..12571b1 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -1,8 +1,8 @@ *** Settings *** -Library ${LIBRARY}.py +Library ${LIBRARY}.py *** Variables *** -${LIBRARY} DynamicLibrary +${LIBRARY} DynamicLibrary *** Test Cases *** Keyword names @@ -12,8 +12,9 @@ Keyword names Method Custom name Cust omna me - Run Keyword If $LIBRARY == "ExtendExistingLibrary" - ... Keyword in extending library + IF $LIBRARY == "ExtendExistingLibrary" + Keyword in extending library + END Method without @keyword are not keyowrds [Documentation] FAIL GLOB: No keyword with name 'Not keyword' found.* @@ -21,22 +22,22 @@ Method without @keyword are not keyowrds Arguments [Template] Return value should be - 'foo', 'bar' Mandatory foo bar - 'foo', 'default', 3 Defaults foo - 'foo', 2, 3 Defaults foo ${2} - 'a', 'b', 'c' Defaults a b c + 'foo', 'bar' Mandatory foo bar + 'foo', 'default', 3 Defaults foo + 'foo', 2, 3 Defaults foo ${2} + 'a', 'b', 'c' Defaults a b c Named arguments [Template] Return value should be - 'foo', 'bar' Mandatory foo arg2=bar - '1', 2 Mandatory arg2=${2} arg1=1 - 'x', 'default', 'y' Defaults x arg3=y + 'foo', 'bar' Mandatory foo arg2=bar + '1', 2 Mandatory arg2=${2} arg1=1 + 'x', 'default', 'y' Defaults x arg3=y Varargs and kwargs [Template] Return value should be - ${EMPTY} Varargs and kwargs - 'a', 'b', 'c' Varargs and kwargs a b c - a\='1', b\=2 Varargs and kwargs a=1 b=${2} + ${EMPTY} Varargs and kwargs + 'a', 'b', 'c' Varargs and kwargs a b c + a\='1', b\=2 Varargs and kwargs a=1 b=${2} 'a', 'b\=b', c\='c' Varargs and kwargs a b\=b c=c Embedded arguments @@ -47,5 +48,5 @@ Embedded arguments *** Keywords *** Return value should be [Arguments] ${expected} ${keyword} @{args} &{kwargs} - ${result} = Run Keyword ${keyword} @{args} &{kwargs} + ${result} Run Keyword ${keyword} @{args} &{kwargs} Should Be Equal ${result} ${expected} diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 436c8c0..7299790 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -1,7 +1,6 @@ *** Settings *** -Library DynamicTypesLibrary.py -Library DynamicTypesAnnotationsLibrary.py xxx -Suite Setup Import DynamicTypesAnnotationsLibrary In Python 3 Only +Library DynamicTypesLibrary.py +Library DynamicTypesAnnotationsLibrary.py xxx *** Test Cases *** Keyword Default Argument As Abject None @@ -17,7 +16,7 @@ Keyword Default Argument As String None Should Match Regexp ${return} None: <(class|type) '(unicode|str|NoneType)'> Keyword Default As Booleans With Defaults - ${return} DynamicTypesLibrary.Keyword Booleans + ${return} = DynamicTypesLibrary.Keyword Booleans Should Match Regexp ${return} True: <(class|type) 'bool'>, False: <(class|type) 'bool'> Keyword Default As Booleans With Objects @@ -25,52 +24,43 @@ Keyword Default As Booleans With Objects Should Match Regexp ${return} False: <(class|type) 'bool'>, True: <(class|type) 'bool'> Keyword Annonations And Bool Defaults Using Default - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Default And Annotation 42 Should Match Regexp ${return} 42: <(class|type) 'int'>, False: <(class|type) 'bool'> Keyword Annonations And Bool Defaults Defining All Arguments - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Default And Annotation 1 true Should Match Regexp ${return} 1: <(class|type) 'int'>, true: <(class|type) 'str'> Keyword Annonations And Bool Defaults Defining All Arguments And With Number - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Default And Annotation ${1} true Should Match Regexp ${return} 1: <(class|type) 'int'>, true: <(class|type) 'str'> Keyword Annonations And Robot Types Disbales Argument Conversion - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Robot Types Disabled And Annotations 111 Should Match Regexp ${return} 111: <(class|type) 'str'> Keyword Annonations And Keyword Only Arguments - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments 1 ${1} some=222 Should Match Regexp ${return} \\('1', 1\\): , 222: Keyword Only Arguments Without VarArg - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments No Vararg other=tidii Should Match ${return} tidii: Varargs and KeywordArgs With Typing Hints - [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Self And Keyword Only Types ... this_is_mandatory # mandatory argument - ... 1 2 3 4 # varargs - ... other=True # other argument - ... key1=1 key2=2 # kwargs - Should Match ${return} + ... 1 2 3 4 # varargs + ... other=True # other argument + ... key1=1 key2=2 # kwargs + Should Match ${return} ... this_is_mandatory: , (1, 2, 3, 4): , True: , {'key1': 1, 'key2': 2}: Enum Conversion Should Work - [Tags] py3 ${value} = Enum Conversion ok Should Match OK penum.ok ${value} Enum Conversion To Invalid Value Should Fail - [Tags] py3 Run Keyword And Expect Error ValueError: Argument 'param' got value 'not ok' that* ... Enum Conversion not ok @@ -84,9 +74,3 @@ Type Conversion With Optional And None ${types} = Keyword Optional With None ${None} Should Contain ${types} arg: None, Should Contain ${types} - -*** Keywords *** -Import DynamicTypesAnnotationsLibrary In Python 3 Only - ${py3} = DynamicTypesLibrary.Is Python 3 - Run Keyword If ${py3} - ... Import Library DynamicTypesAnnotationsLibrary.py Dummy diff --git a/requirements-dev.txt b/requirements-dev.txt index 885b302..124691c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ robotstatuschecker flake8 black isort +robotframework-tidy \ No newline at end of file diff --git a/tasks.py b/tasks.py index e4f0627..cb1807a 100644 --- a/tasks.py +++ b/tasks.py @@ -1,3 +1,4 @@ +import os import sys from pathlib import Path @@ -127,3 +128,16 @@ def lint(ctx): ctx.run("black --target-version py36 --line-length 120 src/") print("Run isort") ctx.run("isort src/") + print("Run tidy") + in_ci = os.getenv("GITHUB_WORKFLOW") + print(f"Lint Robot files {'in ci' if in_ci else ''}") + command = [ + "robotidy", + "--lineseparator", + "unix", + "atest/", + ] + if in_ci: + command.insert(1, "--check") + command.insert(1, "--diff") + ctx.run(" ".join(command)) From b16bcebb60fcc8b1493693757547c9585067610e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 23:07:16 +0300 Subject: [PATCH 028/148] Drop Python 3.6 --- .github/workflows/CI.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 74ea4db..ef3d47b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.9, 3.10.0-rc.1] + python-version: [3.7, 3.9, 3.10.0-rc.1] rf-version: [4.0.2, 4.1.1] steps: diff --git a/setup.py b/setup.py index 5997895..a182fcb 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ keywords = 'robotframework testing testautomation library development', platforms = 'any', classifiers = CLASSIFIERS, - python_requires = '>=3.6, <4', + python_requires = '>=3.7, <4', package_dir = {'': 'src'}, packages = find_packages('src'), py_modules = ['robotlibcore'], From 3799829b545312db1c2f1d001bda6e0d5d9bcce6 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Sep 2021 23:59:32 +0300 Subject: [PATCH 029/148] More tests --- atest/DynamicTypesAnnotationsLibrary.py | 6 +++++- atest/tests_types.robot | 17 +++++++++++++++++ utest/test_get_keyword_types.py | 5 +++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 659e262..7b536c4 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -1,6 +1,6 @@ from enum import Enum from functools import wraps -from typing import List, Union, NewType, Optional, Tuple +from typing import List, Union, NewType, Optional, Tuple, Dict from robot.api import logger @@ -152,3 +152,7 @@ def keyword_with_deco_and_signature(self, arg1: bool = False, arg2: bool = False @keyword def keyword_optional_with_none(self, arg: Optional[str] = None): return f"arg: {arg}, type: {type(arg)}" + + @keyword + def keyword_union_with_none(self, arg: Union[None, Dict, str] = None): + return f"arg: {arg}, type: {type(arg)}" diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 7299790..eee8e48 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -2,6 +2,9 @@ Library DynamicTypesLibrary.py Library DynamicTypesAnnotationsLibrary.py xxx +*** Variables *** +${CUSTOM NONE} = ${None} + *** Test Cases *** Keyword Default Argument As Abject None ${return} = DynamicTypesLibrary.Keyword None ${None} @@ -74,3 +77,17 @@ Type Conversion With Optional And None ${types} = Keyword Optional With None ${None} Should Contain ${types} arg: None, Should Contain ${types} + ${types} = Keyword Optional With None arg=${CUSTOM NONE} + Should Contain ${types} arg: None, + Should Contain ${types} + +Type Conversion With Union And Multiple Types + ${types} = Keyword Union With None + Should Contain ${types} arg: None, + Should Contain ${types} + ${types} = Keyword Union With None None + Should Contain ${types} arg: None, + Should Contain ${types} + ${types} = Keyword Union With None {"key": 1} + Should Contain ${types} arg: {"key": 1}, + Should Contain ${types} diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 47eaae1..7c38f1a 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -186,3 +186,8 @@ def test_keyword_with_decorator_arguments(lib_types): def test_keyword_optional_with_none(lib_types): types = lib_types.get_keyword_types('keyword_optional_with_none') assert types == {'arg': typing.Union[str, type(None)]} + + +def test_keyword_union_with_none(lib_types): + types = lib_types.get_keyword_types('keyword_union_with_none') + assert types == {'arg': typing.Union[type(None), typing.Dict, str]} From 0088e4da8ecb0f78127d394d8b72e94c18d1373d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 21:26:51 +0300 Subject: [PATCH 030/148] Python 3.10 type hint test --- .github/workflows/CI.yml | 2 +- atest/DynamicTypesLibrary.py | 4 ++-- atest/Python310Library.py | 14 ++++++++++++++ atest/run.py | 3 ++- atest/tests_types.robot | 14 ++++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 atest/Python310Library.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ef3d47b..18291e5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.9, 3.10.0-rc.1] + python-version: [3.7, 3.9, 3.10.0-rc.2] rf-version: [4.0.2, 4.1.1] steps: diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index 3626fe7..0d079ed 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -58,8 +58,8 @@ def keyword_none(self, arg=None): return '{}: {}'.format(arg, type(arg)) @keyword - def is_python_3(self): - return sys.version_info >= (3,) + def is_python_3_10(self): + return sys.version_info >= (3, 10) @keyword @def_deco diff --git a/atest/Python310Library.py b/atest/Python310Library.py new file mode 100644 index 0000000..d773b0b --- /dev/null +++ b/atest/Python310Library.py @@ -0,0 +1,14 @@ +from robot.api import logger + +from robotlibcore import DynamicCore, keyword + +class Python310Library(DynamicCore): + + def __init__(self): + DynamicCore.__init__(self, []) + + @keyword + def python310_style(self, arg: int | dict): + typing = f"arg: {arg}, type: {type(arg)}" + logger.info(typing) + return typing diff --git a/atest/run.py b/atest/run.py index c4e4e30..26df6b7 100755 --- a/atest/run.py +++ b/atest/run.py @@ -25,7 +25,8 @@ sys.exit(rc) process_output(output, verbose=False) output = join(outdir, 'lib-DynamicTypesLibrary-python-{}-robot-{}.xml'.format(python_version, rf_version)) -rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug') +exclude = "py310" if sys.version_info < (3, 10) else "" +rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug', exclude=exclude) if rc > 250: sys.exit(rc) process_output(output, verbose=False) diff --git a/atest/tests_types.robot b/atest/tests_types.robot index eee8e48..66874a0 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -91,3 +91,17 @@ Type Conversion With Union And Multiple Types ${types} = Keyword Union With None {"key": 1} Should Contain ${types} arg: {"key": 1}, Should Contain ${types} + +Python 3.10 New Type Hints + [Tags] py310 + [Setup] Import DynamicTypesAnnotationsLibrary In Python 3.10 Only + ${types} = Python310 Style 111 + Should Be Equal ${types} arg: 111, type: + ${types} = Python310 Style {"key": 1} + Should Be Equal ${types} arg: {'key': 1}, type: + +*** Keywords *** +Import DynamicTypesAnnotationsLibrary In Python 3.10 Only + ${py3} = DynamicTypesLibrary.Is Python 3 10 + Run Keyword If ${py3} + ... Import Library Python310Library.py From bcbdcdef9ce59a375193535ef9284fb3ba99ca3d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 21:32:50 +0300 Subject: [PATCH 031/148] RF 4.0.1 fix --- atest/DynamicTypesLibrary.py | 6 ++++++ atest/tests_types.robot | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index 0d079ed..1ea9e8c 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -1,6 +1,8 @@ import functools import sys +from robot import version as rf_version + from robotlibcore import DynamicCore, keyword @@ -61,6 +63,10 @@ def keyword_none(self, arg=None): def is_python_3_10(self): return sys.version_info >= (3, 10) + @keyword + def is_rf_401(self): + return "4.0." in rf_version.VERSION + @keyword @def_deco def keyword_with_def_deco(self): diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 66874a0..0ccb842 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -96,6 +96,12 @@ Python 3.10 New Type Hints [Tags] py310 [Setup] Import DynamicTypesAnnotationsLibrary In Python 3.10 Only ${types} = Python310 Style 111 + ${rf401} = DynamicTypesLibrary.Is Rf 401 + IF ${rf401} != ${True} + Should Be Equal ${types} arg: 111, type: + ELSE + Should Be Equal ${types} arg: 111, type: + END Should Be Equal ${types} arg: 111, type: ${types} = Python310 Style {"key": 1} Should Be Equal ${types} arg: {'key': 1}, type: @@ -103,5 +109,6 @@ Python 3.10 New Type Hints *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3.10 Only ${py3} = DynamicTypesLibrary.Is Python 3 10 - Run Keyword If ${py3} - ... Import Library Python310Library.py + IF ${py3} + Import Library Python310Library.py + END From 8283032c02c1237577e8410b8fd0a9bddab3c156 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 21:44:35 +0300 Subject: [PATCH 032/148] Fix unit test --- atest/DynamicTypesLibrary.py | 8 ++++---- utest/test_get_keyword_source.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index 1ea9e8c..b196fbd 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -63,10 +63,6 @@ def keyword_none(self, arg=None): def is_python_3_10(self): return sys.version_info >= (3, 10) - @keyword - def is_rf_401(self): - return "4.0." in rf_version.VERSION - @keyword @def_deco def keyword_with_def_deco(self): @@ -85,3 +81,7 @@ def varargs_and_kwargs(self, *args, **kwargs): @keyword def keyword_booleans(self, arg1=True, arg2=False): return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) + + @keyword + def is_rf_401(self): + return "4.0." in rf_version.VERSION \ No newline at end of file diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 95d65a0..212e19d 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -50,7 +50,7 @@ def test_location_in_class(lib, lib_path_components): def test_decorator_wrapper(lib_types, lib_path_types): source = lib_types.get_keyword_source('keyword_wrapped') - assert source == '%s:72' % lib_path_types + assert source == '%s:74' % lib_path_types def test_location_in_class_custom_keyword_name(lib, lib_path_components): @@ -79,7 +79,7 @@ def test_no_path_and_no_line_number(lib, when): def test_def_in_decorator(lib_types, lib_path_types): source = lib_types.get_keyword_source('keyword_with_def_deco') - assert source == '%s:66' % lib_path_types + assert source == '%s:68' % lib_path_types def test_error_in_getfile(lib, when): From 1c9e5ca59fd895487480eed8654ba91d82af864e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 21:47:52 +0300 Subject: [PATCH 033/148] Always upload artifacts --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 18291e5..b5e4f9a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,6 +37,7 @@ jobs: run: | python atest/run.py - uses: actions/upload-artifact@v1 + if: ${{ always() }} with: name: atest_results path: atest/results From 0eac720f1090461e6c733d86914af46e3c321aef Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 21:49:37 +0300 Subject: [PATCH 034/148] Fix atest --- atest/tests_types.robot | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 0ccb842..ab9af7d 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -102,9 +102,12 @@ Python 3.10 New Type Hints ELSE Should Be Equal ${types} arg: 111, type: END - Should Be Equal ${types} arg: 111, type: ${types} = Python310 Style {"key": 1} - Should Be Equal ${types} arg: {'key': 1}, type: + IF ${rf401} != ${True} + Should Be Equal ${types} arg: {'key': 1}, type: + ELSE + Should Be Equal ${types} arg: {"key": 1}, type: + END *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3.10 Only From f831f315692998101fe5279d7b63b06b0c798694 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 20 Sep 2021 22:00:42 +0300 Subject: [PATCH 035/148] Fix Python version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a182fcb..02c227b 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,10 @@ License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python :: 3 -Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 +Programming Language :: Python :: 3.10 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From d6d8fbe743e1f1fd1a50298e51c88dd65f9ee19a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 15 Oct 2022 21:42:25 +0300 Subject: [PATCH 036/148] Update build deps --- requirements-build.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-build.txt b/requirements-build.txt index 2bc9f42..ddb9830 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,6 +1,6 @@ # Requirements needed when generating releases. See BUILD.rst for details. -invoke >= 0.20 -rellu >= 0.6 +invoke >= 1.7.3 +rellu >= 0.7 twine wheel From ef35336fbc895d3d329a6d61be52501c2a5a54d4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 15 Oct 2022 21:44:31 +0300 Subject: [PATCH 037/148] Update CI deps --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b5e4f9a..151edf9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,8 +8,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.9, 3.10.0-rc.2] - rf-version: [4.0.2, 4.1.1] + python-version: [3.7, 3.10.7] + rf-version: [5.0.1, 4.1.3] steps: - uses: actions/checkout@v2 From 7f98a1029af2fdb991c0a915c960d9600239008d Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Mon, 24 Oct 2022 23:08:03 +0300 Subject: [PATCH 038/148] Use RF 6.0 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 151edf9..8692e7a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.7, 3.10.7] - rf-version: [5.0.1, 4.1.3] + rf-version: [5.0.1, 6.0.0] steps: - uses: actions/checkout@v2 From 835a2da7faa829b52a2b2fe62ab733d910836945 Mon Sep 17 00:00:00 2001 From: Tatu Aalto <2665023+aaltat@users.noreply.github.com> Date: Fri, 4 Nov 2022 23:10:37 +0200 Subject: [PATCH 039/148] Update RF 6.0.1 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8692e7a..7c578fc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.7, 3.10.7] - rf-version: [5.0.1, 6.0.0] + rf-version: [5.0.1, 6.0.1] steps: - uses: actions/checkout@v2 From bbb648cc04bdbfacfb12725a57b514fe71e9a61c Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 15 Oct 2022 22:40:23 +0300 Subject: [PATCH 040/148] Add testing in invoke --- tasks.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/tasks.py b/tasks.py index cb1807a..7124ca8 100644 --- a/tasks.py +++ b/tasks.py @@ -3,18 +3,17 @@ from pathlib import Path from invoke import task -from rellu import initialize_labels, ReleaseNotesGenerator, Version -from rellu.tasks import clean - +from rellu import ReleaseNotesGenerator, Version, initialize_labels +from rellu.tasks import clean # noqa assert Path.cwd() == Path(__file__).parent -REPOSITORY = 'robotframework/PythonLibCore' -VERSION_PATH = Path('src/robotlibcore.py') -RELEASE_NOTES_PATH = Path('docs/PythonLibCore-{version}.rst') -RELEASE_NOTES_TITLE = 'Python Library Core {version}' -RELEASE_NOTES_INTRO = ''' +REPOSITORY = "robotframework/PythonLibCore" +VERSION_PATH = Path("src/robotlibcore.py") +RELEASE_NOTES_PATH = Path("docs/PythonLibCore-{version}.rst") +RELEASE_NOTES_TITLE = "Python Library Core {version}" +RELEASE_NOTES_INTRO = """ `Python Library Core`_ is a generic component making it easier to create bigger `Robot Framework`_ test libraries. Python Library Core {version} is a new release with **UPDATE** enhancements and bug fixes. **MORE intro stuff** @@ -47,7 +46,7 @@ .. _pip: http://pip-installer.org .. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore .. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3A{version.milestone} -''' +""" @task @@ -99,8 +98,7 @@ def release_notes(ctx, version=None, username=None, password=None, write=False): """ version = Version(version, VERSION_PATH) file = RELEASE_NOTES_PATH if write else sys.stdout - generator = ReleaseNotesGenerator(REPOSITORY, RELEASE_NOTES_TITLE, - RELEASE_NOTES_INTRO) + generator = ReleaseNotesGenerator(REPOSITORY, RELEASE_NOTES_TITLE, RELEASE_NOTES_INTRO) generator.generate(version, username, password, file) @@ -120,14 +118,15 @@ def init_labels(ctx, username=None, password=None): """ initialize_labels(REPOSITORY, username, password) + @task def lint(ctx): print("Run flake8") - ctx.run("flake8 --config .flake8 src/") + ctx.run("flake8 --config .flake8 src/ tasks.py") print("Run black") - ctx.run("black --target-version py36 --line-length 120 src/") + ctx.run("black --target-version py36 --line-length 120 src/ tasks.py") print("Run isort") - ctx.run("isort src/") + ctx.run("isort src/ tasks.py") print("Run tidy") in_ci = os.getenv("GITHUB_WORKFLOW") print(f"Lint Robot files {'in ci' if in_ci else ''}") @@ -141,3 +140,18 @@ def lint(ctx): command.insert(1, "--check") command.insert(1, "--diff") ctx.run(" ".join(command)) + + +@task +def atest(ctx): + ctx.run("python atest/run.py") + + +@task +def utest(ctx): + ctx.run("python utest/run.py") + + +@task(utest, atest) +def test(ctx): + pass From d77e5433f086442415a3b7d2839ce6a9b3a37e6f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 15 Oct 2022 22:43:45 +0300 Subject: [PATCH 041/148] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5682d75..4f4a815 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv build/ develop-eggs/ dist/ From cd6b0f21a6a9270e86fede084312cd591a948a31 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 16 Oct 2022 00:24:43 +0300 Subject: [PATCH 042/148] Runner improvements --- atest/run.py | 67 +++++++++++++++++++++++++++++++++++----------------- tasks.py | 6 ++--- utest/run.py | 26 ++++++++++---------- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/atest/run.py b/atest/run.py index 26df6b7..18577da 100755 --- a/atest/run.py +++ b/atest/run.py @@ -1,42 +1,65 @@ #!/usr/bin/env python - - import platform -from os.path import abspath, dirname, join +import shutil import sys +from os.path import abspath, dirname, isdir, join +from pathlib import Path -from robot import run, rebot +from robot import rebot, run from robot.version import VERSION as rf_version from robotstatuschecker import process_output - -library_variants = ['Hybrid', 'Dynamic', 'ExtendExisting'] +library_variants = ["Hybrid", "Dynamic", "ExtendExisting"] curdir = dirname(abspath(__file__)) -outdir = join(curdir, 'results') -tests = join(curdir, 'tests.robot') -tests_types = join(curdir, 'tests_types.robot') -sys.path.insert(0, join(curdir, '..', 'src')) +outdir = join(curdir, "results") +if isdir(outdir): + shutil.rmtree(outdir, ignore_errors=True) +tests = join(curdir, "tests.robot") +tests_types = join(curdir, "tests_types.robot") +plugin_api = join(curdir, "plugin_api") +sys.path.insert(0, join(curdir, "..", "src")) python_version = platform.python_version() for variant in library_variants: - output = join(outdir, 'lib-{}-python-{}-robot-{}.xml'.format(variant, python_version, rf_version)) - rc = run(tests, name=variant, variable='LIBRARY:%sLibrary' % variant, - output=output, report=None, log=None, loglevel='debug') + output = join(outdir, "lib-{}-python-{}-robot-{}.xml".format(variant, python_version, rf_version)) + rc = run( + tests, + name=variant, + variable="LIBRARY:%sLibrary" % variant, + output=output, + report=None, + log=None, + loglevel="debug", + ) if rc > 250: sys.exit(rc) process_output(output, verbose=False) -output = join(outdir, 'lib-DynamicTypesLibrary-python-{}-robot-{}.xml'.format(python_version, rf_version)) +output = join(outdir, "lib-DynamicTypesLibrary-python-{}-robot-{}.xml".format(python_version, rf_version)) exclude = "py310" if sys.version_info < (3, 10) else "" -rc = run(tests_types, name='Types', output=output, report=None, log=None, loglevel='debug', exclude=exclude) +rc = run(tests_types, name="Types", output=output, report=None, log=None, loglevel="debug", exclude=exclude) +if rc > 250: + sys.exit(rc) +process_output(output, verbose=False) +output = join(outdir, "lib-PluginApi-python-{}-robot-{}.xml".format(python_version, rf_version)) +rc = run(plugin_api, name="Plugin", output=output, report=None, log=None, loglevel="debug") if rc > 250: sys.exit(rc) process_output(output, verbose=False) -print('\nCombining results.') -library_variants.append('DynamicTypesLibrary') -rc = rebot(*(join(outdir, 'lib-{}-python-{}-robot-{}.xml'.format(variant, python_version, rf_version)) for variant in library_variants), - **dict(name='Acceptance Tests', outputdir=outdir, log='log-python-{}-robot-{}.html'.format(python_version, rf_version), - report='report-python-{}-robot-{}.html'.format(python_version, rf_version))) +print("\nCombining results.") +library_variants.append("DynamicTypesLibrary") +xml_files = [str(xml_file) for xml_file in Path(outdir).glob("*.xml")] +for xxx in xml_files: + print(xxx) +rc = rebot( + *xml_files, + **dict( + name="Acceptance Tests", + outputdir=outdir, + log="log-python-{}-robot-{}.html".format(python_version, rf_version), + report="report-python-{}-robot-{}.html".format(python_version, rf_version), + ), +) if rc == 0: - print('\nAll tests passed/failed as expected.') + print("\nAll tests passed/failed as expected.") else: - print('\n%d test%s failed.' % (rc, 's' if rc != 1 else '')) + print("\n%d test%s failed." % (rc, "s" if rc != 1 else "")) sys.exit(rc) diff --git a/tasks.py b/tasks.py index 7124ca8..0a1d7df 100644 --- a/tasks.py +++ b/tasks.py @@ -122,11 +122,11 @@ def init_labels(ctx, username=None, password=None): @task def lint(ctx): print("Run flake8") - ctx.run("flake8 --config .flake8 src/ tasks.py") + ctx.run("flake8 --config .flake8 src/ tasks.py utest/run.py atest/run.py") print("Run black") - ctx.run("black --target-version py36 --line-length 120 src/ tasks.py") + ctx.run("black --target-version py36 --line-length 120 src/ tasks.py utest/run.py atest/run.py") print("Run isort") - ctx.run("isort src/ tasks.py") + ctx.run("isort src/ tasks.py utest/run.py atest/run.py") print("Run tidy") in_ci = os.getenv("GITHUB_WORKFLOW") print(f"Lint Robot files {'in ci' if in_ci else ''}") diff --git a/utest/run.py b/utest/run.py index f5a8c26..0e877bf 100755 --- a/utest/run.py +++ b/utest/run.py @@ -1,34 +1,36 @@ #!/usr/bin/env python import argparse import platform -from os.path import abspath, dirname, join import sys +from os.path import abspath, dirname, join import pytest from robot.version import VERSION as rf_version curdir = dirname(abspath(__file__)) -atest_dir = join(curdir, '..', 'atest') +atest_dir = join(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 = join(atest_dir, "results", "xunit-python-{}-robot{}.xml".format(python_version, rf_version)) +src = join(curdir, "..", "src") sys.path.insert(0, src) sys.path.insert(0, atest_dir) parser = argparse.ArgumentParser() -parser.add_argument('--no-cov', dest='cov', action='store_false') -parser.add_argument('--cov', dest='cov', action='store_true') +parser.add_argument("--no-cov", dest="cov", action="store_false") +parser.add_argument("--cov", dest="cov", action="store_true") parser.set_defaults(cov=True) args = parser.parse_args() pytest_args = [ - '-p', 'no:cacheprovider', - '--junitxml=%s' % xunit_report, - '-o', 'junit_family=xunit2', - '--showlocals', - curdir + "-p", + "no:cacheprovider", + "--junitxml=%s" % xunit_report, + "-o", + "junit_family=xunit2", + "--showlocals", + curdir, ] if args.cov: - pytest_args.insert(0, '--cov=%s' % src) + pytest_args.insert(0, "--cov=%s" % src) rc = pytest.main(pytest_args) sys.exit(rc) From 272efd926efd47f9477bfa2790ab2975c13c6388 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 16 Oct 2022 00:42:55 +0300 Subject: [PATCH 043/148] Delete unsued static core --- atest/StaticLibrary.py | 23 ---------------- docs/example/01-static/StaticLibrary.py | 36 ------------------------- docs/example/01-static/test.robot | 21 --------------- docs/example/run.py | 4 +-- 4 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 atest/StaticLibrary.py delete mode 100644 docs/example/01-static/StaticLibrary.py delete mode 100644 docs/example/01-static/test.robot diff --git a/atest/StaticLibrary.py b/atest/StaticLibrary.py deleted file mode 100644 index af5a9a9..0000000 --- a/atest/StaticLibrary.py +++ /dev/null @@ -1,23 +0,0 @@ -from robotlibcore import StaticCore, keyword - -import librarycomponents - - -class StaticLibrary(StaticCore, - librarycomponents.Names, - librarycomponents.Arguments, - librarycomponents.DocsAndTags): - """General library documentation.""" - class_attribute = 'not keyword' - - def __init__(self): - self.instance_attribute = 'not keyword' - self.function = librarycomponents.function - StaticCore.__init__(self) - - @keyword - def keyword_in_main(self): - pass - - def not_keyword_in_main(self): - pass diff --git a/docs/example/01-static/StaticLibrary.py b/docs/example/01-static/StaticLibrary.py deleted file mode 100644 index 0236ecf..0000000 --- a/docs/example/01-static/StaticLibrary.py +++ /dev/null @@ -1,36 +0,0 @@ -import time -from typing import Optional - -from robot.api import logger - - -class StaticLibrary: - def __init__(self, separator: str = ";"): - self.separator = separator - - def join_strings(self, *strings: str) -> str: - """Joins args strings.""" - logger.info("Joining.") - return " ".join(strings) - - def sum(self, value1: int, value2: int) -> int: - """Do other thing.""" - logger.info(f"Calculating hard.") - return value1 + value2 - - def wait_something_to_happen(self, arg1: str, arg2: int) -> str: - self._waiter(0.3) - arg1 = self.join_strings(arg1, arg1) - self._waiter(0.2) - arg2 = self.sum(arg2, arg2) - self._waiter() - logger.info("Waiting done") - return f"{arg1} and {arg2}" - - def join_string_with_separator(self, *strings, separator: Optional[str] = None): - """Joins strings with separator""" - return f"{separator if separator else self.separator}".join(strings) - - def _waiter(self, timeout: float = 0.1): - logger.info(f"Waiting {timeout}") - time.sleep(timeout) diff --git a/docs/example/01-static/test.robot b/docs/example/01-static/test.robot deleted file mode 100644 index ebaa7ce..0000000 --- a/docs/example/01-static/test.robot +++ /dev/null @@ -1,21 +0,0 @@ -*** Settings *** -Library StaticLibrary - -*** Test Cases *** -Join Stings - ${data} = Join Strings kala is big - Should Be Equal ${data} kala is big - -Sum Values - ${data} = Sum 1 2 - Should Be Equal As Numbers ${data} 3 - -Wait Something To Happen - ${data} = Wait Something To Happen tidii 3 - Should Be Equal ${data} tidii tidii and 6 - -Join Strings With Separator - ${data} = Join String With Separator Foo Bar Tidii separator=|-| - Should Be Equal ${data} Foo|-|Bar|-|Tidii - ${data} = Join String With Separator Foo Bar Tidii - Should Be Equal ${data} Foo;Bar;Tidii diff --git a/docs/example/run.py b/docs/example/run.py index c53eebc..524e74b 100644 --- a/docs/example/run.py +++ b/docs/example/run.py @@ -5,9 +5,7 @@ parser = argparse.ArgumentParser("Runner for examples") parser.add_argument("type", help="Which example is run.") args = parser.parse_args() -if args.type == "static": - folder = f"01-{args.type}" -elif args.type == "hybrid": +if args.type == "hybrid": folder = f"02-{args.type}" else: raise ValueError("Invalid value for library type.") From 891f1b2b04637a77601ec90922f74413f013e79d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 16 Oct 2022 01:00:49 +0300 Subject: [PATCH 044/148] Update black python version --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 0a1d7df..6d3b45f 100644 --- a/tasks.py +++ b/tasks.py @@ -124,7 +124,7 @@ def lint(ctx): print("Run flake8") ctx.run("flake8 --config .flake8 src/ tasks.py utest/run.py atest/run.py") print("Run black") - ctx.run("black --target-version py36 --line-length 120 src/ tasks.py utest/run.py atest/run.py") + ctx.run("black --target-version py37 --line-length 120 src/ tasks.py utest/run.py atest/run.py") print("Run isort") ctx.run("isort src/ tasks.py utest/run.py atest/run.py") print("Run tidy") From 0877cbf248d987638dde37861ccc6d0ceb23c47a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 16 Oct 2022 01:01:09 +0300 Subject: [PATCH 045/148] Black for utests --- utest/test_get_keyword_source.py | 54 ++++----- utest/test_get_keyword_types.py | 125 +++++++++---------- utest/test_keyword_builder.py | 28 ++--- utest/test_robotlibcore.py | 199 ++++++++++++++++--------------- 4 files changed, 209 insertions(+), 197 deletions(-) diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 212e19d..343e4fd 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -8,87 +8,87 @@ from DynamicTypesLibrary import DynamicTypesLibrary -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib(): return DynamicLibrary() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib_types(): return DynamicTypesLibrary() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def cur_dir(): return path.dirname(__file__) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib_path(cur_dir): - return path.normpath(path.join(cur_dir, '..', 'atest', 'DynamicLibrary.py')) + return path.normpath(path.join(cur_dir, "..", "atest", "DynamicLibrary.py")) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib_path_components(cur_dir): - return path.normpath(path.join(cur_dir, '..', 'atest', 'librarycomponents.py')) + return path.normpath(path.join(cur_dir, "..", "atest", "librarycomponents.py")) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib_path_types(cur_dir): - return path.normpath(path.join(cur_dir, '..', 'atest', 'DynamicTypesLibrary.py')) + return path.normpath(path.join(cur_dir, "..", "atest", "DynamicTypesLibrary.py")) def test_location_in_main(lib, lib_path): - source = lib.get_keyword_source('keyword_in_main') - assert source == '%s:20' % lib_path + source = lib.get_keyword_source("keyword_in_main") + assert source == "%s:20" % lib_path def test_location_in_class(lib, lib_path_components): - source = lib.get_keyword_source('method') - assert source == '%s:13' % lib_path_components + source = lib.get_keyword_source("method") + assert source == "%s:13" % lib_path_components def test_decorator_wrapper(lib_types, lib_path_types): - source = lib_types.get_keyword_source('keyword_wrapped') - assert source == '%s:74' % lib_path_types + source = lib_types.get_keyword_source("keyword_wrapped") + assert source == "%s:74" % lib_path_types def test_location_in_class_custom_keyword_name(lib, lib_path_components): - source = lib.get_keyword_source('Custom name') - assert source == '%s:17' % lib_path_components + source = lib.get_keyword_source("Custom name") + assert source == "%s:17" % 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') + source = lib.get_keyword_source("keyword_in_main") assert source == lib_path def test_no_path(lib, when): when(lib)._DynamicCore__get_keyword_path(Any()).thenReturn(None) - source = lib.get_keyword_source('keyword_in_main') - assert source == ':20' + source = lib.get_keyword_source("keyword_in_main") + assert source == ":20" def test_no_path_and_no_line_number(lib, when): when(lib)._DynamicCore__get_keyword_path(Any()).thenReturn(None) when(lib)._DynamicCore__get_keyword_line(Any()).thenReturn(None) - source = lib.get_keyword_source('keyword_in_main') + source = lib.get_keyword_source("keyword_in_main") assert source is None def test_def_in_decorator(lib_types, lib_path_types): - source = lib_types.get_keyword_source('keyword_with_def_deco') - assert source == '%s:68' % lib_path_types + source = lib_types.get_keyword_source("keyword_with_def_deco") + assert source == "%s:68" % lib_path_types def test_error_in_getfile(lib, when): - when(inspect).getfile(Any()).thenRaise(TypeError('Some message')) - source = lib.get_keyword_source('keyword_in_main') + when(inspect).getfile(Any()).thenRaise(TypeError("Some message")) + source = lib.get_keyword_source("keyword_in_main") assert source is None 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') + when(inspect).getsourcelines(Any()).thenRaise(IOError("Some message")) + source = lib.get_keyword_source("keyword_in_main") assert source == lib_path diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 7c38f1a..d072f9e 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -8,186 +8,187 @@ from DynamicTypesLibrary import DynamicTypesLibrary -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib(): return DynamicTypesLibrary() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def lib_types(): - return DynamicTypesAnnotationsLibrary('aaa') + return DynamicTypesAnnotationsLibrary("aaa") def test_using_keyword_types(lib): - types = lib.get_keyword_types('keyword_with_types') - assert types == {'arg1': str} + types = lib.get_keyword_types("keyword_with_types") + assert types == {"arg1": str} def test_types_disabled(lib): - types = lib.get_keyword_types('keyword_with_disabled_types') + types = lib.get_keyword_types("keyword_with_disabled_types") assert types is None def test_keyword_types_and_bool_default(lib): - types = lib.get_keyword_types('keyword_robot_types_and_bool_default') - assert types == {'arg1': str} + types = lib.get_keyword_types("keyword_robot_types_and_bool_default") + assert types == {"arg1": str} def test_one_keyword_type_defined(lib): - types = lib.get_keyword_types('keyword_with_one_type') - assert types == {'arg1': str} + types = lib.get_keyword_types("keyword_with_one_type") + assert types == {"arg1": str} def test_keyword_no_args(lib): - types = lib.get_keyword_types('keyword_with_no_args') + types = lib.get_keyword_types("keyword_with_no_args") assert types == {} def test_not_keyword(lib): with pytest.raises(ValueError): - lib.get_keyword_types('not_keyword') + lib.get_keyword_types("not_keyword") def test_keyword_none(lib): - types = lib.get_keyword_types('keyword_none') + types = lib.get_keyword_types("keyword_none") assert types == {} def test_single_annotation(lib_types): - types = lib_types.get_keyword_types('keyword_with_one_annotation') - assert types == {'arg': str} + types = lib_types.get_keyword_types("keyword_with_one_annotation") + assert types == {"arg": str} def test_multiple_annotations(lib_types): - types = lib_types.get_keyword_types('keyword_with_multiple_annotations') - assert types == {'arg1': str, 'arg2': List} + types = lib_types.get_keyword_types("keyword_with_multiple_annotations") + assert types == {"arg1": str, "arg2": List} def test_multiple_types(lib_types): - types = lib_types.get_keyword_types('keyword_multiple_types') - assert types == {'arg': Union[List, None]} + types = lib_types.get_keyword_types("keyword_multiple_types") + assert types == {"arg": Union[List, None]} def test_keyword_new_type(lib_types): - types = lib_types.get_keyword_types('keyword_new_type') + types = lib_types.get_keyword_types("keyword_new_type") assert len(types) == 1 - assert types['arg'] + assert types["arg"] def test_keyword_return_type(lib_types): - types = lib_types.get_keyword_types('keyword_define_return_type') - assert types == {'arg': str} + types = lib_types.get_keyword_types("keyword_define_return_type") + assert types == {"arg": str} def test_keyword_forward_references(lib_types): - types = lib_types.get_keyword_types('keyword_forward_references') - assert types == {'arg': CustomObject} + types = lib_types.get_keyword_types("keyword_forward_references") + assert types == {"arg": CustomObject} def test_keyword_with_annotation_and_default(lib_types): - types = lib_types.get_keyword_types('keyword_with_annotations_and_default') - assert types == {'arg': str} + types = lib_types.get_keyword_types("keyword_with_annotations_and_default") + assert types == {"arg": str} def test_keyword_with_many_defaults(lib): - types = lib.get_keyword_types('keyword_many_default_types') + types = lib.get_keyword_types("keyword_many_default_types") assert types == {} def test_keyword_with_annotation_external_class(lib_types): - types = lib_types.get_keyword_types('keyword_with_webdriver') - assert types == {'arg': CustomObject} + types = lib_types.get_keyword_types("keyword_with_webdriver") + assert types == {"arg": CustomObject} 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]} + types = lib_types.get_keyword_types("keyword_default_and_annotation") + assert types == {"arg1": int, "arg2": Union[bool, str]} def test_keyword_with_robot_types_and_annotations(lib_types): - types = lib_types.get_keyword_types('keyword_robot_types_and_annotations') - assert types == {'arg': str} + types = lib_types.get_keyword_types("keyword_robot_types_and_annotations") + assert types == {"arg": str} def test_keyword_with_robot_types_disbaled_and_annotations(lib_types): - types = lib_types.get_keyword_types('keyword_robot_types_disabled_and_annotations') + types = lib_types.get_keyword_types("keyword_robot_types_disabled_and_annotations") assert types is None def test_keyword_with_robot_types_and_bool_annotations(lib_types): - types = lib_types.get_keyword_types('keyword_robot_types_and_bool_hint') - assert types == {'arg1': str} + types = lib_types.get_keyword_types("keyword_robot_types_and_bool_hint") + assert types == {"arg1": str} + def test_init_args(lib_types): - types = lib_types.get_keyword_types('__init__') - assert types == {'arg': str} + types = lib_types.get_keyword_types("__init__") + assert types == {"arg": str} def test_dummy_magic_method(lib): with pytest.raises(ValueError): - lib.get_keyword_types('__foobar__') + lib.get_keyword_types("__foobar__") def test_varargs(lib): - types = lib.get_keyword_types('varargs_and_kwargs') + types = lib.get_keyword_types("varargs_and_kwargs") assert types == {} def test_init_args_with_annotation(lib_types): - types = lib_types.get_keyword_types('__init__') - assert types == {'arg': str} + types = lib_types.get_keyword_types("__init__") + assert types == {"arg": str} def test_exception_in_annotations(lib_types): - types = lib_types.get_keyword_types('keyword_exception_annotations') - assert types == {'arg': 'NotHere'} + types = lib_types.get_keyword_types("keyword_exception_annotations") + assert types == {"arg": "NotHere"} def test_keyword_only_arguments(lib_types): - types = lib_types.get_keyword_types('keyword_only_arguments') + types = lib_types.get_keyword_types("keyword_only_arguments") assert types == {} def test_keyword_only_arguments_many(lib_types): - types = lib_types.get_keyword_types('keyword_only_arguments_many') + types = lib_types.get_keyword_types("keyword_only_arguments_many") assert types == {} def test_keyword_mandatory_and_keyword_only_arguments(lib_types): - types = lib_types.get_keyword_types('keyword_mandatory_and_keyword_only_arguments') - assert types == {'arg': int, 'some': bool} + types = lib_types.get_keyword_types("keyword_mandatory_and_keyword_only_arguments") + assert types == {"arg": int, "some": bool} def test_keyword_only_arguments_many_positional_and_default(lib_types): - types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') - assert types == {'four': Union[int, str], 'six': Union[bool, str]} + types = lib_types.get_keyword_types("keyword_only_arguments_many_positional_and_default") + assert types == {"four": Union[int, str], "six": Union[bool, str]} def test_keyword_all_args(lib_types): - types = lib_types.get_keyword_types('keyword_all_args') + types = lib_types.get_keyword_types("keyword_all_args") assert types == {} def test_keyword_self_and_types(lib_types): - types = lib_types.get_keyword_types('keyword_self_and_types') - assert types == {'mandatory': str, 'other': bool} + types = lib_types.get_keyword_types("keyword_self_and_types") + assert types == {"mandatory": str, "other": bool} def test_keyword_self_and_keyword_only_types(lib_types): - types = lib_types.get_keyword_types('keyword_self_and_keyword_only_types') - assert types == {'varargs': int, 'other': bool, 'kwargs': int} + types = lib_types.get_keyword_types("keyword_self_and_keyword_only_types") + assert types == {"varargs": int, "other": bool, "kwargs": int} def test_keyword_with_decorator_arguments(lib_types): - types = lib_types.get_keyword_types('keyword_with_deco_and_signature') - assert types == {'arg1': bool, 'arg2': bool} + types = lib_types.get_keyword_types("keyword_with_deco_and_signature") + assert types == {"arg1": bool, "arg2": bool} def test_keyword_optional_with_none(lib_types): - types = lib_types.get_keyword_types('keyword_optional_with_none') - assert types == {'arg': typing.Union[str, type(None)]} + types = lib_types.get_keyword_types("keyword_optional_with_none") + assert types == {"arg": typing.Union[str, type(None)]} def test_keyword_union_with_none(lib_types): - types = lib_types.get_keyword_types('keyword_union_with_none') - assert types == {'arg': typing.Union[type(None), typing.Dict, str]} + types = lib_types.get_keyword_types("keyword_union_with_none") + assert types == {"arg": typing.Union[type(None), typing.Dict, str]} diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index dc8f3fb..8eb622f 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -18,9 +18,9 @@ def dyn_types(): def test_documentation(lib): spec = KeywordBuilder.build(lib.positional_args) - assert spec.documentation == 'Some documentation\n\nMulti line docs' + assert spec.documentation == "Some documentation\n\nMulti line docs" spec = KeywordBuilder.build(lib.positional_and_default) - assert spec.documentation == '' + assert spec.documentation == "" def test_no_args(lib): @@ -30,37 +30,37 @@ def test_no_args(lib): def test_positional_args(lib): spec = KeywordBuilder.build(lib.positional_args) - assert spec.argument_specification == ['arg1', 'arg2'] + assert spec.argument_specification == ["arg1", "arg2"] def test_positional_and_named(lib): spec = KeywordBuilder.build(lib.positional_and_default) - assert spec.argument_specification == ['arg1', 'arg2', ('named1', 'string1'), ('named2', 123)] + assert spec.argument_specification == ["arg1", "arg2", ("named1", "string1"), ("named2", 123)] def test_named_only(lib): spec = KeywordBuilder.build(lib.default_only) - assert spec.argument_specification == [('named1', 'string1'), ('named2', 123)] + assert spec.argument_specification == [("named1", "string1"), ("named2", 123)] def test_varargs_and_kwargs(lib): spec = KeywordBuilder.build(lib.varargs_kwargs) - assert spec.argument_specification == ['*vargs', '**kwargs'] + assert spec.argument_specification == ["*vargs", "**kwargs"] def test_named_only_part2(lib): spec = KeywordBuilder.build(lib.named_only) - assert spec.argument_specification == ['*varargs', 'key1', 'key2'] + assert spec.argument_specification == ["*varargs", "key1", "key2"] def test_named_only(lib): spec = KeywordBuilder.build(lib.named_only_with_defaults) - assert spec.argument_specification == ['*varargs', 'key1', 'key2', ('key3', 'default1'), ('key4', True)] + assert spec.argument_specification == ["*varargs", "key1", "key2", ("key3", "default1"), ("key4", True)] def test_types_in_keyword_deco(lib): spec = KeywordBuilder.build(lib.positional_args) - assert spec.argument_types == {'arg1': str, 'arg2': int} + assert spec.argument_types == {"arg1": str, "arg2": int} def test_types_disabled_in_keyword_deco(lib): @@ -70,21 +70,21 @@ def test_types_disabled_in_keyword_deco(lib): def test_types_(lib): spec = KeywordBuilder.build(lib.args_with_type_hints) - assert spec.argument_types == {'arg3': str, 'arg4': type(None)} + assert spec.argument_types == {"arg3": str, "arg4": type(None)} def test_types(lib): spec = KeywordBuilder.build(lib.self_and_keyword_only_types) - assert spec.argument_types == {'varargs': int, 'other': bool, 'kwargs': int} + assert spec.argument_types == {"varargs": int, "other": bool, "kwargs": int} def test_optional_none(lib): spec = KeywordBuilder.build(lib.optional_none) - assert spec.argument_types == {'arg1': typing.Union[str, None], 'arg2': typing.Union[str, None]} + assert spec.argument_types == {"arg1": typing.Union[str, None], "arg2": typing.Union[str, None]} def test_complex_deco(dyn_types): spec = KeywordBuilder.build(dyn_types.keyword_with_deco_and_signature) - assert spec.argument_types == {'arg1': bool, 'arg2': bool} - assert spec.argument_specification == [('arg1', False), ('arg2', False)] + assert spec.argument_types == {"arg1": bool, "arg2": bool} + assert spec.argument_specification == [("arg1", False), ("arg2", False)] assert spec.documentation == "Test me doc here" diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 62f02f6..965dc43 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -6,140 +6,151 @@ from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dyn_lib(): return DynamicLibrary() def test_keyword_names(): - expected = ['Custom name', - 'Embedded arguments "${here}"', - 'all_arguments', - 'defaults', - 'doc_and_tags', - 'function', - 'keyword_in_main', - 'kwargs_only', - 'mandatory', - 'method', - 'multi_line_doc', - 'one_line_doc', - 'tags', - 'varargs_and_kwargs'] + expected = [ + "Custom name", + 'Embedded arguments "${here}"', + "all_arguments", + "defaults", + "doc_and_tags", + "function", + "keyword_in_main", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "one_line_doc", + "tags", + "varargs_and_kwargs", + ] assert HybridLibrary().get_keyword_names() == expected assert DynamicLibrary().get_keyword_names() == expected def test_dir(): - expected = ['Custom name', - 'Embedded arguments "${here}"', - '_DynamicCore__get_keyword', - '_DynamicCore__get_keyword_line', - '_DynamicCore__get_keyword_path', - '_HybridCore__get_members', - '_HybridCore__get_members_from_instance', - '_custom_name', - 'add_library_components', - 'all_arguments', - 'attributes', - 'class_attribute', - 'defaults', - 'doc_and_tags', - 'embedded', - 'function', - 'get_keyword_arguments', - 'get_keyword_documentation', - 'get_keyword_names', - 'get_keyword_source', - 'get_keyword_tags', - 'get_keyword_types', - 'instance_attribute', - 'keyword_in_main', - 'keywords', - 'keywords_spec', - 'kwargs_only', - 'mandatory', - 'method', - 'multi_line_doc', - 'not_keyword_in_main', - 'one_line_doc', - 'run_keyword', - 'tags', - 'varargs_and_kwargs'] - assert [a for a in dir(DynamicLibrary()) if a[:2] != '__'] == expected - expected = [e for e in expected if e not in ('_DynamicCore__get_typing_hints', - '_DynamicCore__get_keyword', - '_DynamicCore__get_keyword_line', - '_DynamicCore__get_keyword_path', - '_DynamicCore__join_defaults_with_types', - 'get_keyword_arguments', - 'get_keyword_documentation', - 'get_keyword_source', - 'get_keyword_tags', - 'run_keyword', - 'get_keyword_types')] - assert [a for a in dir(HybridLibrary()) if a[:2] != '__'] == expected + expected = [ + "Custom name", + 'Embedded arguments "${here}"', + "_DynamicCore__get_keyword", + "_DynamicCore__get_keyword_line", + "_DynamicCore__get_keyword_path", + "_HybridCore__get_members", + "_HybridCore__get_members_from_instance", + "_custom_name", + "add_library_components", + "all_arguments", + "attributes", + "class_attribute", + "defaults", + "doc_and_tags", + "embedded", + "function", + "get_keyword_arguments", + "get_keyword_documentation", + "get_keyword_names", + "get_keyword_source", + "get_keyword_tags", + "get_keyword_types", + "instance_attribute", + "keyword_in_main", + "keywords", + "keywords_spec", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "not_keyword_in_main", + "one_line_doc", + "parse_plugins", + "run_keyword", + "tags", + "varargs_and_kwargs", + ] + assert [a for a in dir(DynamicLibrary()) if a[:2] != "__"] == expected + expected = [ + e + for e in expected + if e + not in ( + "_DynamicCore__get_typing_hints", + "_DynamicCore__get_keyword", + "_DynamicCore__get_keyword_line", + "_DynamicCore__get_keyword_path", + "_DynamicCore__join_defaults_with_types", + "get_keyword_arguments", + "get_keyword_documentation", + "get_keyword_source", + "get_keyword_tags", + "parse_plugins", + "run_keyword", + "get_keyword_types", + ) + ] + assert [a for a in dir(HybridLibrary()) if a[:2] != "__"] == expected def test_getattr(): for lib in [HybridLibrary(), DynamicLibrary()]: - assert lib.class_attribute == 'not keyword' - assert lib.instance_attribute == 'not keyword' + assert lib.class_attribute == "not keyword" + assert lib.instance_attribute == "not keyword" assert lib.function() == 1 assert lib.method() == 2 assert lib._custom_name() == 3 - assert getattr(lib, 'Custom name')() == 3 + assert getattr(lib, "Custom name")() == 3 with pytest.raises(AttributeError) as exc_info: lib.non_existing - assert str(exc_info.value) == \ - "'%s' object has no attribute 'non_existing'" % type(lib).__name__ + assert str(exc_info.value) == "'%s' object has no attribute 'non_existing'" % type(lib).__name__ def test_get_keyword_arguments(): args = DynamicLibrary().get_keyword_arguments - assert args('mandatory') == ['arg1', 'arg2'] - assert args('defaults') == ['arg1', ('arg2', 'default'), ('arg3', 3)] - assert args('varargs_and_kwargs') == ['*args', '**kws'] - assert args('kwargs_only') == ['**kws'] - assert args('all_arguments') == ['mandatory', ('default', 'value'), '*varargs', '**kwargs'] - assert args('__init__') == [('arg', None)] + assert args("mandatory") == ["arg1", "arg2"] + assert args("defaults") == ["arg1", ("arg2", "default"), ("arg3", 3)] + assert args("varargs_and_kwargs") == ["*args", "**kws"] + assert args("kwargs_only") == ["**kws"] + assert args("all_arguments") == ["mandatory", ("default", "value"), "*varargs", "**kwargs"] + assert args("__init__") == [("arg", None)] with pytest.raises(AttributeError): - args('__foobar__') + args("__foobar__") def test_keyword_only_arguments_for_get_keyword_arguments(): args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments - assert args('keyword_only_arguments') == ['*varargs', ('some', 111)] - assert args('keyword_only_arguments_many') == ['*varargs', ('some', 'value'), ('other', None)] - assert args('keyword_only_arguments_no_default') == ['*varargs', 'other'] - assert args('keyword_only_arguments_default_and_no_default') == ['*varargs', 'other', ('value', False)] - all_args = ['mandatory', ('positional', 1), '*varargs', 'other', ('value', False), '**kwargs'] - assert args('keyword_all_args') == all_args - assert args('keyword_with_deco_and_signature') == [('arg1', False), ('arg2', False)] + assert args("keyword_only_arguments") == ["*varargs", ("some", 111)] + assert args("keyword_only_arguments_many") == ["*varargs", ("some", "value"), ("other", None)] + assert args("keyword_only_arguments_no_default") == ["*varargs", "other"] + assert args("keyword_only_arguments_default_and_no_default") == ["*varargs", "other", ("value", False)] + all_args = ["mandatory", ("positional", 1), "*varargs", "other", ("value", False), "**kwargs"] + assert args("keyword_all_args") == all_args + assert args("keyword_with_deco_and_signature") == [("arg1", False), ("arg2", False)] def test_get_keyword_documentation(): doc = DynamicLibrary().get_keyword_documentation - assert doc('function') == '' - assert doc('method') == '' - assert doc('one_line_doc') == 'I got doc!' - assert doc('multi_line_doc') == 'I got doc!\n\nWith multiple lines!!\nYeah!!!!' - assert doc('__intro__') == 'General library documentation.' - assert doc('__init__') == 'Library init doc.' + assert doc("function") == "" + assert doc("method") == "" + assert doc("one_line_doc") == "I got doc!" + assert doc("multi_line_doc") == "I got doc!\n\nWith multiple lines!!\nYeah!!!!" + assert doc("__intro__") == "General library documentation." + assert doc("__init__") == "Library init doc." def test_get_keyword_tags(): lib = DynamicLibrary() tags = lib.get_keyword_tags doc = lib.get_keyword_documentation - assert tags('tags') == ['tag', 'another tag'] - assert tags('doc_and_tags') == ['tag'] - assert doc('tags') == '' - assert doc('doc_and_tags') == 'I got doc!' + assert tags("tags") == ["tag", "another tag"] + assert tags("doc_and_tags") == ["tag"] + assert doc("tags") == "" + assert doc("doc_and_tags") == "I got doc!" def test_library_cannot_be_class(): with pytest.raises(TypeError) as exc_info: HybridCore([HybridLibrary]) - assert str(exc_info.value) == \ - "Libraries must be modules or instances, got class 'HybridLibrary' instead." + assert str(exc_info.value) == "Libraries must be modules or instances, got class 'HybridLibrary' instead." From 86eae82fdd5d9fc1a3f092adffd8f4eda34da9e2 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 16 Oct 2022 22:27:37 +0300 Subject: [PATCH 046/148] Plugin str to modules --- src/robotlibcore.py | 63 ++++++++++++++++++++++++++++++++++++++++ utest/test_plugin_api.py | 37 +++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 utest/test_plugin_api.py diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 16ef2c4..a4641d1 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,12 +21,23 @@ import inspect import os import typing +from dataclasses import dataclass from robot.api.deco import keyword # noqa F401 +from robot.utils import Importer # noqa F401 +from robot.errors import DataError __version__ = "3.0.1.dev1" +class PythonLibCoreException(Exception): + pass + + +class PluginError(PythonLibCoreException): + pass + + class HybridCore: def __init__(self, library_components): self.keywords = {} @@ -82,7 +93,15 @@ 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 {})) @@ -105,6 +124,9 @@ def get_keyword_types(self, name): raise ValueError('Keyword "%s" not found.' % name) return spec.argument_types + def parse_plugins(self, plugins: str) -> typing.List: + pass + def __get_keyword(self, keyword_name): if keyword_name == "__init__": return self.__init__ @@ -260,3 +282,44 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ self.argument_specification = argument_specification self.documentation = documentation self.argument_types = argument_types + + +class PluginParser: + + def parse_plugins(self, plugins: str) -> typing.List: + libraries = [] + 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) + plugin = plugin(self, *parsed_plugin.args, **parsed_plugin.kw_args) + if not isinstance(plugin, LibraryComponent): + message = ( + "Plugin does not inherit SeleniumLibrary.base.LibraryComponent" + ) + raise PluginError(message) + self._store_plugin_keywords(plugin) + libraries.append(plugin) + return libraries + + def _string_to_modules(self, modules): + parsed_modules = [] + if not modules: + return parsed_modules + for module in modules.split(","): + module = module.strip() + module_and_args = module.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) + module = Module(module=module_name, args=args, kw_args=kw_args) + parsed_modules.append(module) + return parsed_modules diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py new file mode 100644 index 0000000..6c1202d --- /dev/null +++ b/utest/test_plugin_api.py @@ -0,0 +1,37 @@ +import pytest + +from robotlibcore import Module, PluginParser + + +@pytest.fixture(scope="module") +def plugin_parser() -> PluginParser: + return PluginParser() + + +def test_no_plugins_parsing(plugin_parser): + for item in [None, ""]: + assert plugin_parser._string_to_modules(item) == [] + + +def test_plugins_string_to_modules(plugin_parser): + result = plugin_parser._string_to_modules("foo/bar.by") + assert result == [Module("foo/bar.by", [], {})] + result = plugin_parser._string_to_modules("path.to.MyLibrary,path.to.OtherLibrary") + assert result == [ + Module("path.to.MyLibrary", [], {}), + Module("path.to.OtherLibrary", [], {}) + ] + result = plugin_parser._string_to_modules("path.to.MyLibrary , path.to.OtherLibrary") + assert result == [ + Module("path.to.MyLibrary", [], {}), + Module("path.to.OtherLibrary", [], {}) + ] + result = plugin_parser._string_to_modules("path.to.MyLibrary;foo;bar , path.to.OtherLibrary;1") + assert result == [ + Module("path.to.MyLibrary", ["foo", "bar"], {}), + Module("path.to.OtherLibrary", ["1"], {}) + ] + result = plugin_parser._string_to_modules("PluginWithKwArgs.py;kw1=Text1;kw2=Text2") + assert result == [ + Module("PluginWithKwArgs.py", [], {"kw1": "Text1", "kw2": "Text2"}), + ] From 3c7f25f8edaa16b06643a7f252919ac53e8df2b6 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 4 Nov 2022 22:03:05 +0200 Subject: [PATCH 047/148] Parsing plugins --- atest/tests.robot | 7 ++++--- atest/tests_types.robot | 10 ++++++---- src/robotlibcore.py | 16 ++++++++++------ utest/helpers/my_plugin_test.py | 30 ++++++++++++++++++++++++++++++ utest/run.py | 3 +++ utest/test_plugin_api.py | 15 ++++++++++++++- 6 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 utest/helpers/my_plugin_test.py diff --git a/atest/tests.robot b/atest/tests.robot index 12571b1..962adeb 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -1,9 +1,11 @@ *** Settings *** Library ${LIBRARY}.py + *** Variables *** ${LIBRARY} DynamicLibrary + *** Test Cases *** Keyword names Keyword in main @@ -12,9 +14,7 @@ Keyword names Method Custom name Cust omna me - IF $LIBRARY == "ExtendExistingLibrary" - Keyword in extending library - END + IF $LIBRARY == "ExtendExistingLibrary" Keyword in extending library Method without @keyword are not keyowrds [Documentation] FAIL GLOB: No keyword with name 'Not keyword' found.* @@ -45,6 +45,7 @@ Embedded arguments Embedded arguments "work" embeDded ArgumeNtS "Work but this fails" + *** Keywords *** Return value should be [Arguments] ${expected} ${keyword} @{args} &{kwargs} diff --git a/atest/tests_types.robot b/atest/tests_types.robot index ab9af7d..3337617 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -2,9 +2,11 @@ Library DynamicTypesLibrary.py Library DynamicTypesAnnotationsLibrary.py xxx + *** Variables *** ${CUSTOM NONE} = ${None} + *** Test Cases *** Keyword Default Argument As Abject None ${return} = DynamicTypesLibrary.Keyword None ${None} @@ -56,7 +58,8 @@ Varargs and KeywordArgs With Typing Hints ... 1 2 3 4 # varargs ... other=True # other argument ... key1=1 key2=2 # kwargs - Should Match ${return} + Should Match + ... ${return} ... this_is_mandatory: , (1, 2, 3, 4): , True: , {'key1': 1, 'key2': 2}: Enum Conversion Should Work @@ -109,9 +112,8 @@ Python 3.10 New Type Hints Should Be Equal ${types} arg: {"key": 1}, type: END + *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3.10 Only ${py3} = DynamicTypesLibrary.Is Python 3 10 - IF ${py3} - Import Library Python310Library.py - END + IF ${py3} Import Library Python310Library.py diff --git a/src/robotlibcore.py b/src/robotlibcore.py index a4641d1..2bcd4fa 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -285,24 +285,28 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ class PluginParser: + def __init__(self, base_class: typing.Any): + self._base_class = base_class def parse_plugins(self, plugins: str) -> typing.List: - libraries = [] + 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) - plugin = plugin(self, *parsed_plugin.args, **parsed_plugin.kw_args) - if not isinstance(plugin, LibraryComponent): + plugin = plugin(*parsed_plugin.args, **parsed_plugin.kw_args) + if self._base_class and not isinstance(plugin, self._base_class): message = ( "Plugin does not inherit SeleniumLibrary.base.LibraryComponent" ) raise PluginError(message) - self._store_plugin_keywords(plugin) - libraries.append(plugin) - return libraries + imported_plugins.append(plugin) + return imported_plugins + + def get_plugin_keyword(self, plugins: typing.List): + pass def _string_to_modules(self, modules): parsed_modules = [] diff --git a/utest/helpers/my_plugin_test.py b/utest/helpers/my_plugin_test.py new file mode 100644 index 0000000..abf06c9 --- /dev/null +++ b/utest/helpers/my_plugin_test.py @@ -0,0 +1,30 @@ +from robot.api.deco import keyword + + +class TestClass: + + @keyword + def new_keyword(self, arg: int) -> int: + return arg + self.not_keyword() + + def not_keyword(self): + return 1 + + +class LibraryBase: + + def __init__(self): + self.x = 1 + + def base(self): + return 2 + + +class TestClassWithBase(LibraryBase): + + @keyword + def another_keywor(self) -> int: + return 2 * 2 + + def normal_method(self): + return "xxx" diff --git a/utest/run.py b/utest/run.py index 0e877bf..08da4a4 100755 --- a/utest/run.py +++ b/utest/run.py @@ -14,6 +14,8 @@ src = join(curdir, "..", "src") sys.path.insert(0, src) sys.path.insert(0, atest_dir) +helpers = join(curdir, "helpers") +sys.path.append(helpers) parser = argparse.ArgumentParser() parser.add_argument("--no-cov", dest="cov", action="store_false") @@ -22,6 +24,7 @@ args = parser.parse_args() pytest_args = [ + f"--ignore={helpers}", "-p", "no:cacheprovider", "--junitxml=%s" % xunit_report, diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 6c1202d..43a9d74 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,11 +1,12 @@ import pytest from robotlibcore import Module, PluginParser +import my_plugin_test @pytest.fixture(scope="module") def plugin_parser() -> PluginParser: - return PluginParser() + return PluginParser(None) def test_no_plugins_parsing(plugin_parser): @@ -35,3 +36,15 @@ def test_plugins_string_to_modules(plugin_parser): assert result == [ Module("PluginWithKwArgs.py", [], {"kw1": "Text1", "kw2": "Text2"}), ] + + +def test_parse_plugins(plugin_parser): + plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass") + assert len(plugins) == 1 + assert isinstance(plugins[0], my_plugin_test.TestClass) + plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass,my_plugin_test.TestClassWithBase") + assert len(plugins) == 2 + assert isinstance(plugins[0], my_plugin_test.TestClass) + assert isinstance(plugins[1], my_plugin_test.TestClassWithBase) + + From 2f841768c09a962108e4acb71d04c7df4b8ffd86 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 4 Nov 2022 22:12:16 +0200 Subject: [PATCH 048/148] With base class --- src/robotlibcore.py | 2 +- utest/test_plugin_api.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 2bcd4fa..d65299e 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -299,7 +299,7 @@ def parse_plugins(self, plugins: str) -> typing.List: plugin = plugin(*parsed_plugin.args, **parsed_plugin.kw_args) if self._base_class and not isinstance(plugin, self._base_class): message = ( - "Plugin does not inherit SeleniumLibrary.base.LibraryComponent" + f"Plugin does not inherit {self._base_class}" ) raise PluginError(message) imported_plugins.append(plugin) diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 43a9d74..05d97f6 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,6 +1,6 @@ import pytest -from robotlibcore import Module, PluginParser +from robotlibcore import Module, PluginParser, PluginError import my_plugin_test @@ -46,5 +46,13 @@ def test_parse_plugins(plugin_parser): assert len(plugins) == 2 assert isinstance(plugins[0], my_plugin_test.TestClass) assert isinstance(plugins[1], my_plugin_test.TestClassWithBase) - + +def test_parse_plugins_with_base(): + parser = PluginParser(my_plugin_test.LibraryBase) + plugins = parser.parse_plugins("my_plugin_test.TestClassWithBase") + assert len(plugins) == 1 + assert isinstance(plugins[0], my_plugin_test.TestClassWithBase) + with pytest.raises(PluginError) as excinfo: + parser.parse_plugins("my_plugin_test.TestClass") + assert "Plugin does not inherit " in str(excinfo.value) From 54f59e3da6fb560bbfa81116f8e720638808d979 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 4 Nov 2022 22:30:36 +0200 Subject: [PATCH 049/148] Parse keywords --- src/robotlibcore.py | 4 ++-- utest/helpers/my_plugin_test.py | 2 +- utest/test_plugin_api.py | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index d65299e..6e963c1 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -305,8 +305,8 @@ def parse_plugins(self, plugins: str) -> typing.List: imported_plugins.append(plugin) return imported_plugins - def get_plugin_keyword(self, plugins: typing.List): - pass + def get_plugin_keywords(self, plugins: typing.List): + return DynamicCore(plugins).get_keyword_names() def _string_to_modules(self, modules): parsed_modules = [] diff --git a/utest/helpers/my_plugin_test.py b/utest/helpers/my_plugin_test.py index abf06c9..dfc6475 100644 --- a/utest/helpers/my_plugin_test.py +++ b/utest/helpers/my_plugin_test.py @@ -23,7 +23,7 @@ def base(self): class TestClassWithBase(LibraryBase): @keyword - def another_keywor(self) -> int: + def another_keyword(self) -> int: return 2 * 2 def normal_method(self): diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 05d97f6..85b6ea6 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -56,3 +56,11 @@ def test_parse_plugins_with_base(): with pytest.raises(PluginError) as excinfo: parser.parse_plugins("my_plugin_test.TestClass") assert "Plugin does not inherit " in str(excinfo.value) + + +def test_plugin_keywords(plugin_parser): + plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass,my_plugin_test.TestClassWithBase") + keywords = plugin_parser.get_plugin_keywords(plugins) + assert len(keywords) == 2 + assert keywords[0] == "another_keyword" + assert keywords[1] == "new_keyword" From ca70d5d8758b4c35ee5ad313dd6713bb6be91439 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 4 Nov 2022 23:09:08 +0200 Subject: [PATCH 050/148] Atest for plugin API --- atest/plugin_api/MyPlugin.py | 8 ++++++++ atest/plugin_api/MyPluginBase.py | 10 ++++++++++ atest/plugin_api/PluginLib.py | 16 ++++++++++++++++ atest/plugin_api/PluginWithBaseLib.py | 21 +++++++++++++++++++++ atest/plugin_api/__init__.py | 0 atest/plugin_api/plugin_api.robot | 19 +++++++++++++++++++ src/robotlibcore.py | 5 +---- utest/helpers/__init__.py | 0 utest/test_plugin_api.py | 2 +- utest/test_robotlibcore.py | 1 - 10 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 atest/plugin_api/MyPlugin.py create mode 100644 atest/plugin_api/MyPluginBase.py create mode 100644 atest/plugin_api/PluginLib.py create mode 100644 atest/plugin_api/PluginWithBaseLib.py create mode 100644 atest/plugin_api/__init__.py create mode 100644 atest/plugin_api/plugin_api.robot create mode 100644 utest/helpers/__init__.py diff --git a/atest/plugin_api/MyPlugin.py b/atest/plugin_api/MyPlugin.py new file mode 100644 index 0000000..3629fd9 --- /dev/null +++ b/atest/plugin_api/MyPlugin.py @@ -0,0 +1,8 @@ +from robot.api.deco import keyword # noqa F401 + + +class MyPlugin: + + @keyword + def plugin_keyword(self): + return 2 diff --git a/atest/plugin_api/MyPluginBase.py b/atest/plugin_api/MyPluginBase.py new file mode 100644 index 0000000..cf76523 --- /dev/null +++ b/atest/plugin_api/MyPluginBase.py @@ -0,0 +1,10 @@ +from robot.api.deco import keyword # noqa F401 + +from PluginWithBaseLib import BaseClass + + +class MyPluginBase(BaseClass): + + @keyword + def base_plugin_keyword(self): + return "40" diff --git a/atest/plugin_api/PluginLib.py b/atest/plugin_api/PluginLib.py new file mode 100644 index 0000000..03555c9 --- /dev/null +++ b/atest/plugin_api/PluginLib.py @@ -0,0 +1,16 @@ +from robot.api.deco import keyword # noqa F401 + +from robotlibcore import DynamicCore, PluginParser + + +class PluginLib(DynamicCore): + + def __init__(self, plugins): + plugin_parser = PluginParser() + parsed_plugins = plugin_parser.parse_plugins(plugins) + self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) + DynamicCore.__init__(self, parsed_plugins) + + @keyword + def foo(self): + return 1 diff --git a/atest/plugin_api/PluginWithBaseLib.py b/atest/plugin_api/PluginWithBaseLib.py new file mode 100644 index 0000000..d090cdd --- /dev/null +++ b/atest/plugin_api/PluginWithBaseLib.py @@ -0,0 +1,21 @@ +from robot.api.deco import keyword # noqa F401 + +from robotlibcore import DynamicCore, PluginParser + + +class BaseClass: + def method(self): + return 1 + + +class PluginWithBaseLib(DynamicCore): + + def __init__(self, plugins): + plugin_parser = PluginParser(BaseClass) + parsed_plugins = plugin_parser.parse_plugins(plugins) + self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) + DynamicCore.__init__(self, parsed_plugins) + + @keyword + def base_keyword(self): + return "42" diff --git a/atest/plugin_api/__init__.py b/atest/plugin_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/atest/plugin_api/plugin_api.robot b/atest/plugin_api/plugin_api.robot new file mode 100644 index 0000000..7154d7d --- /dev/null +++ b/atest/plugin_api/plugin_api.robot @@ -0,0 +1,19 @@ +*** Test Cases *** +Plugin Test + Import Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py + ${value} = Foo + Should Be Equal ${value} ${1} + ${value} = Plugin Keyword + Should Be Equal ${value} ${2} + +Plugins With Base Class + Import Library ${CURDIR}/PluginWithBaseLib.py plugins=${CURDIR}/MyPluginBase.py + ${value} = Base Plugin Keyword + Should Be Equal ${value} 40 + ${value} = Base Keyword + Should Be Equal ${value} 42 + +Pugins With No Base Class + Run Keyword And Expect Error + ... *PluginError: Plugin does not inherit + ... Import Library ${CURDIR}/PluginWithBaseLib.py plugins=${CURDIR}/MyPlugin.py diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 6e963c1..36e62c5 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -124,9 +124,6 @@ def get_keyword_types(self, name): raise ValueError('Keyword "%s" not found.' % name) return spec.argument_types - def parse_plugins(self, plugins: str) -> typing.List: - pass - def __get_keyword(self, keyword_name): if keyword_name == "__init__": return self.__init__ @@ -285,7 +282,7 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ class PluginParser: - def __init__(self, base_class: typing.Any): + def __init__(self, base_class: typing.Optional[typing.Any] = None): self._base_class = base_class def parse_plugins(self, plugins: str) -> typing.List: diff --git a/utest/helpers/__init__.py b/utest/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 85b6ea6..e8dfd94 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -6,7 +6,7 @@ @pytest.fixture(scope="module") def plugin_parser() -> PluginParser: - return PluginParser(None) + return PluginParser() def test_no_plugins_parsing(plugin_parser): diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 965dc43..96fb410 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -66,7 +66,6 @@ def test_dir(): "multi_line_doc", "not_keyword_in_main", "one_line_doc", - "parse_plugins", "run_keyword", "tags", "varargs_and_kwargs", From 352fab72161f6a4afc0847922827d2a907ee17d4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 4 Nov 2022 23:12:30 +0200 Subject: [PATCH 051/148] Fix pep errors --- src/robotlibcore.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 36e62c5..126f458 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -24,8 +24,8 @@ from dataclasses import dataclass from robot.api.deco import keyword # noqa F401 -from robot.utils import Importer # noqa F401 from robot.errors import DataError +from robot.utils import Importer # noqa F401 __version__ = "3.0.1.dev1" @@ -101,7 +101,6 @@ class Module: class DynamicCore(HybridCore): - def run_keyword(self, name, args, kwargs=None): return self.keywords[name](*args, **(kwargs or {})) @@ -295,9 +294,7 @@ def parse_plugins(self, plugins: str) -> typing.List: raise DataError(message) plugin = plugin(*parsed_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}" - ) + message = f"Plugin does not inherit {self._base_class}" raise PluginError(message) imported_plugins.append(plugin) return imported_plugins From 91da5248aff2fa157f0b9a5468a60b85e924d6ff Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 16:35:17 +0200 Subject: [PATCH 052/148] Improve test --- atest/plugin_api/MyPluginBase.py | 5 ++++- atest/plugin_api/plugin_api.robot | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/atest/plugin_api/MyPluginBase.py b/atest/plugin_api/MyPluginBase.py index cf76523..f1b720d 100644 --- a/atest/plugin_api/MyPluginBase.py +++ b/atest/plugin_api/MyPluginBase.py @@ -5,6 +5,9 @@ class MyPluginBase(BaseClass): + def __init__(self, arg): + self.arg = int(arg) + @keyword def base_plugin_keyword(self): - return "40" + return 40 + self.arg diff --git a/atest/plugin_api/plugin_api.robot b/atest/plugin_api/plugin_api.robot index 7154d7d..ffe7fb7 100644 --- a/atest/plugin_api/plugin_api.robot +++ b/atest/plugin_api/plugin_api.robot @@ -7,9 +7,9 @@ Plugin Test Should Be Equal ${value} ${2} Plugins With Base Class - Import Library ${CURDIR}/PluginWithBaseLib.py plugins=${CURDIR}/MyPluginBase.py + Import Library ${CURDIR}/PluginWithBaseLib.py plugins=${CURDIR}/MyPluginBase.py;11 ${value} = Base Plugin Keyword - Should Be Equal ${value} 40 + Should Be Equal ${value} ${51} ${value} = Base Keyword Should Be Equal ${value} 42 From b4a0f004ace7fcb2757e47db407cde274c5cc651 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 16:38:01 +0200 Subject: [PATCH 053/148] Refectoring --- src/robotlibcore.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 126f458..1920af9 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -20,8 +20,8 @@ """ import inspect import os -import typing from dataclasses import dataclass +from typing import Any, List, Optional, get_type_hints from robot.api.deco import keyword # noqa F401 from robot.errors import DataError @@ -243,7 +243,7 @@ def _get_types(cls, function): def _get_typing_hints(cls, function): function = cls.unwrap(function) try: - hints = typing.get_type_hints(function) + hints = get_type_hints(function) except Exception: hints = function.__annotations__ arg_spec = cls._get_arg_spec(function) @@ -281,10 +281,10 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ class PluginParser: - def __init__(self, base_class: typing.Optional[typing.Any] = None): + def __init__(self, base_class: Optional[Any] = None): self._base_class = base_class - def parse_plugins(self, plugins: str) -> typing.List: + def parse_plugins(self, plugins: str) -> List: imported_plugins = [] importer = Importer("test library") for parsed_plugin in self._string_to_modules(plugins): @@ -299,7 +299,7 @@ def parse_plugins(self, plugins: str) -> typing.List: imported_plugins.append(plugin) return imported_plugins - def get_plugin_keywords(self, plugins: typing.List): + def get_plugin_keywords(self, plugins: List): return DynamicCore(plugins).get_keyword_names() def _string_to_modules(self, modules): From a1bdee3eb8177000797fc9e3649bd364006bbe6d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 17:22:28 +0200 Subject: [PATCH 054/148] Add possibility to provide Python objects --- atest/plugin_api/MyPluginWithPythonObjects.py | 15 +++++++++++++ .../plugin_api/PluginWithPythonObjectsLib.py | 22 +++++++++++++++++++ atest/plugin_api/plugin_api.robot | 7 ++++++ src/robotlibcore.py | 6 +++-- utest/helpers/my_plugin_test.py | 12 ++++++++++ utest/test_plugin_api.py | 10 +++++++++ 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 atest/plugin_api/MyPluginWithPythonObjects.py create mode 100644 atest/plugin_api/PluginWithPythonObjectsLib.py diff --git a/atest/plugin_api/MyPluginWithPythonObjects.py b/atest/plugin_api/MyPluginWithPythonObjects.py new file mode 100644 index 0000000..af3147c --- /dev/null +++ b/atest/plugin_api/MyPluginWithPythonObjects.py @@ -0,0 +1,15 @@ +from robot.api.deco import keyword # noqa F401 + +from PluginWithPythonObjectsLib import BaseWithPython + + +class MyPluginWithPythonObjects(BaseWithPython): + + def __init__(self, py1, py2, rf1, rf2): + self.rf1 = int(rf1) + self.rf2 = int(rf2) + super().__init__(py1, py2) + + @keyword + def plugin_keyword_with_python(self): + return self.rf1 + self.rf2 + self.py1 + self.py2 diff --git a/atest/plugin_api/PluginWithPythonObjectsLib.py b/atest/plugin_api/PluginWithPythonObjectsLib.py new file mode 100644 index 0000000..2b76e3c --- /dev/null +++ b/atest/plugin_api/PluginWithPythonObjectsLib.py @@ -0,0 +1,22 @@ +from robot.api.deco import keyword # noqa F401 + +from robotlibcore import DynamicCore, PluginParser + + +class BaseWithPython: + def __init__(self, py1, py2): + self.py1 = py1 + self.py2 = py2 + + +class PluginWithPythonObjectsLib(DynamicCore): + + def __init__(self, plugins): + plugin_parser = PluginParser(BaseWithPython, [8, 9]) + parsed_plugins = plugin_parser.parse_plugins(plugins) + self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) + DynamicCore.__init__(self, parsed_plugins) + + @keyword + def keyword_with_python(self): + return "123" diff --git a/atest/plugin_api/plugin_api.robot b/atest/plugin_api/plugin_api.robot index ffe7fb7..a57f47f 100644 --- a/atest/plugin_api/plugin_api.robot +++ b/atest/plugin_api/plugin_api.robot @@ -13,6 +13,13 @@ Plugins With Base Class ${value} = Base Keyword Should Be Equal ${value} 42 +Plugins With Internal Python Objects + Import Library ${CURDIR}/PluginWithPythonObjectsLib.py plugins=${CURDIR}/MyPluginWithPythonObjects.py;123;98 + ${value} = Keyword With Python + Should Be Equal ${value} 123 + ${value} = Plugin Keyword With Python + Should Be Equal ${value} ${238} + Pugins With No Base Class Run Keyword And Expect Error ... *PluginError: Plugin does not inherit diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 1920af9..deb7e1f 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -281,8 +281,9 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ class PluginParser: - def __init__(self, base_class: Optional[Any] = None): + def __init__(self, base_class: Optional[Any] = None, python_object: List[Any] = []): self._base_class = base_class + self._python_object = python_object def parse_plugins(self, plugins: str) -> List: imported_plugins = [] @@ -292,7 +293,8 @@ def parse_plugins(self, plugins: str) -> List: if not inspect.isclass(plugin): message = f"Importing test library: '{parsed_plugin.module}' failed." raise DataError(message) - plugin = plugin(*parsed_plugin.args, **parsed_plugin.kw_args) + 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) diff --git a/utest/helpers/my_plugin_test.py b/utest/helpers/my_plugin_test.py index dfc6475..8f1d19d 100644 --- a/utest/helpers/my_plugin_test.py +++ b/utest/helpers/my_plugin_test.py @@ -28,3 +28,15 @@ def another_keyword(self) -> int: def normal_method(self): return "xxx" + + +class TestPluginWithPythonArgs(LibraryBase): + + def __init__(self, python_class, rf_arg): + self.python_class = python_class + self.rf_arg = rf_arg + super().__init__() + + @keyword + def include_python_object(self): + return self.python_class.x + self.python_class.y + int(self.rf_arg) diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index e8dfd94..441d4eb 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -64,3 +64,13 @@ def test_plugin_keywords(plugin_parser): assert len(keywords) == 2 assert keywords[0] == "another_keyword" assert keywords[1] == "new_keyword" + + +def test_plugin_python_objects(): + class PythonObject: + x = 1 + y = 2 + python_object = PythonObject() + parser = PluginParser(my_plugin_test.LibraryBase, [python_object]) + plugins = parser.parse_plugins("my_plugin_test.TestPluginWithPythonArgs;4") + assert len(plugins) From 37768bface9e64c0eae9b4b440c3e5ed7dc57792 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 17:28:31 +0200 Subject: [PATCH 055/148] Fix utest --- utest/test_plugin_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 441d4eb..9b6d488 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -73,4 +73,8 @@ class PythonObject: python_object = PythonObject() parser = PluginParser(my_plugin_test.LibraryBase, [python_object]) plugins = parser.parse_plugins("my_plugin_test.TestPluginWithPythonArgs;4") - assert len(plugins) + assert len(plugins) == 1 + plugin = plugins[0] + assert plugin.python_class.x == 1 + assert plugin.python_class.y == 2 + From 804fccfbd78b9cefb38c48e78831a5a6464957ed Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 18:19:29 +0200 Subject: [PATCH 056/148] Fix source path with decorator Fixes #99 --- atest/custon_deco.py | 15 +++++++++++++++ atest/librarycomponents.py | 4 +++- src/robotlibcore.py | 2 +- utest/test_get_keyword_source.py | 4 ++-- utest/test_robotlibcore.py | 4 ++-- 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 atest/custon_deco.py diff --git a/atest/custon_deco.py b/atest/custon_deco.py new file mode 100644 index 0000000..6aca28d --- /dev/null +++ b/atest/custon_deco.py @@ -0,0 +1,15 @@ +import functools + + +def custom_deco(arg1, arg2): + print(arg1, arg2) + + def actual_decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + print("BEFORE") + value = func(*args, **kwargs) + print("AFTER") + return value + return wrapper + return actual_decorator diff --git a/atest/librarycomponents.py b/atest/librarycomponents.py index 6859098..eda048e 100644 --- a/atest/librarycomponents.py +++ b/atest/librarycomponents.py @@ -1,3 +1,4 @@ +from custon_deco import custom_deco from robotlibcore import keyword @@ -13,8 +14,9 @@ class Names: def method(self): return 2 + @custom_deco("foo", "bar") @keyword('Custom name') - def _custom_name(self): + def _other_name_here(self): return 3 def not_keyword(self): diff --git a/src/robotlibcore.py b/src/robotlibcore.py index deb7e1f..a63b053 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -157,7 +157,7 @@ def __get_keyword_line(self, method): def __get_keyword_path(self, method): try: - return os.path.normpath(inspect.getfile(method)) + return os.path.normpath(inspect.getfile(inspect.unwrap(method))) except TypeError: return None diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 343e4fd..43c8ad9 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -45,7 +45,7 @@ def test_location_in_main(lib, lib_path): def test_location_in_class(lib, lib_path_components): source = lib.get_keyword_source("method") - assert source == "%s:13" % lib_path_components + assert source == f"{lib_path_components}:14" def test_decorator_wrapper(lib_types, lib_path_types): @@ -55,7 +55,7 @@ def test_decorator_wrapper(lib_types, lib_path_types): def test_location_in_class_custom_keyword_name(lib, lib_path_components): source = lib.get_keyword_source("Custom name") - assert source == "%s:17" % lib_path_components + assert source == f"{lib_path_components}:19" def test_no_line_number(lib, lib_path, when): diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 96fb410..06c4971 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -41,7 +41,7 @@ def test_dir(): "_DynamicCore__get_keyword_path", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", - "_custom_name", + "_other_name_here", "add_library_components", "all_arguments", "attributes", @@ -99,7 +99,7 @@ def test_getattr(): assert lib.instance_attribute == "not keyword" assert lib.function() == 1 assert lib.method() == 2 - assert lib._custom_name() == 3 + assert lib._other_name_here() == 3 assert getattr(lib, "Custom name")() == 3 with pytest.raises(AttributeError) as exc_info: lib.non_existing From 82bc845eddf76ec82504c3b34eaf87b760c69cde Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 18:28:42 +0200 Subject: [PATCH 057/148] Release notes for 4.0.0 --- docs/PythonLibCore-4.0.0.rst | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/PythonLibCore-4.0.0.rst diff --git a/docs/PythonLibCore-4.0.0.rst b/docs/PythonLibCore-4.0.0.rst new file mode 100644 index 0000000..9c18a85 --- /dev/null +++ b/docs/PythonLibCore-4.0.0.rst @@ -0,0 +1,110 @@ +========================= +Python Library Core 4.0.0 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.0.0 is +a new release with support for plugin API and bug fixe for library source. + +All issues targeted for Python Library Core v4.0.0 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.0.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.0.0 was released on Saturday November 5, 2022. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.0.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +Add support for plugin API from SeleniumLibrary (`#103`_) +--------------------------------------------------------- +PLC now support similar plugin API as SeleniumLibrary. This makes +implementation of plugin easier for other libraries in community. + +Support Python 3.10 and ensure that new type hints works (`#87`_) +---------------------------------------------------------------- +Support for Python 3.10. + +Decorator resolves as wron file path (`#99`_) +---------------------------------------------- +Keyword with decorators did not resolve correct path when decorator +was in different file. This is now fixed. + +Backwards incompatible changes +============================== + +Drop RF 3.2 support (`#85`_) +---------------------------- +RF 3.2 is not tested and therefore not officially supported. + +Drop Python 3.6 suopport (`#92`_) +--------------------------------- +Python 3.6 has been end of life for some time and therefore it is +not anymore supported. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#103`_ + - enhancement + - critical + - Add support for plugin API from SeleniumLibrary + * - `#85`_ + - enhancement + - critical + - Drop RF 3.2 support + * - `#87`_ + - enhancement + - critical + - Support Python 3.10 and ensure that new type hints works + * - `#92`_ + - enhancement + - critical + - Drop Python 3.6 suopport + * - `#99`_ + - bug + - high + - Decorator resolves as wron file path + +Altogether 5 issues. View on the `issue tracker `__. + +.. _#103: https://github.com/robotframework/PythonLibCore/issues/103 +.. _#85: https://github.com/robotframework/PythonLibCore/issues/85 +.. _#87: https://github.com/robotframework/PythonLibCore/issues/87 +.. _#92: https://github.com/robotframework/PythonLibCore/issues/92 +.. _#99: https://github.com/robotframework/PythonLibCore/issues/99 From af17c2452e2a4070c50a0217ac549b378d413633 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 18:30:33 +0200 Subject: [PATCH 058/148] Updated version to 4.0.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index a63b053..8d2cada 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "3.0.1.dev1" +__version__ = "4.0.0" class PythonLibCoreException(Exception): From 84c73979e309f59de057ae6a77725ab0f468b71f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 18:33:53 +0200 Subject: [PATCH 059/148] fix finding version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02c227b..17c6327 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ Framework :: Robot Framework """.strip().splitlines() with open(join(CURDIR, 'src', 'robotlibcore.py')) as f: - VERSION = re.search("\n__version__ = '(.*)'", f.read()).group(1) + VERSION = re.search('\n__version__ = "(.*)"', f.read()).group(1) with open(join(CURDIR, 'README.rst')) as f: LONG_DESCRIPTION = f.read() From c086ef313063fb9b1a74333ccb67b064f5acd965 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 5 Nov 2022 18:37:04 +0200 Subject: [PATCH 060/148] Back to dev version --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 8d2cada..e3d6ef4 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.0.0" +__version__ = "4.0.1.dev1" class PythonLibCoreException(Exception): From 8041ef6afe491b1210c1a87d9e324d277ec48ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 12:07:56 +0100 Subject: [PATCH 061/148] added automatic listener detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- atest/ListenerCore.py | 66 ++++++++++++++++++++++++++++++++++++++ atest/tests_listener.robot | 13 ++++++++ src/robotlibcore.py | 34 ++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 atest/ListenerCore.py create mode 100644 atest/tests_listener.robot diff --git a/atest/ListenerCore.py b/atest/ListenerCore.py new file mode 100644 index 0000000..1233910 --- /dev/null +++ b/atest/ListenerCore.py @@ -0,0 +1,66 @@ +from robot.api import logger + + +from robotlibcore import DynamicCore, keyword + + +class ListenerCore(DynamicCore): + + ROBOT_LIBRARY_SCOPE = 'GLOBAL' + + def __init__(self): + self.keyword_name = None + self.keyword_args = {} + self.ROBOT_LISTENER_API_VERSION = 2 + second_comp = SecondComponent() + self.ROBOT_LIBRARY_LISTENER = second_comp.listener + components = [FirstComponent(), second_comp] + super().__init__(components) + + @keyword + def listener_core(self, arg: str): + logger.info(arg) + assert arg == self.keyword_args.get("args", [None])[0], "First argument should be detected by listener, but was not." + + def start_keyword(self, name, args): + self.keyword_name = name + self.keyword_args = args + logger.info(f"start: {name}") + + +class FirstComponent: + + def __init__(self): + self.ROBOT_LISTENER_API_VERSION = 2 + self.suite_name = '' + + def _start_suite(self, name, attrs): + self.suite_name = name + logger.console(f"start suite: {name}") + + @keyword + def first_component(self, arg: str): + logger.info(arg) + assert arg == self.suite_name, f"Suite name '{self.suite_name}' should be detected by listener, but was not." + + +class SecondComponent: + + def __init__(self): + self.listener = ExternalListener() + + @keyword + def second_component(self, arg: str): + logger.info(arg) + assert self.listener.test.name == arg, "Test case name should be detected by listener, but was not." + + +class ExternalListener: + + ROBOT_LISTENER_API_VERSION = 3 + + def __init__(self): + self.test = None + + def start_test(self, test, _): + self.test = test diff --git a/atest/tests_listener.robot b/atest/tests_listener.robot new file mode 100644 index 0000000..176573e --- /dev/null +++ b/atest/tests_listener.robot @@ -0,0 +1,13 @@ +*** Settings *** +Library ListenerCore.py + + +*** Test Cases *** +Automatic Listener + Listener Core This is the first Argument + +External Listener + Second Component External Listener + +No Listener + First Component Tests Listener diff --git a/src/robotlibcore.py b/src/robotlibcore.py index e3d6ef4..798bcb0 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -45,6 +45,7 @@ def __init__(self, library_components): self.attributes = {} self.add_library_components(library_components) self.add_library_components([self]) + self._set_library_listeners(library_components) def add_library_components(self, library_components): self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) @@ -59,6 +60,39 @@ def add_library_components(self, library_components): # method names as well as possible custom names. self.attributes[name] = self.attributes[kw_name] = kw + def _set_library_listeners(self, library_components): + listeners = self._get_component_listeners(library_components) + listeners = self._insert_manually_registered_listeners(listeners) + listeners = self._insert_self_to_listeners(listeners) + if listeners: + self.ROBOT_LIBRARY_LISTENER = listeners + + def _insert_self_to_listeners(self, component_listeners): + if self not in component_listeners: + try: + getattr(self, "ROBOT_LISTENER_API_VERSION") + return [self, *component_listeners] + except AttributeError: + pass + return component_listeners + + def _insert_manually_registered_listeners(self, component_listeners: list) -> list: + try: + manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER") + try: + return [*manually_registered_listener, *component_listeners] + except TypeError: + return [manually_registered_listener, *component_listeners] + except AttributeError: + return component_listeners + + def _get_component_listeners(self, library_listeners): + 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) From b1a596e62628f8cd650e0a39d15ee23c3e26e2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 12:10:04 +0100 Subject: [PATCH 062/148] linting... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- atest/tests_listener.robot | 2 +- src/robotlibcore.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/atest/tests_listener.robot b/atest/tests_listener.robot index 176573e..7e319ed 100644 --- a/atest/tests_listener.robot +++ b/atest/tests_listener.robot @@ -1,5 +1,5 @@ *** Settings *** -Library ListenerCore.py +Library ListenerCore.py *** Test Cases *** diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 798bcb0..2cee941 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -87,11 +87,7 @@ def _insert_manually_registered_listeners(self, component_listeners: list) -> li return component_listeners def _get_component_listeners(self, library_listeners): - return [ - component - for component in library_listeners - if hasattr(component, "ROBOT_LISTENER_API_VERSION") - ] + return [component for component in library_listeners if hasattr(component, "ROBOT_LISTENER_API_VERSION")] def __get_members(self, component): if inspect.ismodule(component): From 7f8b7c0d98025b9f74947d40fd4911cecff58198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 12:13:12 +0100 Subject: [PATCH 063/148] utest fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- utest/test_robotlibcore.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 06c4971..b931f6d 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -41,7 +41,11 @@ def test_dir(): "_DynamicCore__get_keyword_path", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", - "_other_name_here", + '_get_component_listeners', + '_insert_manually_registered_listeners', + '_insert_self_to_listeners', + '_other_name_here', + '_set_library_listeners', "add_library_components", "all_arguments", "attributes", From 17b06f4533c8647961555ed4976fbb6f0111be89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 12:16:33 +0100 Subject: [PATCH 064/148] added type hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- src/robotlibcore.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 2cee941..c752f73 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -60,14 +60,14 @@ def add_library_components(self, library_components): # method names as well as possible custom names. self.attributes[name] = self.attributes[kw_name] = kw - def _set_library_listeners(self, library_components): + def _set_library_listeners(self, library_components: list): listeners = self._get_component_listeners(library_components) listeners = self._insert_manually_registered_listeners(listeners) listeners = self._insert_self_to_listeners(listeners) if listeners: self.ROBOT_LIBRARY_LISTENER = listeners - def _insert_self_to_listeners(self, component_listeners): + def _insert_self_to_listeners(self, component_listeners: list) -> list: if self not in component_listeners: try: getattr(self, "ROBOT_LISTENER_API_VERSION") @@ -86,7 +86,7 @@ def _insert_manually_registered_listeners(self, component_listeners: list) -> li except AttributeError: return component_listeners - def _get_component_listeners(self, library_listeners): + 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): From a019d261ac3967fc298b2f261435f5dfdc452aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 13:24:00 +0100 Subject: [PATCH 065/148] refactored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- src/robotlibcore.py | 24 +++++++----------------- utest/test_robotlibcore.py | 6 +----- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index c752f73..6f24dc7 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -45,7 +45,7 @@ def __init__(self, library_components): self.attributes = {} self.add_library_components(library_components) self.add_library_components([self]) - self._set_library_listeners(library_components) + self.__set_library_listeners(library_components) def add_library_components(self, library_components): self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) @@ -60,23 +60,13 @@ def add_library_components(self, library_components): # method names as well as possible custom names. self.attributes[name] = self.attributes[kw_name] = kw - def _set_library_listeners(self, library_components: list): - listeners = self._get_component_listeners(library_components) - listeners = self._insert_manually_registered_listeners(listeners) - listeners = self._insert_self_to_listeners(listeners) + def __set_library_listeners(self, library_components: list): + listeners = self.__get_component_listeners([self, *library_components]) + listeners = self.__insert_manually_registered_listeners(listeners) if listeners: - self.ROBOT_LIBRARY_LISTENER = listeners + self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners).keys()) - def _insert_self_to_listeners(self, component_listeners: list) -> list: - if self not in component_listeners: - try: - getattr(self, "ROBOT_LISTENER_API_VERSION") - return [self, *component_listeners] - except AttributeError: - pass - return component_listeners - - def _insert_manually_registered_listeners(self, component_listeners: list) -> list: + def __insert_manually_registered_listeners(self, component_listeners: list) -> list: try: manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER") try: @@ -86,7 +76,7 @@ def _insert_manually_registered_listeners(self, component_listeners: list) -> li except AttributeError: return component_listeners - def _get_component_listeners(self, library_listeners: list) -> list: + 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): diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index b931f6d..06c4971 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -41,11 +41,7 @@ def test_dir(): "_DynamicCore__get_keyword_path", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", - '_get_component_listeners', - '_insert_manually_registered_listeners', - '_insert_self_to_listeners', - '_other_name_here', - '_set_library_listeners', + "_other_name_here", "add_library_components", "all_arguments", "attributes", From b7e3027b329df57c22d58a3ee6bb7bf7d9a36357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 13:26:42 +0100 Subject: [PATCH 066/148] refactored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- utest/test_robotlibcore.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 06c4971..722565e 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -39,8 +39,11 @@ def test_dir(): "_DynamicCore__get_keyword", "_DynamicCore__get_keyword_line", "_DynamicCore__get_keyword_path", + "_HybridCore__get_component_listeners", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", + "_HybridCore__insert_manually_registered_listeners", + "_HybridCore__set_library_listeners", "_other_name_here", "add_library_components", "all_arguments", From b52603855bd8404b7d9ec01dc45303104508d7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Fri, 27 Jan 2023 13:33:16 +0100 Subject: [PATCH 067/148] refactored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- atest/tests_listener.robot | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/atest/tests_listener.robot b/atest/tests_listener.robot index 7e319ed..43bb131 100644 --- a/atest/tests_listener.robot +++ b/atest/tests_listener.robot @@ -3,11 +3,23 @@ Library ListenerCore.py *** Test Cases *** -Automatic Listener +Tests The Keyword Argument + [Documentation] This test case tests that the keyword argument is equal + ... to the keyword argument from start_keyword. + ... + ... It uses the core lib as listener. Listener Core This is the first Argument -External Listener - Second Component External Listener +Tests The Test Name + [Documentation] This test case tests that the test case name is equal + ... to the test name from start_test. + ... + ... It uses a component as listener. + Second Component Tests The Test Name -No Listener +Tests The Suite Name + [Documentation] This test case tests that the suite name is equal + ... to the suite name from _start_suite. + ... + ... It uses an independent class as listener which is manually set. First Component Tests Listener From e5b4f9f8fb161eb45c419b8a1499784724017822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Sat, 28 Jan 2023 15:46:28 +0100 Subject: [PATCH 068/148] fixed review .keys() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 6f24dc7..d5c9daa 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -64,7 +64,7 @@ def __set_library_listeners(self, library_components: list): listeners = self.__get_component_listeners([self, *library_components]) listeners = self.__insert_manually_registered_listeners(listeners) if listeners: - self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners).keys()) + self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners)) def __insert_manually_registered_listeners(self, component_listeners: list) -> list: try: From 729aa6c7504982052cf410deb967c6615d331cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Sun, 29 Jan 2023 11:22:16 +0100 Subject: [PATCH 069/148] fixed review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- atest/ListenerCore.py | 8 -------- src/robotlibcore.py | 11 ++++------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/atest/ListenerCore.py b/atest/ListenerCore.py index 1233910..899810f 100644 --- a/atest/ListenerCore.py +++ b/atest/ListenerCore.py @@ -1,6 +1,3 @@ -from robot.api import logger - - from robotlibcore import DynamicCore, keyword @@ -19,13 +16,11 @@ def __init__(self): @keyword def listener_core(self, arg: str): - logger.info(arg) assert arg == self.keyword_args.get("args", [None])[0], "First argument should be detected by listener, but was not." def start_keyword(self, name, args): self.keyword_name = name self.keyword_args = args - logger.info(f"start: {name}") class FirstComponent: @@ -36,11 +31,9 @@ def __init__(self): def _start_suite(self, name, attrs): self.suite_name = name - logger.console(f"start suite: {name}") @keyword def first_component(self, arg: str): - logger.info(arg) assert arg == self.suite_name, f"Suite name '{self.suite_name}' should be detected by listener, but was not." @@ -51,7 +44,6 @@ def __init__(self): @keyword def second_component(self, arg: str): - logger.info(arg) assert self.listener.test.name == arg, "Test case name should be detected by listener, but was not." diff --git a/src/robotlibcore.py b/src/robotlibcore.py index d5c9daa..ad61670 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -67,14 +67,11 @@ def __set_library_listeners(self, library_components: list): self.ROBOT_LIBRARY_LISTENER = list(dict.fromkeys(listeners)) def __insert_manually_registered_listeners(self, component_listeners: list) -> list: + manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER", []) try: - manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER") - try: - return [*manually_registered_listener, *component_listeners] - except TypeError: - return [manually_registered_listener, *component_listeners] - except AttributeError: - return component_listeners + return [*manually_registered_listener, *component_listeners] + except TypeError: + return [manually_registered_listener, *component_listeners] def __get_component_listeners(self, library_listeners: list) -> list: return [component for component in library_listeners if hasattr(component, "ROBOT_LISTENER_API_VERSION")] From fd15e5406a02be5af8fbf1845cd7b20d770e97b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Sun, 29 Jan 2023 11:27:24 +0100 Subject: [PATCH 070/148] small refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- src/robotlibcore.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index ad61670..9f1b59d 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -61,17 +61,17 @@ def add_library_components(self, library_components): self.attributes[name] = self.attributes[kw_name] = kw def __set_library_listeners(self, library_components: list): - listeners = self.__get_component_listeners([self, *library_components]) - listeners = self.__insert_manually_registered_listeners(listeners) + 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 __insert_manually_registered_listeners(self, component_listeners: list) -> list: + def __get_manually_registered_listeners(self) -> list: manually_registered_listener = getattr(self, "ROBOT_LIBRARY_LISTENER", []) try: - return [*manually_registered_listener, *component_listeners] + return [*manually_registered_listener] except TypeError: - return [manually_registered_listener, *component_listeners] + 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")] From a10af5564041af22049bf907f98790f8910d598c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Sun, 29 Jan 2023 11:38:19 +0100 Subject: [PATCH 071/148] fixed utest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- utest/test_robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 722565e..1011c13 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -40,9 +40,9 @@ def test_dir(): "_DynamicCore__get_keyword_line", "_DynamicCore__get_keyword_path", "_HybridCore__get_component_listeners", + "_HybridCore__get_manually_registered_listeners", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", - "_HybridCore__insert_manually_registered_listeners", "_HybridCore__set_library_listeners", "_other_name_here", "add_library_components", From fdd8d16cb56cb28c7209cfe092714c1f87ec6fbb Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 22:18:54 +0200 Subject: [PATCH 072/148] Release notes for 4.1.0 --- docs/PythonLibCore-4.1.0.rst | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/PythonLibCore-4.1.0.rst diff --git a/docs/PythonLibCore-4.1.0.rst b/docs/PythonLibCore-4.1.0.rst new file mode 100644 index 0000000..9c717fe --- /dev/null +++ b/docs/PythonLibCore-4.1.0.rst @@ -0,0 +1,71 @@ +========================= +Python Library Core 4.1.0 +========================= + + +.. default-role:: code + + +`PythonLibCore`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.1.0 is +a new release with support registering Robot Framework listener from +keyword class. + +All issues targeted for Python Library Core v4.1.0 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.1.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.1.0 was released on Tuesday January 31, 2023. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.1.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +Add support adding classes also as a listener. (`#107`_) +--------------------------------------------------------- +Now it is possible to register Robot Framework listener also from the +class which implements keyword and not only from the library main class. + + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#107`_ + - enhancement + - critical + - Add support adding classes also as a listener. + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#107: https://github.com/robotframework/PythonLibCore/issues/107 From 8fa602f7222eb60c7f3ef4c217f4faf91993d4b0 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 22:21:32 +0200 Subject: [PATCH 073/148] Updated version to 4.1.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 9f1b59d..524289f 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.0.1.dev1" +__version__ = "4.1.0" class PythonLibCoreException(Exception): From 594a2bf88bf1735d28da7d21d0297f5e06041d31 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 22:26:15 +0200 Subject: [PATCH 074/148] Back to dev version --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 524289f..f95d292 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.1.0" +__version__ = "4.1.0.dev1" class PythonLibCoreException(Exception): From 6d19325e683c5e2c9cd18f86dee8c57c9e2a0a4b Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 22:57:04 +0200 Subject: [PATCH 075/148] Add example for plugins --- README.rst | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.rst b/README.rst index fcb5ca9..5166682 100644 --- a/README.rst +++ b/README.rst @@ -94,6 +94,52 @@ Example pass +Plugin API +---------- +It is possible to create plugin API to a library by using PythonLibCore. This allows extending library +with external Python classes. Plugins can be imported during library import time, example by defining argumet +in library `__init__` which allows defining the plugins. It is possible to define multiple plugins, by seperating +plugins with with comma. Also it is possible to provide arguments to plugin by seperating arguments with +semicolon. + + +.. sourcecode:: python + + from robot.api.deco import keyword # noqa F401 + + from robotlibcore import DynamicCore, PluginParser + + from mystuff import Library1, Library2 + + + class PluginLib(DynamicCore): + + def __init__(self, plugins): + plugin_parser = PluginParser() + libraries = [Library1(), Library2()] + parsed_plugins = plugin_parser.parse_plugins(plugins) + libraries.extend(parsed_plugins) + DynamicCore.__init__(self, libraries) + + +When plugin class can look like this: + +.. sourcecode:: python + + class MyPlugi: + + @keyword + def plugin_keyword(self): + return 123 + +Then Library can be imported in Robot Framework side like this: + +.. sourcecode:: bash + + Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py + + + .. _Robot Framework: http://robotframework.org .. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary/ .. _WhiteLibrary: https://pypi.org/project/robotframework-whitelibrary/ From 4911928b0fc3aa41aa47c33cbc43f4aaabd47ff9 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 22:59:41 +0200 Subject: [PATCH 076/148] Use RF 6.0.2 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7c578fc..5c634d3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: [3.7, 3.10.7] - rf-version: [5.0.1, 6.0.1] + rf-version: [5.0.1, 6.0.2] steps: - uses: actions/checkout@v2 From 448f9e80167773128af150fa27079232d069d49d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 31 Jan 2023 23:02:48 +0200 Subject: [PATCH 077/148] Use Python 3.11.1 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5c634d3..f566616 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.10.7] + python-version: [3.7, 3.11.1] rf-version: [5.0.1, 6.0.2] steps: From 9315f4fa0b0166758ac17a45d15a3445eeaa383a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 14 Feb 2023 22:29:39 +0200 Subject: [PATCH 078/148] Add dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..10c6e91 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" From a6a57faaf6b105e63c461f638f7e168c10265cff Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 14 Feb 2023 22:16:52 +0200 Subject: [PATCH 079/148] Fix named only arguments Fixes #111 --- atest/DynamicTypesAnnotationsLibrary.py | 16 ++++++++++++++++ atest/Python310Library.py | 1 + atest/tests.robot | 24 ++++++++++++------------ atest/tests_types.robot | 5 ++++- src/robotlibcore.py | 10 +++++++--- tasks.py | 6 ++++++ utest/test_get_keyword_types.py | 5 +++++ utest/test_robotlibcore.py | 7 +++++++ 8 files changed, 58 insertions(+), 16 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 7b536c4..be721f5 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -156,3 +156,19 @@ def keyword_optional_with_none(self, arg: Optional[str] = None): @keyword def keyword_union_with_none(self, arg: Union[None, Dict, str] = None): return f"arg: {arg}, type: {type(arg)}" + + @keyword + def kw_with_named_arguments(self, *, arg): + print(arg) + return f"arg: {arg}, type: {type(arg)}" + + @keyword + def kw_with_many_named_arguments(self, *, arg1, arg2): + print(arg1) + print(arg2) + return f"arg1: {arg1}, type: {type(arg1)}, arg2: {arg2}, type: {type(arg2)}" + + @keyword + def kw_with_named_arguments_and_variable_number_args(self, *varargs, arg): + print(arg) + return f"arg: {arg}, type: {type(arg)}" diff --git a/atest/Python310Library.py b/atest/Python310Library.py index d773b0b..5d8aeb3 100644 --- a/atest/Python310Library.py +++ b/atest/Python310Library.py @@ -2,6 +2,7 @@ from robotlibcore import DynamicCore, keyword + class Python310Library(DynamicCore): def __init__(self): diff --git a/atest/tests.robot b/atest/tests.robot index 962adeb..3099c07 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -7,14 +7,14 @@ ${LIBRARY} DynamicLibrary *** Test Cases *** -Keyword names - Keyword in main +Keyword Names + Keyword In Main Function FUNCTION Method - Custom name - Cust omna me - IF $LIBRARY == "ExtendExistingLibrary" Keyword in extending library + Custom Name + Cust Omna Me + IF $LIBRARY == "ExtendExistingLibrary" Keyword In Extending Library Method without @keyword are not keyowrds [Documentation] FAIL GLOB: No keyword with name 'Not keyword' found.* @@ -27,27 +27,27 @@ Arguments 'foo', 2, 3 Defaults foo ${2} 'a', 'b', 'c' Defaults a b c -Named arguments +Named Arguments [Template] Return value should be 'foo', 'bar' Mandatory foo arg2=bar '1', 2 Mandatory arg2=${2} arg1=1 'x', 'default', 'y' Defaults x arg3=y -Varargs and kwargs +Varargs And Kwargs [Template] Return value should be ${EMPTY} Varargs and kwargs 'a', 'b', 'c' Varargs and kwargs a b c a\='1', b\=2 Varargs and kwargs a=1 b=${2} 'a', 'b\=b', c\='c' Varargs and kwargs a b\=b c=c -Embedded arguments - [Documentation] FAIL Work but this fails - Embedded arguments "work" - embeDded ArgumeNtS "Work but this fails" +Embedded Arguments + [Documentation] FAIL Work But This Fails + Embedded Arguments "work" + EmbeDded ArgumeNtS "Work But This Fails" *** Keywords *** -Return value should be +Return Value Should Be [Arguments] ${expected} ${keyword} @{args} &{kwargs} ${result} Run Keyword ${keyword} @{args} &{kwargs} Should Be Equal ${result} ${expected} diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 3337617..23a20fb 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -52,7 +52,7 @@ Keyword Only Arguments Without VarArg ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments No Vararg other=tidii Should Match ${return} tidii: -Varargs and KeywordArgs With Typing Hints +Varargs And KeywordArgs With Typing Hints ${return} = DynamicTypesAnnotationsLibrary.Keyword Self And Keyword Only Types ... this_is_mandatory # mandatory argument ... 1 2 3 4 # varargs @@ -112,6 +112,9 @@ Python 3.10 New Type Hints Should Be Equal ${types} arg: {"key": 1}, type: END +Keyword With Named Only Arguments + Kw With Named Arguments arg=1 + *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3.10 Only diff --git a/src/robotlibcore.py b/src/robotlibcore.py index f95d292..d03619a 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -229,18 +229,22 @@ def _drop_self_from_args(cls, function, arg_spec): @classmethod def _get_var_args(cls, arg_spec): if arg_spec.varargs: - return ["*%s" % arg_spec.varargs] + return [f"*{arg_spec.varargs}"] return [] @classmethod def _get_kwargs(cls, arg_spec): - return ["**%s" % arg_spec.varkw] if arg_spec.varkw else [] + return [f"**{arg_spec.varkw}"] if arg_spec.varkw else [] @classmethod def _get_kw_only(cls, arg_spec): kw_only_args = [] + kw_only_defaults = arg_spec.kwonlydefaults if arg_spec.kwonlydefaults else [] for arg in arg_spec.kwonlyargs: - if not arg_spec.kwonlydefaults or arg not in arg_spec.kwonlydefaults: + if not arg_spec.varargs and arg not in kw_only_defaults and not kw_only_args: + kw_only_args.append("*") + kw_only_args.append(arg) + elif arg not in kw_only_defaults: kw_only_args.append(arg) else: value = arg_spec.kwonlydefaults.get(arg, "") diff --git a/tasks.py b/tasks.py index 6d3b45f..140368d 100644 --- a/tasks.py +++ b/tasks.py @@ -132,6 +132,12 @@ def lint(ctx): print(f"Lint Robot files {'in ci' if in_ci else ''}") command = [ "robotidy", + "--transform", + "RenameKeywords", + "--transform", + "RenameTestCases", + "-c", + "RenameTestCases:capitalize_each_word=True", "--lineseparator", "unix", "atest/", diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index d072f9e..9d790c9 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -192,3 +192,8 @@ def test_keyword_optional_with_none(lib_types): def test_keyword_union_with_none(lib_types): types = lib_types.get_keyword_types("keyword_union_with_none") assert types == {"arg": typing.Union[type(None), typing.Dict, str]} + + +def test_kw_with_named_arguments(lib_types: DynamicTypesAnnotationsLibrary): + types = lib_types.get_keyword_types("kw_with_named_arguments") + assert types == {} diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 1011c13..440a67b 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -132,6 +132,13 @@ def test_keyword_only_arguments_for_get_keyword_arguments(): assert args("keyword_with_deco_and_signature") == [("arg1", False), ("arg2", False)] +def test_named_only_argumens(): + args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments + assert args("kw_with_named_arguments") == ["*", "arg"] + assert args("kw_with_many_named_arguments") == ["*", "arg1", "arg2"] + assert args("kw_with_named_arguments_and_variable_number_args") == ["*varargs", "arg"] + + def test_get_keyword_documentation(): doc = DynamicLibrary().get_keyword_documentation assert doc("function") == "" From b12c2b584c87ed481fa7a769b058b0a74699baba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:31:22 +0000 Subject: [PATCH 080/148] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f566616..30037d9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,7 +12,7 @@ jobs: rf-version: [5.0.1, 6.0.2] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} with Robot Framework ${{ matrix.rf-version }} uses: actions/setup-python@v2 with: From b901165aa628a13ab91e550ba9b8c95f9d8164af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:37:24 +0000 Subject: [PATCH 081/148] Bump actions/setup-python from 2 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 30037d9..bfd2611 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} with Robot Framework ${{ matrix.rf-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From c7124c2a0f97769cad8037f7cf5a040bf1ff5379 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:31:15 +0000 Subject: [PATCH 082/148] Bump actions/upload-artifact from 1 to 3 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bfd2611..3a5cb28 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -36,7 +36,7 @@ jobs: - name: Run acceptance tests run: | python atest/run.py - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: atest_results From cf07a47f34179a86e387da02c1c3e98075041c6d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 17 Feb 2023 21:03:54 +0200 Subject: [PATCH 083/148] Add tests --- atest/DynamicTypesAnnotationsLibrary.py | 6 ++++++ utest/test_get_keyword_types.py | 5 +++++ utest/test_robotlibcore.py | 1 + 3 files changed, 12 insertions(+) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index be721f5..f601dfc 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -172,3 +172,9 @@ def kw_with_many_named_arguments(self, *, arg1, arg2): def kw_with_named_arguments_and_variable_number_args(self, *varargs, arg): print(arg) return f"arg: {arg}, type: {type(arg)}" + + @keyword + def kw_with_many_named_arguments_with_default(self, *, arg1, arg2: int): + print(arg1) + print(arg2) + return f"arg1: {arg1}, type: {type(arg1)}, arg2: {arg2}, type: {type(arg2)}" diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 9d790c9..51c2f95 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -197,3 +197,8 @@ def test_keyword_union_with_none(lib_types): def test_kw_with_named_arguments(lib_types: DynamicTypesAnnotationsLibrary): types = lib_types.get_keyword_types("kw_with_named_arguments") assert types == {} + + +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} diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 440a67b..3b23c5e 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -137,6 +137,7 @@ def test_named_only_argumens(): assert args("kw_with_named_arguments") == ["*", "arg"] assert args("kw_with_many_named_arguments") == ["*", "arg1", "arg2"] assert args("kw_with_named_arguments_and_variable_number_args") == ["*varargs", "arg"] + assert args("kw_with_many_named_arguments_with_default") == ["*", "arg1", "arg2"] def test_get_keyword_documentation(): From 5cd79530b23657bb40ba11ce1ef8b31f967fb419 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 17 Feb 2023 21:15:07 +0200 Subject: [PATCH 084/148] Release notes for 4.1.1 --- docs/PythonLibCore-4.1.1.rst | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/PythonLibCore-4.1.1.rst diff --git a/docs/PythonLibCore-4.1.1.rst b/docs/PythonLibCore-4.1.1.rst new file mode 100644 index 0000000..820921f --- /dev/null +++ b/docs/PythonLibCore-4.1.1.rst @@ -0,0 +1,70 @@ +========================= +Python Library Core 4.1.1 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.1.1 is +a new hotfix release with bug fixes for named arguments support. + +All issues targeted for Python Library Core v4.1.1 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.1.1 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.1.1 was released on Friday February 17, 2023. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.1.1 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +`DynamicCore` doesn't handle named only arguments properly (`#111`_) +-------------------------------------------------------------------- +PLC did not handle named only argumets correctly. If keyword looked like: +`def kw(self, *, arg)` then argument secification not correcly returned. + + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#111`_ + - bug + - high + - `DynamicCore` doesn't handle named only arguments properly + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#111: https://github.com/robotframework/PythonLibCore/issues/111 From 504e1837de2829b7780d32ec39011405d590e342 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 17 Feb 2023 21:16:15 +0200 Subject: [PATCH 085/148] Updated version to 4.1.1 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index d03619a..e4ec77f 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.1.0.dev1" +__version__ = "4.1.1" class PythonLibCoreException(Exception): From c7ac7944e3330b9cbc0fcf0d082d3c2ee5e015d5 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 00:25:08 +0200 Subject: [PATCH 086/148] lint fixes --- atest/tests.robot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atest/tests.robot b/atest/tests.robot index 3099c07..bac73fd 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -16,9 +16,9 @@ Keyword Names Cust Omna Me IF $LIBRARY == "ExtendExistingLibrary" Keyword In Extending Library -Method without @keyword are not keyowrds +Method Without @keyword Are Not Keyowrds [Documentation] FAIL GLOB: No keyword with name 'Not keyword' found.* - Not keyword + Not Keyword Arguments [Template] Return value should be From 77cac537cc044e8245be3819537173b1794542be Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 00:32:45 +0200 Subject: [PATCH 087/148] Fixing named argumens with default values Simply logic also Fixes #118 --- atest/DynamicTypesAnnotationsLibrary.py | 9 +++++ atest/tests.robot | 2 +- src/robotlibcore.py | 47 +++++++++++-------------- utest/test_keyword_builder.py | 2 +- utest/test_robotlibcore.py | 2 ++ 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index f601dfc..79732b1 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -178,3 +178,12 @@ def kw_with_many_named_arguments_with_default(self, *, arg1, arg2: int): print(arg1) print(arg2) return f"arg1: {arg1}, type: {type(arg1)}, arg2: {arg2}, type: {type(arg2)}" + + @keyword + def kw_with_positional_and_named_arguments(self, arg1, *, arg2: int): + return f"arg1: {arg1}, type: {type(arg1)}, arg2: {arg2}, type: {type(arg2)}" + + @keyword + def kw_with_positional_and_named_arguments_with_defaults(self, arg1: int = 1, *, arg2: str = "foobar"): + return f"arg1: {arg1}, type: {type(arg1)}, arg2: {arg2}, type: {type(arg2)}" + diff --git a/atest/tests.robot b/atest/tests.robot index bac73fd..3c66808 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -17,7 +17,7 @@ Keyword Names IF $LIBRARY == "ExtendExistingLibrary" Keyword In Extending Library Method Without @keyword Are Not Keyowrds - [Documentation] FAIL GLOB: No keyword with name 'Not keyword' found.* + [Documentation] FAIL GLOB: No keyword with name 'Not Keyword' found.* Not Keyword Arguments diff --git a/src/robotlibcore.py b/src/robotlibcore.py index e4ec77f..51004d6 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,13 +21,13 @@ import inspect import os from dataclasses import dataclass -from typing import Any, List, Optional, get_type_hints +from typing import Any, Callable, List, Optional, get_type_hints from robot.api.deco import keyword # noqa F401 from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.1.1" +__version__ = "4.1.2" class PythonLibCoreException(Exception): @@ -196,20 +196,18 @@ def unwrap(cls, function): def _get_arguments(cls, function): unwrap_function = cls.unwrap(function) arg_spec = cls._get_arg_spec(unwrap_function) - argument_specification = cls._get_default_and_named_args(arg_spec, function) - argument_specification.extend(cls._get_var_args(arg_spec)) - kw_only_args = cls._get_kw_only(arg_spec) - if kw_only_args: - argument_specification.extend(kw_only_args) + 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): + def _get_arg_spec(cls, function: Callable): return inspect.getfullargspec(function) @classmethod - def _get_default_and_named_args(cls, arg_spec, function): + def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable): args = cls._drop_self_from_args(function, arg_spec) args.reverse() defaults = list(arg_spec.defaults) if arg_spec.defaults else [] @@ -223,33 +221,30 @@ def _get_default_and_named_args(cls, arg_spec, function): return formated_args @classmethod - def _drop_self_from_args(cls, function, arg_spec): + def _drop_self_from_args(cls, function: Callable, arg_spec: inspect.FullArgSpec): return arg_spec.args[1:] if inspect.ismethod(function) else arg_spec.args @classmethod - def _get_var_args(cls, arg_spec): - if arg_spec.varargs: - return [f"*{arg_spec.varargs}"] - return [] + 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): + def _get_kwargs(cls, arg_spec: inspect.FullArgSpec) -> list: return [f"**{arg_spec.varkw}"] if arg_spec.varkw else [] @classmethod - def _get_kw_only(cls, arg_spec): - kw_only_args = [] + def _get_named_only_args(cls, arg_spec: inspect.FullArgSpec) -> list: + rf_spec = [] + 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 arg in arg_spec.kwonlyargs: - if not arg_spec.varargs and arg not in kw_only_defaults and not kw_only_args: - kw_only_args.append("*") - kw_only_args.append(arg) - elif arg not in kw_only_defaults: - kw_only_args.append(arg) + 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: - value = arg_spec.kwonlydefaults.get(arg, "") - kw_only_args.append((arg, value)) - return kw_only_args + rf_spec.append(kw_only_arg) + return rf_spec @classmethod def _get_types(cls, function): diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 8eb622f..42ccad1 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -38,7 +38,7 @@ def test_positional_and_named(lib): assert spec.argument_specification == ["arg1", "arg2", ("named1", "string1"), ("named2", 123)] -def test_named_only(lib): +def test_named_only_default_only(lib): spec = KeywordBuilder.build(lib.default_only) assert spec.argument_specification == [("named1", "string1"), ("named2", 123)] diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 3b23c5e..cd14b86 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -138,6 +138,8 @@ def test_named_only_argumens(): assert args("kw_with_many_named_arguments") == ["*", "arg1", "arg2"] assert args("kw_with_named_arguments_and_variable_number_args") == ["*varargs", "arg"] assert args("kw_with_many_named_arguments_with_default") == ["*", "arg1", "arg2"] + assert args("kw_with_positional_and_named_arguments") == ["arg1", "*", "arg2"] + assert args("kw_with_positional_and_named_arguments_with_defaults") == [("arg1", 1), "*", ("arg2", "foobar")] def test_get_keyword_documentation(): From 7c66fbb5800aa8b47798e3772b7c26c1bcffc224 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 00:41:23 +0200 Subject: [PATCH 088/148] Release notes for 4.1.2 --- docs/PythonLibCore-4.1.2.rst | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/PythonLibCore-4.1.2.rst diff --git a/docs/PythonLibCore-4.1.2.rst b/docs/PythonLibCore-4.1.2.rst new file mode 100644 index 0000000..22595a9 --- /dev/null +++ b/docs/PythonLibCore-4.1.2.rst @@ -0,0 +1,57 @@ +========================= +Python Library Core 4.1.2 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.1.2 is +a new hotfix release with bug fixes for handling named only arguments +default values. + +All issues targeted for Python Library Core v4.1.2 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.1.2 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.1.2 was released on Saturday February 18, 2023. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.1.2 + + +.. contents:: + :depth: 2 + :local: + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + +Altogether 0 issues. View on the `issue tracker `__. + From 6756b809b185e3ccbb9984267f1aeca6e8fa238e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 00:59:19 +0200 Subject: [PATCH 089/148] Back to dev version --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 51004d6..17cadeb 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.1.2" +__version__ = "4.1.3.dev1" class PythonLibCoreException(Exception): From 2aa0d272d79f5d6852f0ad38887ad6954e2a89ee Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 01:08:10 +0200 Subject: [PATCH 090/148] More tests --- utest/test_get_keyword_types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 51c2f95..acc4cd9 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -202,3 +202,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} + 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") + assert types == {"arg2": int} From 6d1e1e715cbcfe8c80ab9a69dc4bc5324b0a992a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 01:45:08 +0200 Subject: [PATCH 091/148] Add type hints --- src/robotlibcore.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 17cadeb..ca8aa87 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -203,11 +203,11 @@ def _get_arguments(cls, function): return argument_specification @classmethod - def _get_arg_spec(cls, function: Callable): + def _get_arg_spec(cls, function: Callable) -> inspect.FullArgSpec: return inspect.getfullargspec(function) @classmethod - def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable): + 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 [] @@ -221,7 +221,7 @@ def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable): return formated_args @classmethod - def _drop_self_from_args(cls, function: Callable, arg_spec: inspect.FullArgSpec): + 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 @@ -271,9 +271,8 @@ def _get_typing_hints(cls, function): return hints @classmethod - def _args_as_list(cls, function, arg_spec): - function_args = [] - function_args.extend(cls._drop_self_from_args(function, arg_spec)) + 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 []) From bbd45c6dcf00bbfadfe71e251f96f84e3fed9854 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 18 Feb 2023 02:03:47 +0200 Subject: [PATCH 092/148] Mypy fixes --- requirements-dev.txt | 1 + src/robotlibcore.py | 18 +++++++++++++----- tasks.py | 2 ++ utest/test_robotlibcore.py | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 124691c..89d34b9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,5 @@ robotstatuschecker flake8 black isort +mypy robotframework-tidy \ No newline at end of file diff --git a/src/robotlibcore.py b/src/robotlibcore.py index ca8aa87..4ca647f 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -38,6 +38,10 @@ class PluginError(PythonLibCoreException): pass +class NoKeywordFound(PythonLibCoreException): + pass + + class HybridCore: def __init__(self, library_components): self.keywords = {} @@ -48,7 +52,7 @@ def __init__(self, library_components): self.__set_library_listeners(library_components) def add_library_components(self, library_components): - self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) + self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) # type: ignore for component in library_components: for name, func in self.__get_members(component): if callable(func) and hasattr(func, "robot_name"): @@ -123,6 +127,8 @@ def run_keyword(self, name, args, kwargs=None): def get_keyword_arguments(self, name): spec = self.keywords_spec.get(name) + if not spec: + raise NoKeywordFound(f"Could not find keyword: {name}") return spec.argument_specification def get_keyword_tags(self, name): @@ -132,6 +138,8 @@ def get_keyword_documentation(self, name): if name == "__intro__": return inspect.getdoc(self) or "" spec = self.keywords_spec.get(name) + if not spec: + raise NoKeywordFound(f"Could not find keyword: {name}") return spec.documentation def get_keyword_types(self, name): @@ -142,7 +150,7 @@ def get_keyword_types(self, name): def __get_keyword(self, keyword_name): if keyword_name == "__init__": - return self.__init__ + return self.__init__ # type: ignore if keyword_name.startswith("__") and keyword_name.endswith("__"): return None method = self.keywords.get(keyword_name) @@ -234,11 +242,11 @@ def _get_kwargs(cls, arg_spec: inspect.FullArgSpec) -> list: @classmethod def _get_named_only_args(cls, arg_spec: inspect.FullArgSpec) -> list: - rf_spec = [] + 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 [] + 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])) @@ -320,7 +328,7 @@ def get_plugin_keywords(self, plugins: List): return DynamicCore(plugins).get_keyword_names() def _string_to_modules(self, modules): - parsed_modules = [] + parsed_modules: list = [] if not modules: return parsed_modules for module in modules.split(","): diff --git a/tasks.py b/tasks.py index 140368d..68ff29e 100644 --- a/tasks.py +++ b/tasks.py @@ -146,6 +146,8 @@ def lint(ctx): command.insert(1, "--check") command.insert(1, "--diff") ctx.run(" ".join(command)) + print("Run mypy") + ctx.run("mypy --exclude .venv --show-error-codes --config-file mypy.ini src/") @task diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index cd14b86..365d526 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,6 +1,6 @@ import pytest -from robotlibcore import HybridCore +from robotlibcore import HybridCore, NoKeywordFound from HybridLibrary import HybridLibrary from DynamicLibrary import DynamicLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary @@ -117,7 +117,7 @@ def test_get_keyword_arguments(): assert args("kwargs_only") == ["**kws"] assert args("all_arguments") == ["mandatory", ("default", "value"), "*varargs", "**kwargs"] assert args("__init__") == [("arg", None)] - with pytest.raises(AttributeError): + with pytest.raises(NoKeywordFound): args("__foobar__") From 98adc8841dca0548447d3b5a2c2465b4a64228f7 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 10 Jul 2023 14:11:28 +0300 Subject: [PATCH 093/148] Add support for list in plugin import Fixes #112 --- src/robotlibcore.py | 20 ++++++++++++++------ utest/test_plugin_api.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 4ca647f..341a302 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,7 +21,7 @@ import inspect import os from dataclasses import dataclass -from typing import Any, Callable, List, Optional, get_type_hints +from typing import Any, Callable, List, Optional, Union, get_type_hints from robot.api.deco import keyword # noqa F401 from robot.errors import DataError @@ -304,11 +304,11 @@ def __init__(self, argument_specification=None, documentation=None, argument_typ class PluginParser: - def __init__(self, base_class: Optional[Any] = None, python_object: List[Any] = []): + def __init__(self, base_class: Optional[Any] = None, python_object=None): self._base_class = base_class - self._python_object = python_object + self._python_object = python_object if python_object else [] - def parse_plugins(self, plugins: str) -> List: + 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): @@ -327,11 +327,11 @@ def parse_plugins(self, plugins: str) -> List: def get_plugin_keywords(self, plugins: List): return DynamicCore(plugins).get_keyword_names() - def _string_to_modules(self, modules): + def _string_to_modules(self, modules: Union[str, List[str]]): parsed_modules: list = [] if not modules: return parsed_modules - for module in modules.split(","): + for module in self._modules_splitter(modules): module = module.strip() module_and_args = module.split(";") module_name = module_and_args.pop(0) @@ -346,3 +346,11 @@ def _string_to_modules(self, modules): module = Module(module=module_name, args=args, kw_args=kw_args) parsed_modules.append(module) 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 diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 9b6d488..0826fbf 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -48,6 +48,18 @@ def test_parse_plugins(plugin_parser): assert isinstance(plugins[1], my_plugin_test.TestClassWithBase) +def test_parse_plugins_as_list(plugin_parser): + plugins = plugin_parser.parse_plugins(["my_plugin_test.TestClass"]) + assert len(plugins) == 1 + assert isinstance(plugins[0], my_plugin_test.TestClass) + plugins = plugin_parser.parse_plugins( + ["my_plugin_test.TestClass", "my_plugin_test.TestClassWithBase"] + ) + assert len(plugins) == 2 + assert isinstance(plugins[0], my_plugin_test.TestClass) + assert isinstance(plugins[1], my_plugin_test.TestClassWithBase) + + def test_parse_plugins_with_base(): parser = PluginParser(my_plugin_test.LibraryBase) plugins = parser.parse_plugins("my_plugin_test.TestClassWithBase") From 855c41e3454f5230d8a2c02389a68f9c42611f87 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 10 Jul 2023 14:17:52 +0300 Subject: [PATCH 094/148] Drop support for Python 3.7 --- .flake8 | 1 + .github/workflows/CI.yml | 4 ++-- mypy.ini | 8 ++++++++ setup.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 mypy.ini diff --git a/.flake8 b/.flake8 index 0014793..91030e3 100644 --- a/.flake8 +++ b/.flake8 @@ -3,3 +3,4 @@ exclude = __pycache__, ignore = E203 max-line-length = 120 +max-complexity = 10 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3a5cb28..9e2c80d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,8 +8,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.11.1] - rf-version: [5.0.1, 6.0.2] + python-version: [3.8, 3.11.1] + rf-version: [5.0.1, 6.1.0] steps: - uses: actions/checkout@v3 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..468e56c --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +python_version = 3.8 +warn_unused_ignores = True +no_implicit_optional = True +check_untyped_defs = True + +[mypy-robot.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/setup.py b/setup.py index 17c6327..c8c73ff 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,10 @@ License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python :: 3 -Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 +Programming Language :: Python :: 3.11 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 2bb965a0858668c5875411d9fd0fdd2fd86f9cc3 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 10 Jul 2023 14:25:48 +0300 Subject: [PATCH 095/148] Release notes for 4.2.0 --- docs/PythonLibCore-4.2.0.rst | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/PythonLibCore-4.2.0.rst diff --git a/docs/PythonLibCore-4.2.0.rst b/docs/PythonLibCore-4.2.0.rst new file mode 100644 index 0000000..0014168 --- /dev/null +++ b/docs/PythonLibCore-4.2.0.rst @@ -0,0 +1,83 @@ +========================= +Python Library Core 4.2.0 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.2.0 is +a new release with supporting list when importing plugins and +dropping Python 3.7 support. + +All issues targeted for Python Library Core v4.2.0 can be found +from the `issue tracker`_. + +**REMOVE ``--pre`` from the next command with final releases.** +If you have pip_ installed, just run + +:: + + pip install --pre --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.2.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.2.0 was released on Monday July 10, 2023. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.2.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +Support list in plugin import (`#122`_) +--------------------------------------- +Now plugins can be imported as a list and not only a comma separated string. + +Backwards incompatible changes +============================== + +Drop Python 3.7 support (`#125`_) +--------------------------------- +Python 3.7 has been end of life for while and it is time to drop +support for Python 3.7. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#122`_ + - enhancement + - high + - Support list in plugin import + * - `#125`_ + - enhancement + - high + - Drop Python 3.7 support + +Altogether 2 issues. View on the `issue tracker `__. + +.. _#122: https://github.com/robotframework/PythonLibCore/issues/122 +.. _#125: https://github.com/robotframework/PythonLibCore/issues/125 From 8b6ebe4f8d29e9dc426569d268b0d6af0ebaa09f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Mon, 10 Jul 2023 14:27:33 +0300 Subject: [PATCH 096/148] Updated version to 4.2.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 341a302..d0860ac 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer # noqa F401 -__version__ = "4.1.3.dev1" +__version__ = "4.2.0" class PythonLibCoreException(Exception): From 37d5f3166dd502465c8b168ee94740c864bc0498 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 11 Jul 2023 00:15:03 +0300 Subject: [PATCH 097/148] Release notes for 4.2.0 --- docs/PythonLibCore-4.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PythonLibCore-4.2.0.rst b/docs/PythonLibCore-4.2.0.rst index 0014168..5005a00 100644 --- a/docs/PythonLibCore-4.2.0.rst +++ b/docs/PythonLibCore-4.2.0.rst @@ -56,7 +56,7 @@ Backwards incompatible changes Drop Python 3.7 support (`#125`_) --------------------------------- Python 3.7 has been end of life for while and it is time to drop -support for Python 3.7. +support for Python 3.7. Full list of fixes and enhancements =================================== From c428c558558ff8d93c56925f1beea9fee805c52a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 11 Jul 2023 00:17:04 +0300 Subject: [PATCH 098/148] Fix release notes --- docs/PythonLibCore-4.2.0.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/PythonLibCore-4.2.0.rst b/docs/PythonLibCore-4.2.0.rst index 5005a00..6530ae3 100644 --- a/docs/PythonLibCore-4.2.0.rst +++ b/docs/PythonLibCore-4.2.0.rst @@ -14,12 +14,11 @@ dropping Python 3.7 support. All issues targeted for Python Library Core v4.2.0 can be found from the `issue tracker`_. -**REMOVE ``--pre`` from the next command with final releases.** If you have pip_ installed, just run :: - pip install --pre --upgrade pip install robotframework-pythonlibcore + pip install --upgrade pip install robotframework-pythonlibcore to install the latest available release or use From 19d9f49999fa680eb02de5d9144dfe6a9a476b94 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 11 Jul 2023 01:21:49 +0300 Subject: [PATCH 099/148] Use ruff --- atest/DynamicLibrary.py | 5 +- atest/DynamicTypesAnnotationsLibrary.py | 37 +++++++++---- atest/DynamicTypesLibrary.py | 5 +- atest/ExtendExistingLibrary.py | 2 +- atest/HybridLibrary.py | 5 +- atest/ListenerCore.py | 12 +++-- atest/Python310Library.py | 3 +- atest/moc_library.py | 8 ++- atest/plugin_api/MyPluginBase.py | 2 +- atest/plugin_api/MyPluginWithPythonObjects.py | 2 +- atest/plugin_api/PluginLib.py | 2 +- atest/plugin_api/PluginWithBaseLib.py | 2 +- .../plugin_api/PluginWithPythonObjectsLib.py | 4 +- atest/run.py | 28 +++++++--- docs/example/02-hybrid/HybridLibrary.py | 3 +- docs/example/02-hybrid/calculator.py | 2 +- pyproject.toml | 53 ++++++++++++++++++ requirements-dev.txt | 12 +++-- setup.py | 4 +- src/robotlibcore.py | 54 ++++++++++++------- tasks.py | 39 ++++++++------ utest/helpers/my_plugin_test.py | 4 +- utest/run.py | 8 ++- utest/test_get_keyword_source.py | 3 +- utest/test_get_keyword_types.py | 6 +-- utest/test_keyword_builder.py | 6 +-- utest/test_plugin_api.py | 5 +- utest/test_robotlibcore.py | 5 +- 28 files changed, 216 insertions(+), 105 deletions(-) create mode 100644 pyproject.toml diff --git a/atest/DynamicLibrary.py b/atest/DynamicLibrary.py index bfb9bbb..38fc047 100644 --- a/atest/DynamicLibrary.py +++ b/atest/DynamicLibrary.py @@ -1,13 +1,12 @@ -from robotlibcore import DynamicCore, keyword - import librarycomponents +from robotlibcore import DynamicCore, keyword class DynamicLibrary(DynamicCore): """General library documentation.""" class_attribute = 'not keyword' - def __init__(self, arg=None): + def __init__(self, arg=None) -> None: """Library init doc.""" components = [librarycomponents, librarycomponents.Names(), diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 79732b1..fa47ed5 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -1,9 +1,8 @@ from enum import Enum from functools import wraps -from typing import List, Union, NewType, Optional, Tuple, Dict +from typing import Dict, List, NewType, Optional, Tuple, Union from robot.api import logger - from robotlibcore import DynamicCore, keyword UserId = NewType('UserId', int) @@ -28,14 +27,14 @@ def wrapper(*args, **kwargs): class CustomObject: - def __init__(self, x, y): + def __init__(self, x, y) -> None: self.x = x self.y = y class DynamicTypesAnnotationsLibrary(DynamicCore): - def __init__(self, arg: str): + def __init__(self, arg: str) -> None: DynamicCore.__init__(self, []) self.instance_attribute = 'not keyword' self.arg = arg @@ -74,7 +73,11 @@ def keyword_with_webdriver(self, arg: CustomObject): return arg @keyword - def keyword_default_and_annotation(self: 'DynamicTypesAnnotationsLibrary', arg1: int, arg2: Union[bool, str] = False) -> str: + def keyword_default_and_annotation( + self: 'DynamicTypesAnnotationsLibrary', + arg1: int, + arg2: Union[bool, str] = False + ) -> str: return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) @keyword(types={'arg': str}) @@ -90,7 +93,10 @@ def keyword_robot_types_and_bool_hint(self, arg1, arg2: bool): return '{}: {}, {}: {}'.format(arg1, type(arg1), arg2, type(arg2)) @keyword - def keyword_exception_annotations(self: 'DynamicTypesAnnotationsLibrary', arg: 'NotHere'): + def keyword_exception_annotations( + self: 'DynamicTypesAnnotationsLibrary', + arg: 'NotHere' # noqa F821 + ): return arg @keyword @@ -124,7 +130,15 @@ def keyword_mandatory_and_keyword_only_arguments(self, arg: int, *vararg, some: return f'{arg}, {vararg}, {some}' @keyword - def keyword_all_args(self: 'DynamicTypesAnnotationsLibrary', mandatory, positional=1, *varargs, other, value=False, **kwargs): + def keyword_all_args( + self: 'DynamicTypesAnnotationsLibrary', + mandatory, + positional=1, + *varargs, + other, + value=False, + **kwargs + ): return True @keyword @@ -132,8 +146,13 @@ def keyword_self_and_types(self: 'DynamicTypesAnnotationsLibrary', mandatory: st return True @keyword - def keyword_self_and_keyword_only_types(x: 'DynamicTypesAnnotationsLibrary', mandatory, *varargs: int, other: bool, - **kwargs: int): + def keyword_self_and_keyword_only_types( + x: 'DynamicTypesAnnotationsLibrary', # noqa: N805 + mandatory, + *varargs: int, + other: bool, + **kwargs: int + ): return (f'{mandatory}: {type(mandatory)}, {varargs}: {type(varargs)}, ' f'{other}: {type(other)}, {kwargs}: {type(kwargs)}') diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index b196fbd..f206a61 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -2,7 +2,6 @@ import sys from robot import version as rf_version - from robotlibcore import DynamicCore, keyword @@ -19,7 +18,7 @@ def wrapper(*args, **kwargs): class DynamicTypesLibrary(DynamicCore): - def __init__(self, arg=False): + def __init__(self, arg=False) -> None: DynamicCore.__init__(self, []) self.instance_attribute = 'not keyword' self.arg = arg @@ -84,4 +83,4 @@ def keyword_booleans(self, arg1=True, arg2=False): @keyword def is_rf_401(self): - return "4.0." in rf_version.VERSION \ No newline at end of file + return "4.0." in rf_version.VERSION diff --git a/atest/ExtendExistingLibrary.py b/atest/ExtendExistingLibrary.py index a1abcff..06fad7b 100644 --- a/atest/ExtendExistingLibrary.py +++ b/atest/ExtendExistingLibrary.py @@ -3,7 +3,7 @@ class ExtendExistingLibrary(HybridLibrary): - def __init__(self): + def __init__(self) -> None: HybridLibrary.__init__(self) self.add_library_components([ExtendingComponent()]) diff --git a/atest/HybridLibrary.py b/atest/HybridLibrary.py index 58abf4d..7276a0c 100644 --- a/atest/HybridLibrary.py +++ b/atest/HybridLibrary.py @@ -1,13 +1,12 @@ -from robotlibcore import HybridCore, keyword - import librarycomponents +from robotlibcore import HybridCore, keyword class HybridLibrary(HybridCore): """General library documentation.""" class_attribute = 'not keyword' - def __init__(self): + def __init__(self) -> None: components = [librarycomponents, librarycomponents.Names(), librarycomponents.Arguments(), diff --git a/atest/ListenerCore.py b/atest/ListenerCore.py index 899810f..b3ca4ee 100644 --- a/atest/ListenerCore.py +++ b/atest/ListenerCore.py @@ -5,7 +5,7 @@ class ListenerCore(DynamicCore): ROBOT_LIBRARY_SCOPE = 'GLOBAL' - def __init__(self): + def __init__(self) -> None: self.keyword_name = None self.keyword_args = {} self.ROBOT_LISTENER_API_VERSION = 2 @@ -16,7 +16,9 @@ def __init__(self): @keyword def listener_core(self, arg: str): - assert arg == self.keyword_args.get("args", [None])[0], "First argument should be detected by listener, but was not." + assert arg == self.keyword_args.get( + "args", [None] + )[0], "First argument should be detected by listener, but was not." def start_keyword(self, name, args): self.keyword_name = name @@ -25,7 +27,7 @@ def start_keyword(self, name, args): class FirstComponent: - def __init__(self): + def __init__(self) -> None: self.ROBOT_LISTENER_API_VERSION = 2 self.suite_name = '' @@ -39,7 +41,7 @@ def first_component(self, arg: str): class SecondComponent: - def __init__(self): + def __init__(self) -> None: self.listener = ExternalListener() @keyword @@ -51,7 +53,7 @@ class ExternalListener: ROBOT_LISTENER_API_VERSION = 3 - def __init__(self): + def __init__(self) -> None: self.test = None def start_test(self, test, _): diff --git a/atest/Python310Library.py b/atest/Python310Library.py index 5d8aeb3..f838dd0 100644 --- a/atest/Python310Library.py +++ b/atest/Python310Library.py @@ -1,11 +1,10 @@ from robot.api import logger - from robotlibcore import DynamicCore, keyword class Python310Library(DynamicCore): - def __init__(self): + def __init__(self) -> None: DynamicCore.__init__(self, []) @keyword diff --git a/atest/moc_library.py b/atest/moc_library.py index 88377d1..4d99f2c 100644 --- a/atest/moc_library.py +++ b/atest/moc_library.py @@ -39,7 +39,13 @@ def named_only_with_defaults(self, *varargs, key1, key2, key3='default1', key4=T def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool: pass - def self_and_keyword_only_types(x: 'MockLibrary', mandatory, *varargs: int, other: bool, **kwargs: int): + def self_and_keyword_only_types( + x: 'MockLibrary', # noqa: N805 + mandatory, + *varargs: int, + other: bool, + **kwargs: int + ): pass def optional_none(self, xxx, arg1: Optional[str] = None, arg2: Optional[str] = None, arg3=False): diff --git a/atest/plugin_api/MyPluginBase.py b/atest/plugin_api/MyPluginBase.py index f1b720d..2d81f9e 100644 --- a/atest/plugin_api/MyPluginBase.py +++ b/atest/plugin_api/MyPluginBase.py @@ -5,7 +5,7 @@ class MyPluginBase(BaseClass): - def __init__(self, arg): + def __init__(self, arg) -> None: self.arg = int(arg) @keyword diff --git a/atest/plugin_api/MyPluginWithPythonObjects.py b/atest/plugin_api/MyPluginWithPythonObjects.py index af3147c..1340639 100644 --- a/atest/plugin_api/MyPluginWithPythonObjects.py +++ b/atest/plugin_api/MyPluginWithPythonObjects.py @@ -5,7 +5,7 @@ class MyPluginWithPythonObjects(BaseWithPython): - def __init__(self, py1, py2, rf1, rf2): + def __init__(self, py1, py2, rf1, rf2) -> None: self.rf1 = int(rf1) self.rf2 = int(rf2) super().__init__(py1, py2) diff --git a/atest/plugin_api/PluginLib.py b/atest/plugin_api/PluginLib.py index 03555c9..e87dcb2 100644 --- a/atest/plugin_api/PluginLib.py +++ b/atest/plugin_api/PluginLib.py @@ -5,7 +5,7 @@ class PluginLib(DynamicCore): - def __init__(self, plugins): + def __init__(self, plugins) -> None: plugin_parser = PluginParser() parsed_plugins = plugin_parser.parse_plugins(plugins) self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) diff --git a/atest/plugin_api/PluginWithBaseLib.py b/atest/plugin_api/PluginWithBaseLib.py index d090cdd..8198d4e 100644 --- a/atest/plugin_api/PluginWithBaseLib.py +++ b/atest/plugin_api/PluginWithBaseLib.py @@ -10,7 +10,7 @@ def method(self): class PluginWithBaseLib(DynamicCore): - def __init__(self, plugins): + def __init__(self, plugins) -> None: plugin_parser = PluginParser(BaseClass) parsed_plugins = plugin_parser.parse_plugins(plugins) self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) diff --git a/atest/plugin_api/PluginWithPythonObjectsLib.py b/atest/plugin_api/PluginWithPythonObjectsLib.py index 2b76e3c..69d3c35 100644 --- a/atest/plugin_api/PluginWithPythonObjectsLib.py +++ b/atest/plugin_api/PluginWithPythonObjectsLib.py @@ -4,14 +4,14 @@ class BaseWithPython: - def __init__(self, py1, py2): + def __init__(self, py1, py2) -> None: self.py1 = py1 self.py2 = py2 class PluginWithPythonObjectsLib(DynamicCore): - def __init__(self, plugins): + def __init__(self, plugins) -> None: plugin_parser = PluginParser(BaseWithPython, [8, 9]) parsed_plugins = plugin_parser.parse_plugins(plugins) self._plugin_keywords = plugin_parser.get_plugin_keywords(plugins) diff --git a/atest/run.py b/atest/run.py index 18577da..8491ca2 100755 --- a/atest/run.py +++ b/atest/run.py @@ -6,7 +6,7 @@ from pathlib import Path from robot import rebot, run -from robot.version import VERSION as rf_version +from robot.version import VERSION as RF_VERSION from robotstatuschecker import process_output library_variants = ["Hybrid", "Dynamic", "ExtendExisting"] @@ -20,7 +20,10 @@ sys.path.insert(0, join(curdir, "..", "src")) python_version = platform.python_version() for variant in library_variants: - output = join(outdir, "lib-{}-python-{}-robot-{}.xml".format(variant, python_version, rf_version)) + output = join( + outdir, + "lib-{}-python-{}-robot-{}.xml".format(variant, python_version, RF_VERSION), + ) rc = run( tests, name=variant, @@ -33,13 +36,24 @@ if rc > 250: sys.exit(rc) process_output(output, verbose=False) -output = join(outdir, "lib-DynamicTypesLibrary-python-{}-robot-{}.xml".format(python_version, rf_version)) +output = join( + outdir, + "lib-DynamicTypesLibrary-python-{}-robot-{}.xml".format(python_version, RF_VERSION), +) exclude = "py310" if sys.version_info < (3, 10) else "" -rc = run(tests_types, name="Types", output=output, report=None, log=None, loglevel="debug", exclude=exclude) +rc = run( + tests_types, + name="Types", + output=output, + report=None, + log=None, + loglevel="debug", + exclude=exclude, +) if rc > 250: sys.exit(rc) process_output(output, verbose=False) -output = join(outdir, "lib-PluginApi-python-{}-robot-{}.xml".format(python_version, rf_version)) +output = join(outdir, "lib-PluginApi-python-{}-robot-{}.xml".format(python_version, RF_VERSION)) rc = run(plugin_api, name="Plugin", output=output, report=None, log=None, loglevel="debug") if rc > 250: sys.exit(rc) @@ -54,8 +68,8 @@ **dict( name="Acceptance Tests", outputdir=outdir, - log="log-python-{}-robot-{}.html".format(python_version, rf_version), - report="report-python-{}-robot-{}.html".format(python_version, rf_version), + log="log-python-{}-robot-{}.html".format(python_version, RF_VERSION), + report="report-python-{}-robot-{}.html".format(python_version, RF_VERSION), ), ) if rc == 0: diff --git a/docs/example/02-hybrid/HybridLibrary.py b/docs/example/02-hybrid/HybridLibrary.py index c402ae1..218c56a 100644 --- a/docs/example/02-hybrid/HybridLibrary.py +++ b/docs/example/02-hybrid/HybridLibrary.py @@ -1,4 +1,3 @@ -from robot.api import logger from calculator import Calculator from stringtools import StringTools @@ -6,7 +5,7 @@ class HybridLibrary(Calculator, StringTools, Waiter): - def __init__(self, separator: str = ";"): + def __init__(self, separator: str = ";") -> None: self.separator = separator def get_keyword_names(self): diff --git a/docs/example/02-hybrid/calculator.py b/docs/example/02-hybrid/calculator.py index 2dc2e54..5412b7e 100644 --- a/docs/example/02-hybrid/calculator.py +++ b/docs/example/02-hybrid/calculator.py @@ -6,5 +6,5 @@ class Calculator: @keyword def sum(self, value1: int, value2: int) -> int: """Do other thing.""" - logger.info(f"Calculating hard.") + logger.info("Calculating hard.") return value1 + value2 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6a54d74 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[tool.black] +target-version = ['py38'] +line-length = 120 + +[tool.ruff] +line-length = 120 +fixable = ["ALL"] +target-version = "py38" +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "YTT", + "S", + "BLE", + "FBT", + "B", + "A", + "COM", + "CPY", + "C4", + "T10", + "EM", + "EXE", + "FA", + "ISC", + "ICN", + "G", + "PIE", + "PYI", + "Q", + "RSE", + "RET", + "SLF", + "SIM", + "TCH", + "INT", + "ARG", + "PTH", + "ERA", + "PL", + "PERF", + "RUF" +] + +[tool.ruff.mccabe] +max-complexity = 9 + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" diff --git a/requirements-dev.txt b/requirements-dev.txt index 89d34b9..97723dc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,8 +2,10 @@ pytest pytest-cov pytest-mockito robotstatuschecker -flake8 -black -isort -mypy -robotframework-tidy \ No newline at end of file +black >= 23.3.0 +ruff >= 0.0.277 +robotframework-tidy +invoke >= 1.7.3 +rellu >= 0.7 +twine +wheel diff --git a/setup.py b/setup.py index c8c73ff..1e0d34e 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ #!/usr/bin/env python import re -from os.path import abspath, join, dirname -from setuptools import find_packages, setup +from os.path import abspath, dirname, join +from setuptools import find_packages, setup CURDIR = dirname(abspath(__file__)) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index d0860ac..457aa05 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -23,14 +23,14 @@ from dataclasses import dataclass from typing import Any, Callable, List, Optional, Union, get_type_hints -from robot.api.deco import keyword # noqa F401 +from robot.api.deco import keyword # noqa: F401 from robot.errors import DataError -from robot.utils import Importer # noqa F401 +from robot.utils import Importer __version__ = "4.2.0" -class PythonLibCoreException(Exception): +class PythonLibCoreException(Exception): # noqa: N818 pass @@ -43,7 +43,7 @@ class NoKeywordFound(PythonLibCoreException): class HybridCore: - def __init__(self, library_components): + def __init__(self, library_components: List) -> None: self.keywords = {} self.keywords_spec = {} self.attributes = {} @@ -51,7 +51,7 @@ def __init__(self, library_components): self.add_library_components([self]) self.__set_library_listeners(library_components) - def add_library_components(self, library_components): + def add_library_components(self, library_components: List): self.keywords_spec["__init__"] = KeywordBuilder.build(self.__init__) # type: ignore for component in library_components: for name, func in self.__get_members(component): @@ -84,13 +84,17 @@ 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( - "Libraries must be modules or instances, got " "class {!r} instead.".format(component.__name__) + 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( - "Libraries must be modules or new-style class " - "instances, got old-style class {!r} instead.".format(component.__class__.__name__) + msg, ) return self.__get_members_from_instance(component) @@ -104,7 +108,10 @@ def __get_members_from_instance(self, instance): def __getattr__(self, name): if name in self.attributes: return self.attributes[name] - raise AttributeError("{!r} object has no attribute {!r}".format(type(self).__name__, name)) + msg = "{!r} object has no attribute {!r}".format(type(self).__name__, name) + raise AttributeError( + msg, + ) def __dir__(self): my_attrs = super().__dir__() @@ -128,7 +135,8 @@ def run_keyword(self, name, args, kwargs=None): def get_keyword_arguments(self, name): spec = self.keywords_spec.get(name) if not spec: - raise NoKeywordFound(f"Could not find keyword: {name}") + msg = f"Could not find keyword: {name}" + raise NoKeywordFound(msg) return spec.argument_specification def get_keyword_tags(self, name): @@ -139,7 +147,8 @@ def get_keyword_documentation(self, name): return inspect.getdoc(self) or "" spec = self.keywords_spec.get(name) if not spec: - raise NoKeywordFound(f"Could not find keyword: {name}") + msg = f"Could not find keyword: {name}" + raise NoKeywordFound(msg) return spec.documentation def get_keyword_types(self, name): @@ -229,7 +238,11 @@ def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable) -> list: return formated_args @classmethod - def _drop_self_from_args(cls, function: Callable, arg_spec: inspect.FullArgSpec) -> list: + 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 @@ -268,7 +281,7 @@ def _get_typing_hints(cls, function): function = cls.unwrap(function) try: hints = get_type_hints(function) - except Exception: + except Exception: # noqa: BLE001 hints = function.__annotations__ arg_spec = cls._get_arg_spec(function) all_args = cls._args_as_list(function, arg_spec) @@ -297,14 +310,19 @@ def _get_defaults(cls, arg_spec): class KeywordSpecification: - def __init__(self, argument_specification=None, documentation=None, argument_types=None): + 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): + 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 [] @@ -332,8 +350,7 @@ def _string_to_modules(self, modules: Union[str, List[str]]): if not modules: return parsed_modules for module in self._modules_splitter(modules): - module = module.strip() - module_and_args = module.split(";") + module_and_args = module.strip().split(";") module_name = module_and_args.pop(0) kw_args = {} args = [] @@ -343,8 +360,7 @@ def _string_to_modules(self, modules: Union[str, List[str]]): kw_args[key] = value else: args.append(argument) - module = Module(module=module_name, args=args, kw_args=kw_args) - parsed_modules.append(module) + 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]]): diff --git a/tasks.py b/tasks.py index 68ff29e..b2b07d3 100644 --- a/tasks.py +++ b/tasks.py @@ -6,7 +6,7 @@ from rellu import ReleaseNotesGenerator, Version, initialize_labels from rellu.tasks import clean # noqa -assert Path.cwd() == Path(__file__).parent +assert Path.cwd() == Path(__file__).parent # noqa: S101 REPOSITORY = "robotframework/PythonLibCore" @@ -50,7 +50,7 @@ @task -def set_version(ctx, version): +def set_version(ctx, version): # noqa: ARG001 """Set project version in ``src/robotlibcore.py`` file. Args: @@ -60,7 +60,7 @@ def set_version(ctx, version): - Final version like 3.0 or 3.1.2. - Alpha, beta or release candidate with ``a``, ``b`` or ``rc`` postfix, respectively, and an incremented number like 3.0a1 or 3.0.1rc1. - - Development version with ``.dev`` postix and an incremented number like + - Development version with ``.dev`` postfix and an incremented number like 3.0.dev1 or 3.1a1.dev2. When the given version is ``dev``, the existing version number is updated @@ -73,13 +73,13 @@ def set_version(ctx, version): @task -def print_version(ctx): +def print_version(ctx): # noqa: ARG001 """Print the current project version.""" print(Version(path=VERSION_PATH)) @task -def release_notes(ctx, version=None, username=None, password=None, write=False): +def release_notes(ctx, version=None, username=None, password=None, write=False): # noqa: FBT002, ARG001 """Generates release notes based on issues in the issue tracker. Args: @@ -88,7 +88,7 @@ def release_notes(ctx, version=None, username=None, password=None, write=False): username: GitHub username. password: GitHub password. write: When set to True, write release notes to a file overwriting - possible existing file. Otherwise just print them to the + possible existing file. Otherwise, just print them to the terminal. Username and password can also be specified using ``GITHUB_USERNAME`` and @@ -98,12 +98,16 @@ def release_notes(ctx, version=None, username=None, password=None, write=False): """ version = Version(version, VERSION_PATH) file = RELEASE_NOTES_PATH if write else sys.stdout - generator = ReleaseNotesGenerator(REPOSITORY, RELEASE_NOTES_TITLE, RELEASE_NOTES_INTRO) + generator = ReleaseNotesGenerator( + REPOSITORY, + RELEASE_NOTES_TITLE, + RELEASE_NOTES_INTRO, + ) generator.generate(version, username, password, file) @task -def init_labels(ctx, username=None, password=None): +def init_labels(ctx, username=None, password=None): # noqa: ARG001 """Initialize project by setting labels in the issue tracker. Args: @@ -121,14 +125,17 @@ def init_labels(ctx, username=None, password=None): @task def lint(ctx): - print("Run flake8") - ctx.run("flake8 --config .flake8 src/ tasks.py utest/run.py atest/run.py") + in_ci = os.getenv("GITHUB_WORKFLOW") + print("Run ruff") + ruff_cmd = ["ruff", "check"] + if not in_ci: + ruff_cmd.append("--fix") + ruff_cmd.append("./src") + ruff_cmd.append("./tasks.py") + ctx.run(" ".join(ruff_cmd)) print("Run black") - ctx.run("black --target-version py37 --line-length 120 src/ tasks.py utest/run.py atest/run.py") - print("Run isort") - ctx.run("isort src/ tasks.py utest/run.py atest/run.py") + ctx.run("black src/ tasks.py utest/run.py atest/run.py") print("Run tidy") - in_ci = os.getenv("GITHUB_WORKFLOW") print(f"Lint Robot files {'in ci' if in_ci else ''}") command = [ "robotidy", @@ -146,8 +153,6 @@ def lint(ctx): command.insert(1, "--check") command.insert(1, "--diff") ctx.run(" ".join(command)) - print("Run mypy") - ctx.run("mypy --exclude .venv --show-error-codes --config-file mypy.ini src/") @task @@ -161,5 +166,5 @@ def utest(ctx): @task(utest, atest) -def test(ctx): +def test(ctx): # noqa: ARG001 pass diff --git a/utest/helpers/my_plugin_test.py b/utest/helpers/my_plugin_test.py index 8f1d19d..e684758 100644 --- a/utest/helpers/my_plugin_test.py +++ b/utest/helpers/my_plugin_test.py @@ -13,7 +13,7 @@ def not_keyword(self): class LibraryBase: - def __init__(self): + def __init__(self) -> None: self.x = 1 def base(self): @@ -32,7 +32,7 @@ def normal_method(self): class TestPluginWithPythonArgs(LibraryBase): - def __init__(self, python_class, rf_arg): + def __init__(self, python_class, rf_arg) -> None: self.python_class = python_class self.rf_arg = rf_arg super().__init__() diff --git a/utest/run.py b/utest/run.py index 08da4a4..1da6cd3 100755 --- a/utest/run.py +++ b/utest/run.py @@ -5,12 +5,16 @@ from os.path import abspath, dirname, join import pytest -from robot.version import VERSION as rf_version +from robot.version import VERSION as RF_VERSION curdir = dirname(abspath(__file__)) atest_dir = join(curdir, "..", "atest") python_version = platform.python_version() -xunit_report = join(atest_dir, "results", "xunit-python-{}-robot{}.xml".format(python_version, rf_version)) +xunit_report = join( + atest_dir, + "results", + "xunit-python-{}-robot{}.xml".format(python_version, RF_VERSION), +) src = join(curdir, "..", "src") sys.path.insert(0, src) sys.path.insert(0, atest_dir) diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 43c8ad9..7acaa06 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -2,10 +2,9 @@ from os import path import pytest -from mockito.matchers import Any - from DynamicLibrary import DynamicLibrary from DynamicTypesLibrary import DynamicTypesLibrary +from mockito.matchers import Any @pytest.fixture(scope="module") diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index acc4cd9..7a0dba2 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -1,10 +1,8 @@ -import pytest import typing - from typing import List, Union -from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary -from DynamicTypesAnnotationsLibrary import CustomObject +import pytest +from DynamicTypesAnnotationsLibrary import CustomObject, DynamicTypesAnnotationsLibrary from DynamicTypesLibrary import DynamicTypesLibrary diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 42ccad1..54ad082 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -1,9 +1,9 @@ -import pytest import typing -from robotlibcore import KeywordBuilder -from moc_library import MockLibrary +import pytest from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary +from moc_library import MockLibrary +from robotlibcore import KeywordBuilder @pytest.fixture diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 0826fbf..b51dce2 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,7 +1,6 @@ -import pytest - -from robotlibcore import Module, PluginParser, PluginError import my_plugin_test +import pytest +from robotlibcore import Module, PluginError, PluginParser @pytest.fixture(scope="module") diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 365d526..cc4a779 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,9 +1,8 @@ import pytest - -from robotlibcore import HybridCore, NoKeywordFound -from HybridLibrary import HybridLibrary from DynamicLibrary import DynamicLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary +from HybridLibrary import HybridLibrary +from robotlibcore import HybridCore, NoKeywordFound @pytest.fixture(scope="module") From 7c5439327725e62b2a446c06a942344aa4f7e3f1 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 11 Jul 2023 01:25:46 +0300 Subject: [PATCH 100/148] Remove flake8 and mypy config files --- .flake8 | 6 ------ .github/workflows/CI.yml | 6 +++--- mypy.ini | 8 -------- src/robotlibcore.py | 2 +- utest/test_get_keyword_source.py | 8 ++++---- 5 files changed, 8 insertions(+), 22 deletions(-) delete mode 100644 .flake8 delete mode 100644 mypy.ini diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 91030e3..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -exclude = - __pycache__, -ignore = E203 -max-line-length = 120 -max-complexity = 10 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9e2c80d..d20262a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,12 +24,12 @@ jobs: - name: Install RF ${{ matrix.rf-version }} run: | pip install -U --pre robotframework==${{ matrix.rf-version }} - - name: Run flake8 + - name: Run ruff run: | - flake8 --config .flake8 src/ + ruff check ./src tasks.py - name: Run balck run: | - black --target-version py36 --line-length 120 --check src/ + black --config pyproject.toml --check src/ - name: Run unit tests run: | python utest/run.py diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 468e56c..0000000 --- a/mypy.ini +++ /dev/null @@ -1,8 +0,0 @@ -[mypy] -python_version = 3.8 -warn_unused_ignores = True -no_implicit_optional = True -check_untyped_defs = True - -[mypy-robot.*] -ignore_missing_imports = True \ No newline at end of file diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 457aa05..c87c5c8 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -84,7 +84,7 @@ 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." + msg = f"Libraries must be modules or instances, got class '{component.__name__}' instead." raise TypeError( msg, ) diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 7acaa06..11828e4 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -39,7 +39,7 @@ def lib_path_types(cur_dir): def test_location_in_main(lib, lib_path): source = lib.get_keyword_source("keyword_in_main") - assert source == "%s:20" % lib_path + assert source == f"{lib_path}:19" def test_location_in_class(lib, lib_path_components): @@ -49,7 +49,7 @@ def test_location_in_class(lib, lib_path_components): def test_decorator_wrapper(lib_types, lib_path_types): source = lib_types.get_keyword_source("keyword_wrapped") - assert source == "%s:74" % lib_path_types + assert source == f"{lib_path_types}:73" def test_location_in_class_custom_keyword_name(lib, lib_path_components): @@ -66,7 +66,7 @@ def test_no_line_number(lib, lib_path, when): def test_no_path(lib, when): when(lib)._DynamicCore__get_keyword_path(Any()).thenReturn(None) source = lib.get_keyword_source("keyword_in_main") - assert source == ":20" + assert source == ":19" def test_no_path_and_no_line_number(lib, when): @@ -78,7 +78,7 @@ def test_no_path_and_no_line_number(lib, when): def test_def_in_decorator(lib_types, lib_path_types): source = lib_types.get_keyword_source("keyword_with_def_deco") - assert source == "%s:68" % lib_path_types + assert source == f"{lib_path_types}:67" def test_error_in_getfile(lib, when): From 023a9e6a43d3a5fbdce3180190f2f0b4aa44f32c Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 11 Jul 2023 01:39:19 +0300 Subject: [PATCH 101/148] Run tidy in CI --- .github/workflows/CI.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d20262a..bbcee38 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -27,6 +27,9 @@ jobs: - name: Run ruff run: | ruff check ./src tasks.py + - name: Run tidy + run: | + robotidy --transform RenameKeywords --transform RenameTestCases -c RenameTestCases:capitalize_each_word=True --lineseparator unix atest/ - name: Run balck run: | black --config pyproject.toml --check src/ From 5a8cec1e510d14399030cf65381eed9603583df0 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 26 Aug 2023 01:52:21 +0300 Subject: [PATCH 102/148] Fix Python version to 3.8+ in setup.py --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6a54d74..6fa99a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ select = [ "T10", "EM", "EXE", - "FA", + # "FA", "ISC", "ICN", "G", diff --git a/setup.py b/setup.py index 1e0d34e..d0fb2d4 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ keywords = 'robotframework testing testautomation library development', platforms = 'any', classifiers = CLASSIFIERS, - python_requires = '>=3.7, <4', + python_requires = '>=3.8, <4', package_dir = {'': 'src'}, packages = find_packages('src'), py_modules = ['robotlibcore'], From ffcea3d9139709d4a3e84970f00e60f1be7f7b8e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 26 Aug 2023 02:00:12 +0300 Subject: [PATCH 103/148] Run CI by cron, once a week --- .github/workflows/CI.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bbcee38..5dfa9d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,6 +1,12 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: 33 6 * * 3 jobs: build: From 4139558f010265dbbac8cb316bbcea6885abbcd3 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 26 Aug 2023 02:04:20 +0300 Subject: [PATCH 104/148] Update deps --- requirements-build.txt | 1 - requirements-dev.txt | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/requirements-build.txt b/requirements-build.txt index ddb9830..9c49708 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,5 +1,4 @@ # Requirements needed when generating releases. See BUILD.rst for details. -invoke >= 1.7.3 rellu >= 0.7 twine wheel diff --git a/requirements-dev.txt b/requirements-dev.txt index 97723dc..7438901 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,10 +2,9 @@ pytest pytest-cov pytest-mockito robotstatuschecker -black >= 23.3.0 -ruff >= 0.0.277 +black >= 23.7.0 +ruff >= 0.0.286 robotframework-tidy -invoke >= 1.7.3 -rellu >= 0.7 +invoke >= 2.2.0 twine wheel From c57dd2e7c405445f969e8b6849ab87129021fad6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 07:26:58 +0000 Subject: [PATCH 105/148] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5dfa9d7..73e58c4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,7 +18,7 @@ jobs: rf-version: [5.0.1, 6.1.0] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} with Robot Framework ${{ matrix.rf-version }} uses: actions/setup-python@v4 with: From c57fc10441b07eb49c3c60137f0b554634e17762 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 15 Nov 2023 22:40:11 +0200 Subject: [PATCH 106/148] Merge dev and build reg files --- requirements-build.txt | 7 ------- requirements-dev.txt | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 requirements-build.txt diff --git a/requirements-build.txt b/requirements-build.txt deleted file mode 100644 index 9c49708..0000000 --- a/requirements-build.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Requirements needed when generating releases. See BUILD.rst for details. -rellu >= 0.7 -twine -wheel - -# Include other dev dependencies from requirements-dev.txt. --r requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt index 7438901..2f4358f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,3 +8,6 @@ robotframework-tidy invoke >= 2.2.0 twine wheel +rellu >= 0.7 +twine +wheel From a42bd4f22a2099bd5ae2f694b7036a770e6b0a25 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 15 Nov 2023 22:43:48 +0200 Subject: [PATCH 107/148] Ignore more lint outputs --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4f4a815..b74338b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ htmlcov/ .coverage .coverage.* .cache +.ruff_cache +.mypy_cache +.pytest_cache nosetests.xml coverage.xml *,cover From 4bf4f90b1d38ea73eeaae7029d163aa493d11ef0 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Nov 2023 20:42:31 +0200 Subject: [PATCH 108/148] Improve testing for TypedDict --- atest/lib_future_annotation.py | 22 ++++++++++++++++++++++ requirements-dev.txt | 1 + src/robotlibcore.py | 18 +++++++++++++----- utest/test_get_keyword_types.py | 12 ++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 atest/lib_future_annotation.py diff --git a/atest/lib_future_annotation.py b/atest/lib_future_annotation.py new file mode 100644 index 0000000..1fd6576 --- /dev/null +++ b/atest/lib_future_annotation.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing_extensions import TypedDict + +from robotlibcore import DynamicCore, keyword + + +class Location(TypedDict): + longitude: float + latitude: float + + +class lib_future_annotation(DynamicCore): + + def __init__(self) -> None: + DynamicCore.__init__(self, []) + + @keyword + def future_annotations(self, arg: Location): + longitude = arg["longitude"] + latitude = arg["latitude"] + return f'{longitude} type({type(longitude)}), {latitude} {type(latitude)}' diff --git a/requirements-dev.txt b/requirements-dev.txt index 2f4358f..aff0549 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ wheel rellu >= 0.7 twine wheel +typing-extensions >= 4.5.0 diff --git a/src/robotlibcore.py b/src/robotlibcore.py index c87c5c8..0d95b29 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,7 +21,7 @@ import inspect import os from dataclasses import dataclass -from typing import Any, Callable, List, Optional, Union, get_type_hints +from typing import Any, Callable, List, Optional, Union, get_type_hints, ForwardRef from robot.api.deco import keyword # noqa: F401 from robot.errors import DataError @@ -223,6 +223,17 @@ def _get_arguments(cls, function): 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__ + for arg, hint in hints.items(): + if isinstance(hint, ForwardRef): + hint = hint.__forward_arg__ + return hints + @classmethod def _get_args(cls, arg_spec: inspect.FullArgSpec, function: Callable) -> list: args = cls._drop_self_from_args(function, arg_spec) @@ -279,10 +290,7 @@ def _get_types(cls, function): @classmethod def _get_typing_hints(cls, function): function = cls.unwrap(function) - try: - hints = get_type_hints(function) - except Exception: # noqa: BLE001 - hints = function.__annotations__ + 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): diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 7a0dba2..7e407f3 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -4,6 +4,7 @@ import pytest from DynamicTypesAnnotationsLibrary import CustomObject, DynamicTypesAnnotationsLibrary from DynamicTypesLibrary import DynamicTypesLibrary +from lib_future_annotation import lib_future_annotation, Location @pytest.fixture(scope="module") @@ -16,6 +17,11 @@ def lib_types(): return DynamicTypesAnnotationsLibrary("aaa") +@pytest.fixture(scope="module") +def lib_annotation(): + return lib_future_annotation() + + def test_using_keyword_types(lib): types = lib.get_keyword_types("keyword_with_types") assert types == {"arg1": str} @@ -204,3 +210,9 @@ def test_kw_with_many_named_arguments_with_default(lib_types: DynamicTypesAnnota assert types == {"arg1": int, "arg2": str} types = lib_types.get_keyword_types("kw_with_positional_and_named_arguments") assert types == {"arg2": int} + + +def test_lib_annotations(lib_annotation: lib_future_annotation): + types = lib_annotation.get_keyword_types("future_annotations") + expected = {"arg1": Location} + assert types == expected From 65147195457dd4fd48c03d8a406a81f47ef8afbe Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Nov 2023 20:44:55 +0200 Subject: [PATCH 109/148] remove debug code --- src/robotlibcore.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 0d95b29..0c4d9a7 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -229,9 +229,6 @@ def _get_type_hint(cls, function: Callable): hints = get_type_hints(function) except Exception: # noqa: BLE001 hints = function.__annotations__ - for arg, hint in hints.items(): - if isinstance(hint, ForwardRef): - hint = hint.__forward_arg__ return hints @classmethod From ba28e4ebab77e08b590c9852b9c971adfc3f16f3 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Nov 2023 20:48:01 +0200 Subject: [PATCH 110/148] Lint fixes --- src/robotlibcore.py | 4 ++-- tasks.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 0c4d9a7..4e314c1 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -21,7 +21,7 @@ import inspect import os from dataclasses import dataclass -from typing import Any, Callable, List, Optional, Union, get_type_hints, ForwardRef +from typing import Any, Callable, List, Optional, Union, get_type_hints from robot.api.deco import keyword # noqa: F401 from robot.errors import DataError @@ -226,7 +226,7 @@ def _get_arg_spec(cls, function: Callable) -> inspect.FullArgSpec: @classmethod def _get_type_hint(cls, function: Callable): try: - hints = get_type_hints(function) + hints = get_type_hints(function) except Exception: # noqa: BLE001 hints = function.__annotations__ return hints diff --git a/tasks.py b/tasks.py index b2b07d3..2260aee 100644 --- a/tasks.py +++ b/tasks.py @@ -140,8 +140,6 @@ def lint(ctx): command = [ "robotidy", "--transform", - "RenameKeywords", - "--transform", "RenameTestCases", "-c", "RenameTestCases:capitalize_each_word=True", From b8c9340ff4cf6670e6243371f88c3cacb3aa38b4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 16 Nov 2023 20:49:56 +0200 Subject: [PATCH 111/148] Fix utest --- utest/test_get_keyword_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 7e407f3..92029a2 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -214,5 +214,5 @@ def test_kw_with_many_named_arguments_with_default(lib_types: DynamicTypesAnnota def test_lib_annotations(lib_annotation: lib_future_annotation): types = lib_annotation.get_keyword_types("future_annotations") - expected = {"arg1": Location} + expected = {"arg": Location} assert types == expected From 4628ad558e84af2b7878431724e72b555f32dbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rene=CC=81?= Date: Sat, 18 Nov 2023 22:36:52 +0100 Subject: [PATCH 112/148] added RF7 compatibility (Return Types) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René --- atest/DynamicTypesAnnotationsLibrary.py | 2 +- src/robotlibcore.py | 4 ++-- utest/test_get_keyword_types.py | 8 ++++---- utest/test_keyword_builder.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index fa47ed5..551a591 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -56,7 +56,7 @@ def keyword_new_type(self, arg: UserId): return arg @keyword - def keyword_define_return_type(self, arg: str) -> None: + def keyword_define_return_type(self, arg: str) -> Union[List[str], str]: logger.info(arg) return None diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 4e314c1..5672fb0 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -291,8 +291,8 @@ def _get_typing_hints(cls, 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 return and self statements - if arg_with_hint not in all_args: + # remove self statements + if arg_with_hint not in [*all_args, "return"]: hints.pop(arg_with_hint) return hints diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 92029a2..925ebe3 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -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} + assert types == {"arg": str, 'return': Union[List[str], str]} def test_keyword_forward_references(lib_types): @@ -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]} + assert types == {"arg1": int, "arg2": Union[bool, str], 'return': str} def test_keyword_with_robot_types_and_annotations(lib_types): @@ -125,7 +125,7 @@ def test_keyword_with_robot_types_and_bool_annotations(lib_types): def test_init_args(lib_types): types = lib_types.get_keyword_types("__init__") - assert types == {"arg": str} + assert types == {"arg": str, "return": type(None)} def test_dummy_magic_method(lib): @@ -140,7 +140,7 @@ def test_varargs(lib): def test_init_args_with_annotation(lib_types): types = lib_types.get_keyword_types("__init__") - assert types == {"arg": str} + assert types == {"arg": str, "return": type(None)} def test_exception_in_annotations(lib_types): diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 54ad082..093c20f 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -70,7 +70,7 @@ def test_types_disabled_in_keyword_deco(lib): def test_types_(lib): spec = KeywordBuilder.build(lib.args_with_type_hints) - assert spec.argument_types == {"arg3": str, "arg4": type(None)} + assert spec.argument_types == {"arg3": str, "arg4": type(None), "return": bool} def test_types(lib): From 068baf48e1e759a4d30f9673f8cea06b718e38d3 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 19 Nov 2023 15:23:29 +0200 Subject: [PATCH 113/148] Release notes for 4.3.0 --- docs/PythonLibCore-4.3.0.rst | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/PythonLibCore-4.3.0.rst diff --git a/docs/PythonLibCore-4.3.0.rst b/docs/PythonLibCore-4.3.0.rst new file mode 100644 index 0000000..da43c7e --- /dev/null +++ b/docs/PythonLibCore-4.3.0.rst @@ -0,0 +1,66 @@ +========================= +Python Library Core 4.3.0 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.3.0 is +a new release with support of Robot Framework 7.0 and return type hints. + +All issues targeted for Python Library Core v4.3.0 can be found +from the `issue tracker`_. + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.3.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.3.0 was released on Sunday November 19, 2023. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.3.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +Support RF 7.0 (`#135`_) +------------------------ +THis release supports FF 7 return type hints. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#135`_ + - enhancement + - high + - Support RF 7.0 + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#135: https://github.com/robotframework/PythonLibCore/issues/135 From d577b91ff40cd835f4d02f77dd71293219cc8586 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 19 Nov 2023 15:24:46 +0200 Subject: [PATCH 114/148] Updated version to 4.3.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 5672fb0..251a20d 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -27,7 +27,7 @@ from robot.errors import DataError from robot.utils import Importer -__version__ = "4.2.0" +__version__ = "4.3.0" class PythonLibCoreException(Exception): # noqa: N818 From c742ccf47d2906ec116e39db898e37833d9f949f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 07:27:06 +0000 Subject: [PATCH 115/148] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 73e58c4..3e9fa37 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} with Robot Framework ${{ matrix.rf-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 927995b44d896bc0b4cc78fae5a50e0c4e9292c0 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Dec 2023 00:18:45 +0200 Subject: [PATCH 116/148] Use upload-artifact v4 --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3e9fa37..f0c5860 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,8 +45,8 @@ jobs: - name: Run acceptance tests run: | python atest/run.py - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ always() }} with: - name: atest_results + name: atest_results-${{ matrix.python-version }}-${{ matrix.rf-version }} path: atest/results From 6c0319df50168c46b8d7de5e469a212688e3c3b5 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 01:38:05 +0200 Subject: [PATCH 117/148] Add VS Code setting to ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b74338b..13badd1 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,9 @@ ENV/ # PyCharm project settings .idea +# VSCode project settings +.vscode + # Robot Ouput files log.html output.xml From 8e36e69e660d63ac70ff78f95b244b5db2c8e5bd Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 01:38:30 +0200 Subject: [PATCH 118/148] For VSCode and pytest support --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c796d95 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = ./atest ./src ./utest From adae90e06374f29b4086845762915b14e3701ec7 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 01:49:06 +0200 Subject: [PATCH 119/148] Support for translations --- atest/SmallLibrary.py | 36 +++++++ atest/tests_types.robot | 6 ++ atest/translation.json | 11 ++ requirements-dev.txt | 1 + src/robotlibcore.py | 51 +++++++-- utest/test_keyword_builder.py | 24 ++--- utest/test_plugin_api.py | 22 ++-- utest/test_robotlibcore.py | 101 ++++-------------- ...robotlibcore.test_dir_dyn_lib.approved.txt | 41 +++++++ ...otlibcore.test_dir_hubrid_lib.approved.txt | 32 ++++++ ...botlibcore.test_keyword_names.approved.txt | 0 ...botlibcore.test_keyword_names.received.txt | 3 + ...re.test_keyword_names_dynamic.approved.txt | 16 +++ ...ore.test_keyword_names_hybrid.approved.txt | 16 +++ utest/test_translations.py | 26 +++++ 15 files changed, 271 insertions(+), 115 deletions(-) create mode 100644 atest/SmallLibrary.py create mode 100644 atest/translation.json create mode 100644 utest/test_robotlibcore.test_dir_dyn_lib.approved.txt create mode 100644 utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt create mode 100644 utest/test_robotlibcore.test_keyword_names.approved.txt create mode 100644 utest/test_robotlibcore.test_keyword_names.received.txt create mode 100644 utest/test_robotlibcore.test_keyword_names_dynamic.approved.txt create mode 100644 utest/test_robotlibcore.test_keyword_names_hybrid.approved.txt create mode 100644 utest/test_translations.py diff --git a/atest/SmallLibrary.py b/atest/SmallLibrary.py new file mode 100644 index 0000000..ce0c4b0 --- /dev/null +++ b/atest/SmallLibrary.py @@ -0,0 +1,36 @@ +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.""" + 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 diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 23a20fb..2388942 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -1,6 +1,7 @@ *** Settings *** Library DynamicTypesLibrary.py Library DynamicTypesAnnotationsLibrary.py xxx +Library SmallLibrary.py ${CURDIR}/translation.json *** Variables *** @@ -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 diff --git a/atest/translation.json b/atest/translation.json new file mode 100644 index 0000000..af5efd1 --- /dev/null +++ b/atest/translation.json @@ -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." + } + +} \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index aff0549..7d36f77 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ rellu >= 0.7 twine wheel typing-extensions >= 4.5.0 +approvaltests >= 11.1.1 diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 251a20d..6d41922 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -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 @@ -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 find file: {translation}") + 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])) @@ -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) diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 093c20f..4222aea 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -17,59 +17,59 @@ def dyn_types(): def test_documentation(lib): - spec = KeywordBuilder.build(lib.positional_args) + spec = KeywordBuilder.build(lib.positional_args, {}) assert spec.documentation == "Some documentation\n\nMulti line docs" - spec = KeywordBuilder.build(lib.positional_and_default) + spec = KeywordBuilder.build(lib.positional_and_default, {}) assert spec.documentation == "" def test_no_args(lib): - spec = KeywordBuilder.build(lib.no_args) + spec = KeywordBuilder.build(lib.no_args, {}) assert spec.argument_specification == [] def test_positional_args(lib): - spec = KeywordBuilder.build(lib.positional_args) + spec = KeywordBuilder.build(lib.positional_args, {}) assert spec.argument_specification == ["arg1", "arg2"] def test_positional_and_named(lib): - spec = KeywordBuilder.build(lib.positional_and_default) + spec = KeywordBuilder.build(lib.positional_and_default, {}) assert spec.argument_specification == ["arg1", "arg2", ("named1", "string1"), ("named2", 123)] def test_named_only_default_only(lib): - spec = KeywordBuilder.build(lib.default_only) + spec = KeywordBuilder.build(lib.default_only, {}) assert spec.argument_specification == [("named1", "string1"), ("named2", 123)] def test_varargs_and_kwargs(lib): - spec = KeywordBuilder.build(lib.varargs_kwargs) + spec = KeywordBuilder.build(lib.varargs_kwargs, {}) assert spec.argument_specification == ["*vargs", "**kwargs"] def test_named_only_part2(lib): - spec = KeywordBuilder.build(lib.named_only) + spec = KeywordBuilder.build(lib.named_only, {}) assert spec.argument_specification == ["*varargs", "key1", "key2"] def test_named_only(lib): - spec = KeywordBuilder.build(lib.named_only_with_defaults) + spec = KeywordBuilder.build(lib.named_only_with_defaults, {}) assert spec.argument_specification == ["*varargs", "key1", "key2", ("key3", "default1"), ("key4", True)] def test_types_in_keyword_deco(lib): - spec = KeywordBuilder.build(lib.positional_args) + spec = KeywordBuilder.build(lib.positional_args, {}) assert spec.argument_types == {"arg1": str, "arg2": int} def test_types_disabled_in_keyword_deco(lib): - spec = KeywordBuilder.build(lib.types_disabled) + spec = KeywordBuilder.build(lib.types_disabled, {}) assert spec.argument_types is None def test_types_(lib): - spec = KeywordBuilder.build(lib.args_with_type_hints) + spec = KeywordBuilder.build(lib.args_with_type_hints, {}) assert spec.argument_types == {"arg3": str, "arg4": type(None), "return": bool} diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index b51dce2..6464ede 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,4 +1,4 @@ -import my_plugin_test +from helpers import my_plugin_test import pytest from robotlibcore import Module, PluginError, PluginParser @@ -37,22 +37,22 @@ def test_plugins_string_to_modules(plugin_parser): ] -def test_parse_plugins(plugin_parser): - plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass") +def test_parse_plugins(plugin_parser: PluginParser): + plugins = plugin_parser.parse_plugins("helpers.my_plugin_test.TestClass") assert len(plugins) == 1 assert isinstance(plugins[0], my_plugin_test.TestClass) - plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass,my_plugin_test.TestClassWithBase") + plugins = plugin_parser.parse_plugins("helpers.my_plugin_test.TestClass,helpers.my_plugin_test.TestClassWithBase") assert len(plugins) == 2 assert isinstance(plugins[0], my_plugin_test.TestClass) assert isinstance(plugins[1], my_plugin_test.TestClassWithBase) def test_parse_plugins_as_list(plugin_parser): - plugins = plugin_parser.parse_plugins(["my_plugin_test.TestClass"]) + plugins = plugin_parser.parse_plugins(["helpers.my_plugin_test.TestClass"]) assert len(plugins) == 1 assert isinstance(plugins[0], my_plugin_test.TestClass) plugins = plugin_parser.parse_plugins( - ["my_plugin_test.TestClass", "my_plugin_test.TestClassWithBase"] + ["helpers.my_plugin_test.TestClass", "helpers.my_plugin_test.TestClassWithBase"] ) assert len(plugins) == 2 assert isinstance(plugins[0], my_plugin_test.TestClass) @@ -61,16 +61,16 @@ def test_parse_plugins_as_list(plugin_parser): def test_parse_plugins_with_base(): parser = PluginParser(my_plugin_test.LibraryBase) - plugins = parser.parse_plugins("my_plugin_test.TestClassWithBase") + plugins = parser.parse_plugins("helpers.my_plugin_test.TestClassWithBase") assert len(plugins) == 1 assert isinstance(plugins[0], my_plugin_test.TestClassWithBase) with pytest.raises(PluginError) as excinfo: - parser.parse_plugins("my_plugin_test.TestClass") - assert "Plugin does not inherit " in str(excinfo.value) + parser.parse_plugins("helpers.my_plugin_test.TestClass") + assert "Plugin does not inherit " in str(excinfo.value) def test_plugin_keywords(plugin_parser): - plugins = plugin_parser.parse_plugins("my_plugin_test.TestClass,my_plugin_test.TestClassWithBase") + plugins = plugin_parser.parse_plugins("helpers.my_plugin_test.TestClass,helpers.my_plugin_test.TestClassWithBase") keywords = plugin_parser.get_plugin_keywords(plugins) assert len(keywords) == 2 assert keywords[0] == "another_keyword" @@ -83,7 +83,7 @@ class PythonObject: y = 2 python_object = PythonObject() parser = PluginParser(my_plugin_test.LibraryBase, [python_object]) - plugins = parser.parse_plugins("my_plugin_test.TestPluginWithPythonArgs;4") + plugins = parser.parse_plugins("helpers.my_plugin_test.TestPluginWithPythonArgs;4") assert len(plugins) == 1 plugin = plugins[0] assert plugin.python_class.x == 1 diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index cc4a779..b4b0a96 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,8 +1,10 @@ +import json import pytest from DynamicLibrary import DynamicLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from HybridLibrary import HybridLibrary from robotlibcore import HybridCore, NoKeywordFound +from approvaltests.approvals import verify, verify_all @pytest.fixture(scope="module") @@ -10,89 +12,22 @@ def dyn_lib(): return DynamicLibrary() -def test_keyword_names(): - expected = [ - "Custom name", - 'Embedded arguments "${here}"', - "all_arguments", - "defaults", - "doc_and_tags", - "function", - "keyword_in_main", - "kwargs_only", - "mandatory", - "method", - "multi_line_doc", - "one_line_doc", - "tags", - "varargs_and_kwargs", - ] - assert HybridLibrary().get_keyword_names() == expected - assert DynamicLibrary().get_keyword_names() == expected - - -def test_dir(): - expected = [ - "Custom name", - 'Embedded arguments "${here}"', - "_DynamicCore__get_keyword", - "_DynamicCore__get_keyword_line", - "_DynamicCore__get_keyword_path", - "_HybridCore__get_component_listeners", - "_HybridCore__get_manually_registered_listeners", - "_HybridCore__get_members", - "_HybridCore__get_members_from_instance", - "_HybridCore__set_library_listeners", - "_other_name_here", - "add_library_components", - "all_arguments", - "attributes", - "class_attribute", - "defaults", - "doc_and_tags", - "embedded", - "function", - "get_keyword_arguments", - "get_keyword_documentation", - "get_keyword_names", - "get_keyword_source", - "get_keyword_tags", - "get_keyword_types", - "instance_attribute", - "keyword_in_main", - "keywords", - "keywords_spec", - "kwargs_only", - "mandatory", - "method", - "multi_line_doc", - "not_keyword_in_main", - "one_line_doc", - "run_keyword", - "tags", - "varargs_and_kwargs", - ] - assert [a for a in dir(DynamicLibrary()) if a[:2] != "__"] == expected - expected = [ - e - for e in expected - if e - not in ( - "_DynamicCore__get_typing_hints", - "_DynamicCore__get_keyword", - "_DynamicCore__get_keyword_line", - "_DynamicCore__get_keyword_path", - "_DynamicCore__join_defaults_with_types", - "get_keyword_arguments", - "get_keyword_documentation", - "get_keyword_source", - "get_keyword_tags", - "parse_plugins", - "run_keyword", - "get_keyword_types", - ) - ] - assert [a for a in dir(HybridLibrary()) if a[:2] != "__"] == expected +def test_keyword_names_hybrid(): + verify(json.dumps(HybridLibrary().get_keyword_names(), indent=4)) + + +def test_keyword_names_dynamic(): + verify(json.dumps(DynamicLibrary().get_keyword_names(), indent=4)) + +def test_dir_dyn_lib(): + result = [a for a in dir(DynamicLibrary()) if a[:2] != "__"] + result = json.dumps(result, indent=4) + verify(result) + +def test_dir_hubrid_lib(): + result = [a for a in dir(HybridLibrary()) if a[:2] != "__"] + result = json.dumps(result, indent=4) + verify(result) def test_getattr(): diff --git a/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt b/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt new file mode 100644 index 0000000..9a8ef25 --- /dev/null +++ b/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt @@ -0,0 +1,41 @@ +[ + "Custom name", + "Embedded arguments \"${here}\"", + "_DynamicCore__get_keyword", + "_DynamicCore__get_keyword_line", + "_DynamicCore__get_keyword_path", + "_HybridCore__get_component_listeners", + "_HybridCore__get_keyword_name", + "_HybridCore__get_manually_registered_listeners", + "_HybridCore__get_members", + "_HybridCore__get_members_from_instance", + "_HybridCore__set_library_listeners", + "_other_name_here", + "add_library_components", + "all_arguments", + "attributes", + "class_attribute", + "defaults", + "doc_and_tags", + "embedded", + "function", + "get_keyword_arguments", + "get_keyword_documentation", + "get_keyword_names", + "get_keyword_source", + "get_keyword_tags", + "get_keyword_types", + "instance_attribute", + "keyword_in_main", + "keywords", + "keywords_spec", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "not_keyword_in_main", + "one_line_doc", + "run_keyword", + "tags", + "varargs_and_kwargs" +] diff --git a/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt b/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt new file mode 100644 index 0000000..8579980 --- /dev/null +++ b/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt @@ -0,0 +1,32 @@ +[ + "Custom name", + "Embedded arguments \"${here}\"", + "_HybridCore__get_component_listeners", + "_HybridCore__get_keyword_name", + "_HybridCore__get_manually_registered_listeners", + "_HybridCore__get_members", + "_HybridCore__get_members_from_instance", + "_HybridCore__set_library_listeners", + "_other_name_here", + "add_library_components", + "all_arguments", + "attributes", + "class_attribute", + "defaults", + "doc_and_tags", + "embedded", + "function", + "get_keyword_names", + "instance_attribute", + "keyword_in_main", + "keywords", + "keywords_spec", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "not_keyword_in_main", + "one_line_doc", + "tags", + "varargs_and_kwargs" +] diff --git a/utest/test_robotlibcore.test_keyword_names.approved.txt b/utest/test_robotlibcore.test_keyword_names.approved.txt new file mode 100644 index 0000000..e69de29 diff --git a/utest/test_robotlibcore.test_keyword_names.received.txt b/utest/test_robotlibcore.test_keyword_names.received.txt new file mode 100644 index 0000000..4d1dd17 --- /dev/null +++ b/utest/test_robotlibcore.test_keyword_names.received.txt @@ -0,0 +1,3 @@ +Keywords + +0) ['Custom name', 'Embedded arguments "${here}"', 'all_arguments', 'defaults', 'doc_and_tags', 'function', 'keyword_in_main', 'kwargs_only', 'mandatory', 'method', 'multi_line_doc', 'one_line_doc', 'tags', 'varargs_and_kwargs', 'Custom name', 'Embedded arguments "${here}"', 'all_arguments', 'defaults', 'doc_and_tags', 'function', 'keyword_in_main', 'kwargs_only', 'mandatory', 'method', 'multi_line_doc', 'one_line_doc', 'tags', 'varargs_and_kwargs'] diff --git a/utest/test_robotlibcore.test_keyword_names_dynamic.approved.txt b/utest/test_robotlibcore.test_keyword_names_dynamic.approved.txt new file mode 100644 index 0000000..d5882b3 --- /dev/null +++ b/utest/test_robotlibcore.test_keyword_names_dynamic.approved.txt @@ -0,0 +1,16 @@ +[ + "Custom name", + "Embedded arguments \"${here}\"", + "all_arguments", + "defaults", + "doc_and_tags", + "function", + "keyword_in_main", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "one_line_doc", + "tags", + "varargs_and_kwargs" +] diff --git a/utest/test_robotlibcore.test_keyword_names_hybrid.approved.txt b/utest/test_robotlibcore.test_keyword_names_hybrid.approved.txt new file mode 100644 index 0000000..d5882b3 --- /dev/null +++ b/utest/test_robotlibcore.test_keyword_names_hybrid.approved.txt @@ -0,0 +1,16 @@ +[ + "Custom name", + "Embedded arguments \"${here}\"", + "all_arguments", + "defaults", + "doc_and_tags", + "function", + "keyword_in_main", + "kwargs_only", + "mandatory", + "method", + "multi_line_doc", + "one_line_doc", + "tags", + "varargs_and_kwargs" +] diff --git a/utest/test_translations.py b/utest/test_translations.py new file mode 100644 index 0000000..c462a77 --- /dev/null +++ b/utest/test_translations.py @@ -0,0 +1,26 @@ +from pathlib import Path +import pytest + +from SmallLibrary import SmallLibrary + + +@pytest.fixture(scope="module") +def lib(): + translation = Path(__file__).parent.parent / "atest" / "translation.json" + return SmallLibrary(translation=translation) + +def test_invalid_translation(): + translation = Path(__file__) + assert SmallLibrary(translation=translation) + +def test_translations_names(lib: SmallLibrary): + keywords = lib.keywords_spec + assert "other_name" in keywords + assert "name_changed_again" in keywords + +def test_translations_docs(lib: SmallLibrary): + keywords = lib.keywords_spec + kw = keywords["other_name"] + assert kw.documentation == "This is new doc" + kw = keywords["name_changed_again"] + assert kw.documentation == "This is also replaced.\n\nnew line." \ No newline at end of file From a798346b0b446ff25916d041f7d4058b94c51194 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 01:49:32 +0200 Subject: [PATCH 120/148] Ruff config fixes --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6fa99a2..76b9f02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -46,8 +46,8 @@ select = [ "RUF" ] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 9 -[tool.ruff.flake8-quotes] +[tool.ruff.lint.flake8-quotes] docstring-quotes = "double" From 9705679c36a0e74e8602d077644cd0b2f54e28d7 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 02:14:39 +0200 Subject: [PATCH 121/148] Lint fixes --- pyproject.toml | 8 ++++++++ tasks.py | 3 ++- utest/run.py | 16 ++++++---------- utest/test_get_keyword_source.py | 25 ++++++++++++++----------- utest/test_get_keyword_types.py | 8 ++++---- utest/test_plugin_api.py | 12 ++++++------ utest/test_robotlibcore.py | 9 ++++++--- utest/test_translations.py | 7 +++++-- 8 files changed, 51 insertions(+), 37 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 76b9f02..16759fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,14 @@ lint.select = [ "RUF" ] +[tool.ruff.lint.extend-per-file-ignores] +"utest/*" = [ + "S", + "SLF", + "PLR", + "B018" +] + [tool.ruff.lint.mccabe] max-complexity = 9 diff --git a/tasks.py b/tasks.py index 2260aee..3e98212 100644 --- a/tasks.py +++ b/tasks.py @@ -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 = [ diff --git a/utest/run.py b/utest/run.py index 1da6cd3..7400196 100755 --- a/utest/run.py +++ b/utest/run.py @@ -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() diff --git a/utest/test_get_keyword_source.py b/utest/test_get_keyword_source.py index 11828e4..f9bcc76 100644 --- a/utest/test_get_keyword_source.py +++ b/utest/test_get_keyword_source.py @@ -1,5 +1,5 @@ import inspect -from os import path +from pathlib import Path import pytest from DynamicLibrary import DynamicLibrary @@ -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): @@ -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): @@ -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 diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 925ebe3..e72803b 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -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") @@ -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): @@ -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): @@ -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") diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 6464ede..67226d6 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,5 +1,5 @@ -from helpers import my_plugin_test import pytest +from helpers import my_plugin_test from robotlibcore import Module, PluginError, PluginParser @@ -19,17 +19,17 @@ def test_plugins_string_to_modules(plugin_parser): result = plugin_parser._string_to_modules("path.to.MyLibrary,path.to.OtherLibrary") assert result == [ Module("path.to.MyLibrary", [], {}), - Module("path.to.OtherLibrary", [], {}) + Module("path.to.OtherLibrary", [], {}), ] result = plugin_parser._string_to_modules("path.to.MyLibrary , path.to.OtherLibrary") assert result == [ Module("path.to.MyLibrary", [], {}), - Module("path.to.OtherLibrary", [], {}) + Module("path.to.OtherLibrary", [], {}), ] result = plugin_parser._string_to_modules("path.to.MyLibrary;foo;bar , path.to.OtherLibrary;1") assert result == [ Module("path.to.MyLibrary", ["foo", "bar"], {}), - Module("path.to.OtherLibrary", ["1"], {}) + Module("path.to.OtherLibrary", ["1"], {}), ] result = plugin_parser._string_to_modules("PluginWithKwArgs.py;kw1=Text1;kw2=Text2") assert result == [ @@ -52,7 +52,7 @@ def test_parse_plugins_as_list(plugin_parser): assert len(plugins) == 1 assert isinstance(plugins[0], my_plugin_test.TestClass) plugins = plugin_parser.parse_plugins( - ["helpers.my_plugin_test.TestClass", "helpers.my_plugin_test.TestClassWithBase"] + ["helpers.my_plugin_test.TestClass", "helpers.my_plugin_test.TestClassWithBase"], ) assert len(plugins) == 2 assert isinstance(plugins[0], my_plugin_test.TestClass) @@ -81,6 +81,7 @@ def test_plugin_python_objects(): class PythonObject: x = 1 y = 2 + python_object = PythonObject() parser = PluginParser(my_plugin_test.LibraryBase, [python_object]) plugins = parser.parse_plugins("helpers.my_plugin_test.TestPluginWithPythonArgs;4") @@ -88,4 +89,3 @@ class PythonObject: plugin = plugins[0] assert plugin.python_class.x == 1 assert plugin.python_class.y == 2 - diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index b4b0a96..52689ad 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -1,10 +1,11 @@ import json + import pytest +from approvaltests.approvals import verify from DynamicLibrary import DynamicLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from HybridLibrary import HybridLibrary from robotlibcore import HybridCore, NoKeywordFound -from approvaltests.approvals import verify, verify_all @pytest.fixture(scope="module") @@ -17,15 +18,17 @@ def test_keyword_names_hybrid(): def test_keyword_names_dynamic(): - verify(json.dumps(DynamicLibrary().get_keyword_names(), indent=4)) + verify(json.dumps(DynamicLibrary().get_keyword_names(), indent=4)) + def test_dir_dyn_lib(): result = [a for a in dir(DynamicLibrary()) if a[:2] != "__"] result = json.dumps(result, indent=4) verify(result) + def test_dir_hubrid_lib(): - result = [a for a in dir(HybridLibrary()) if a[:2] != "__"] + result = [a for a in dir(HybridLibrary()) if a[:2] != "__"] result = json.dumps(result, indent=4) verify(result) diff --git a/utest/test_translations.py b/utest/test_translations.py index c462a77..20c3dc6 100644 --- a/utest/test_translations.py +++ b/utest/test_translations.py @@ -1,6 +1,6 @@ from pathlib import Path -import pytest +import pytest from SmallLibrary import SmallLibrary @@ -9,18 +9,21 @@ def lib(): translation = Path(__file__).parent.parent / "atest" / "translation.json" return SmallLibrary(translation=translation) + def test_invalid_translation(): translation = Path(__file__) assert SmallLibrary(translation=translation) + def test_translations_names(lib: SmallLibrary): keywords = lib.keywords_spec assert "other_name" in keywords assert "name_changed_again" in keywords + def test_translations_docs(lib: SmallLibrary): keywords = lib.keywords_spec kw = keywords["other_name"] assert kw.documentation == "This is new doc" kw = keywords["name_changed_again"] - assert kw.documentation == "This is also replaced.\n\nnew line." \ No newline at end of file + assert kw.documentation == "This is also replaced.\n\nnew line." From 68eb98559a03cd99215409ee1f2db5b69e1c2756 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 02:23:10 +0200 Subject: [PATCH 122/148] Fix for RF 5 --- atest/SmallLibrary.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/atest/SmallLibrary.py b/atest/SmallLibrary.py index ce0c4b0..55a9540 100644 --- a/atest/SmallLibrary.py +++ b/atest/SmallLibrary.py @@ -9,6 +9,9 @@ class SmallLibrary(DynamicCore): 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)) From d41993435a4f91722d38522585a7f430b6a0986b Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 16 Mar 2024 02:29:15 +0200 Subject: [PATCH 123/148] Better error --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 6d41922..8fa8f56 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -51,7 +51,7 @@ def _translation(translation: Optional[Path] = None): try: return json.load(file) except json.decoder.JSONDecodeError: - logger.warn(f"Could not find file: {translation}") + logger.warn(f"Could not convert json file {translation} to dictionary.") return {} else: return {} From b1503dcc80d3bac96fca07a89150923637bcce82 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 22:23:21 +0200 Subject: [PATCH 124/148] Fix intro doc replace --- atest/translation.json | 11 +++++++++-- src/robotlibcore.py | 5 +++++ utest/test_robotlibcore.test_dir_dyn_lib.approved.txt | 1 + ...test_robotlibcore.test_dir_hubrid_lib.approved.txt | 1 + utest/test_translations.py | 7 +++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/atest/translation.json b/atest/translation.json index af5efd1..36795c5 100644 --- a/atest/translation.json +++ b/atest/translation.json @@ -6,6 +6,13 @@ "name_changed": { "name": "name_changed_again", "doc": "This is also replaced.\n\nnew line." + }, + "__init__": { + "name": "__init__", + "doc": "Replaces init docs with this one." + }, + "__intro__": { + "name": "__intro__", + "doc": "New __intro__ documentation is here." } - -} \ No newline at end of file +} diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 8fa8f56..b42f8e6 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -70,6 +70,7 @@ def __init__(self, library_components: List, translation: Optional[Path] = None) 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 + 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"): @@ -86,6 +87,10 @@ def __get_keyword_name(self, func: Callable, name: str, translation: dict): return translation[name]["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])) diff --git a/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt b/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt index 9a8ef25..d4bb728 100644 --- a/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt +++ b/utest/test_robotlibcore.test_dir_dyn_lib.approved.txt @@ -9,6 +9,7 @@ "_HybridCore__get_manually_registered_listeners", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", + "_HybridCore__replace_intro_doc", "_HybridCore__set_library_listeners", "_other_name_here", "add_library_components", diff --git a/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt b/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt index 8579980..4de4be5 100644 --- a/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt +++ b/utest/test_robotlibcore.test_dir_hubrid_lib.approved.txt @@ -6,6 +6,7 @@ "_HybridCore__get_manually_registered_listeners", "_HybridCore__get_members", "_HybridCore__get_members_from_instance", + "_HybridCore__replace_intro_doc", "_HybridCore__set_library_listeners", "_other_name_here", "add_library_components", diff --git a/utest/test_translations.py b/utest/test_translations.py index 20c3dc6..a482a52 100644 --- a/utest/test_translations.py +++ b/utest/test_translations.py @@ -27,3 +27,10 @@ def test_translations_docs(lib: SmallLibrary): assert kw.documentation == "This is new doc" kw = keywords["name_changed_again"] assert kw.documentation == "This is also replaced.\n\nnew line." + +def test_init_and_lib_docs(lib: SmallLibrary): + keywords = lib.keywords_spec + init = keywords["__init__"] + assert init.documentation == "Replaces init docs with this one." + doc = lib.get_keyword_documentation("__intro__") + assert doc == "New __intro__ documentation is here." From 504d2106ed3c62e87680c49b0e794ea94b123e82 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 22:44:29 +0200 Subject: [PATCH 125/148] Use markdown as readme --- README.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9ab3ad --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +--- +title: Python Library Core +--- + +Tools to ease creating larger test libraries for [Robot +Framework](http://robotframework.org) using Python. The Robot Framework +[hybrid](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#hybrid-library-api) +and [dynamic library +API](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#dynamic-library-api) +gives more flexibility for library than the static library API, but they +also sets requirements for libraries which needs to be implemented in +the library side. PythonLibCore eases the problem by providing simpler +interface and handling all the requirements towards the Robot Framework +library APIs. + +Code is stable and version 1.0 is already used by +[SeleniumLibrary](https://github.com/robotframework/SeleniumLibrary/) +and +[WhiteLibrary](https://pypi.org/project/robotframework-whitelibrary/). +The version 2.0 support changes in the Robot Framework 3.2. + +[![image](https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg?branch=master)](https://github.com/robotframework/PythonLibCore) + +# Usage + +There are two ways to use PythonLibCore, either by +[HybridCore]{.title-ref} or by using [DynamicCore]{.title-ref}. +[HybridCore]{.title-ref} provides support for the hybrid library API and +[DynamicCore]{.title-ref} provides support for dynamic library API. +Consult the Robot Framework [User +Guide](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-test-libraries), +for choosing the correct API for library. + +Regardless which library API is chosen, both have similar requirements. + +1) Library must inherit either the [HybridCore]{.title-ref} or + [DynamicCore]{.title-ref}. +2) Library keywords must be decorated with Robot Framework + [\@keyword](https://github.com/robotframework/robotframework/blob/master/src/robot/api/deco.py) + decorator. +3) Provide a list of class instances implementing keywords to + [library_components]{.title-ref} argument in the + [HybridCore]{.title-ref} or [DynamicCore]{.title-ref} + [\_\_init\_\_]{.title-ref}. + +It is also possible implement keywords in the library main class, by +marking method with [\@keyword]{.title-ref} as keywords. It is not +requires pass main library instance in the +[library_components]{.title-ref} argument. + +All keyword, also keywords implemented in the classes outside of the +main library are available in the library instance as methods. This +automatically publish library keywords in as methods in the Python +public API. + +The example in below demonstrates how the PythonLibCore can be used with +a library. + +# Example + +``` python +"""Main library.""" + +from robotlibcore import DynamicCore + +from mystuff import Library1, Library2 + + +class MyLibrary(DynamicCore): + """General library documentation.""" + + def __init__(self): + libraries = [Library1(), Library2()] + DynamicCore.__init__(self, libraries) + + @keyword + def keyword_in_main(self): + pass +``` + +``` python +"""Library components.""" + +from robotlibcore import keyword + + +class Library1(object): + + @keyword + def example(self): + """Keyword documentation.""" + pass + + @keyword + def another_example(self, arg1, arg2='default'): + pass + + def not_keyword(self): + pass + + +class Library2(object): + + @keyword('Custom name') + def this_name_is_not_used(self): + pass + + @keyword(tags=['tag', 'another']) + def tags(self): + pass +``` + +# Plugin API + +It is possible to create plugin API to a library by using PythonLibCore. +This allows extending library with external Python classes. Plugins can +be imported during library import time, example by defining argumet in +library [\_\_init\_\_]{.title-ref} which allows defining the plugins. It +is possible to define multiple plugins, by seperating plugins with with +comma. Also it is possible to provide arguments to plugin by seperating +arguments with semicolon. + +``` python +from robot.api.deco import keyword # noqa F401 + +from robotlibcore import DynamicCore, PluginParser + +from mystuff import Library1, Library2 + + +class PluginLib(DynamicCore): + + def __init__(self, plugins): + plugin_parser = PluginParser() + libraries = [Library1(), Library2()] + parsed_plugins = plugin_parser.parse_plugins(plugins) + libraries.extend(parsed_plugins) + DynamicCore.__init__(self, libraries) +``` + +When plugin class can look like this: + +``` python +class MyPlugi: + + @keyword + def plugin_keyword(self): + return 123 +``` + +Then Library can be imported in Robot Framework side like this: + +``` bash +Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py +``` From 568d5e9f3818ca4e3f1a80319180014aa5bda5b3 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 22:56:50 +0200 Subject: [PATCH 126/148] Fix README --- README.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f9ab3ad..a645dca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ ---- -title: Python Library Core ---- +# Python Library Core Tools to ease creating larger test libraries for [Robot Framework](http://robotframework.org) using Python. The Robot Framework @@ -21,32 +19,27 @@ The version 2.0 support changes in the Robot Framework 3.2. [![image](https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg?branch=master)](https://github.com/robotframework/PythonLibCore) -# Usage +## Usage There are two ways to use PythonLibCore, either by -[HybridCore]{.title-ref} or by using [DynamicCore]{.title-ref}. -[HybridCore]{.title-ref} provides support for the hybrid library API and -[DynamicCore]{.title-ref} provides support for dynamic library API. +`HybridCore` or by using `DynamicCore`. `HybridCore` provides support for +the hybrid library API and `DynamicCore` provides support for dynamic library API. Consult the Robot Framework [User Guide](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-test-libraries), for choosing the correct API for library. Regardless which library API is chosen, both have similar requirements. -1) Library must inherit either the [HybridCore]{.title-ref} or - [DynamicCore]{.title-ref}. +1) Library must inherit either the `HybridCore` or `DynamicCore`. 2) Library keywords must be decorated with Robot Framework [\@keyword](https://github.com/robotframework/robotframework/blob/master/src/robot/api/deco.py) decorator. 3) Provide a list of class instances implementing keywords to - [library_components]{.title-ref} argument in the - [HybridCore]{.title-ref} or [DynamicCore]{.title-ref} - [\_\_init\_\_]{.title-ref}. + `library_components` argument in the `HybridCore` or `DynamicCore` `__init__`. -It is also possible implement keywords in the library main class, by -marking method with [\@keyword]{.title-ref} as keywords. It is not -requires pass main library instance in the -[library_components]{.title-ref} argument. +It is also possible implement keywords in the library main class, by marking method with +`@keyword` as keywords. It is not required pass main library instance in the +`library_components` argument. All keyword, also keywords implemented in the classes outside of the main library are available in the library instance as methods. This @@ -150,6 +143,6 @@ class MyPlugi: Then Library can be imported in Robot Framework side like this: -``` bash +``` robotframework Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py ``` From 169b4a69c72a52e6e2219de169f72144b0dfd421 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 23:21:55 +0200 Subject: [PATCH 127/148] Fix badge --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a645dca..4b98d9c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ and [WhiteLibrary](https://pypi.org/project/robotframework-whitelibrary/). The version 2.0 support changes in the Robot Framework 3.2. -[![image](https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg?branch=master)](https://github.com/robotframework/PythonLibCore) +[![Version](https://img.shields.io/pypi/v/robotframework-pythonlibcore.svg)](https://pypi.python.org/pypi/robotframework-pythonlibcore/) +[![Actions Status](https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg)](https://github.com/robotframework/PythonLibCore/actions) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ## Usage From 21d242480a0b35eb4a3aa9c23a294b120a605c8a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 23:24:47 +0200 Subject: [PATCH 128/148] Remove rst version of README Also fix wording in README --- README.md | 6 +-- README.rst | 149 ----------------------------------------------------- 2 files changed, 3 insertions(+), 152 deletions(-) delete mode 100644 README.rst diff --git a/README.md b/README.md index 4b98d9c..8e0c52f 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ the library side. PythonLibCore eases the problem by providing simpler interface and handling all the requirements towards the Robot Framework library APIs. -Code is stable and version 1.0 is already used by +Code is stable and is already used by [SeleniumLibrary](https://github.com/robotframework/SeleniumLibrary/) and -[WhiteLibrary](https://pypi.org/project/robotframework-whitelibrary/). -The version 2.0 support changes in the Robot Framework 3.2. +[Browser library](https://github.com/MarketSquare/robotframework-browser/). +Project supports two latest version of Robot Framework. [![Version](https://img.shields.io/pypi/v/robotframework-pythonlibcore.svg)](https://pypi.python.org/pypi/robotframework-pythonlibcore/) [![Actions Status](https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg)](https://github.com/robotframework/PythonLibCore/actions) diff --git a/README.rst b/README.rst deleted file mode 100644 index 5166682..0000000 --- a/README.rst +++ /dev/null @@ -1,149 +0,0 @@ -Python Library Core -=================== - -Tools to ease creating larger test libraries for `Robot Framework`_ using -Python. The Robot Framework `hybrid`_ and `dynamic library API`_ gives more -flexibility for library than the static library API, but they also sets requirements -for libraries which needs to be implemented in the library side. PythonLibCore -eases the problem by providing simpler interface and handling all the requirements -towards the Robot Framework library APIs. - -Code is stable and version 1.0 is already used by SeleniumLibrary_ and -WhiteLibrary_. The version 2.0 support changes in the Robot Framework -3.2. - -.. image:: https://github.com/robotframework/PythonLibCore/workflows/CI/badge.svg?branch=master - :target: https://github.com/robotframework/PythonLibCore - -Usage ------ -There are two ways to use PythonLibCore, either by `HybridCore` or by using `DynamicCore`. -`HybridCore` provides support for the hybrid library API and `DynamicCore` provides support -for dynamic library API. Consult the Robot Framework `User Guide`_, for choosing the -correct API for library. - -Regardless which library API is chosen, both have similar requirements. - -1) Library must inherit either the `HybridCore` or `DynamicCore`. -2) Library keywords must be decorated with Robot Framework `@keyword`_ decorator. -3) Provide a list of class instances implementing keywords to `library_components` argument in the `HybridCore` or `DynamicCore` `__init__`. - -It is also possible implement keywords in the library main class, by marking method with -`@keyword` as keywords. It is not requires pass main library instance in the -`library_components` argument. - -All keyword, also keywords implemented in the classes outside of the main library are -available in the library instance as methods. This automatically publish library keywords -in as methods in the Python public API. - -The example in below demonstrates how the PythonLibCore can be used with a library. - -Example -------- - -.. sourcecode:: python - - """Main library.""" - - from robotlibcore import DynamicCore - - from mystuff import Library1, Library2 - - - class MyLibrary(DynamicCore): - """General library documentation.""" - - def __init__(self): - libraries = [Library1(), Library2()] - DynamicCore.__init__(self, libraries) - - @keyword - def keyword_in_main(self): - pass - -.. sourcecode:: python - - """Library components.""" - - from robotlibcore import keyword - - - class Library1(object): - - @keyword - def example(self): - """Keyword documentation.""" - pass - - @keyword - def another_example(self, arg1, arg2='default'): - pass - - def not_keyword(self): - pass - - - class Library2(object): - - @keyword('Custom name') - def this_name_is_not_used(self): - pass - - @keyword(tags=['tag', 'another']) - def tags(self): - pass - - -Plugin API ----------- -It is possible to create plugin API to a library by using PythonLibCore. This allows extending library -with external Python classes. Plugins can be imported during library import time, example by defining argumet -in library `__init__` which allows defining the plugins. It is possible to define multiple plugins, by seperating -plugins with with comma. Also it is possible to provide arguments to plugin by seperating arguments with -semicolon. - - -.. sourcecode:: python - - from robot.api.deco import keyword # noqa F401 - - from robotlibcore import DynamicCore, PluginParser - - from mystuff import Library1, Library2 - - - class PluginLib(DynamicCore): - - def __init__(self, plugins): - plugin_parser = PluginParser() - libraries = [Library1(), Library2()] - parsed_plugins = plugin_parser.parse_plugins(plugins) - libraries.extend(parsed_plugins) - DynamicCore.__init__(self, libraries) - - -When plugin class can look like this: - -.. sourcecode:: python - - class MyPlugi: - - @keyword - def plugin_keyword(self): - return 123 - -Then Library can be imported in Robot Framework side like this: - -.. sourcecode:: bash - - Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py - - - -.. _Robot Framework: http://robotframework.org -.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary/ -.. _WhiteLibrary: https://pypi.org/project/robotframework-whitelibrary/ -.. _hybrid: https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#hybrid-library-api -.. _dynamic library API: https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#dynamic-library-api -.. _User Guide: https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-test-libraries -.. _@keyword: https://github.com/robotframework/robotframework/blob/master/src/robot/api/deco.py From 081bc84301d25eb9e0736ac2e2a129496a07fd2f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 19 Mar 2024 23:33:54 +0200 Subject: [PATCH 129/148] Convert BUILD to markdown --- BUILD.md | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++ BUILD.rst | 238 ------------------------------------------------------ 2 files changed, 227 insertions(+), 238 deletions(-) create mode 100644 BUILD.md delete mode 100644 BUILD.rst diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..3939625 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,227 @@ +# Creating PythonLibCore releases + +These instructions cover steps needed to create new releases of +PythonLibCore. Many individual steps are automated, but we don\'t want +to automate the whole procedure because it would be hard to react if +something goes terribly wrong. When applicable, the steps are listed as +commands that can be copied and executed on the command line. + +# Preconditions + +## Operating system and Python requirements + +Generating releases has only been tested on Linux, but it ought to work +the same way also on OSX and other unixes. Generating releases on +Windows may work but is not tested, supported, or recommended. + +Creating releases is only supported with Python 3.6 or newer. + +The `pip` and `invoke` commands below are also expected to run on Python +3.6+. Alternatively, it\'s possible to use the `python3.6 -m pip` +approach to run these commands. + +## Python dependencies + +Many steps are automated using the generic [Invoke](http://pyinvoke.org) +tool with a help by our [rellu](https://github.com/robotframework/rellu) +utilities, but also other tools and modules are needed. A pre-condition +is installing all these, and that\'s easiest done using +[pip](http://pip-installer.org) and the provided +[requirements-build.txt](requirements-build.txt) file: + + pip install -r requirements-build.txt + +## Using Invoke + +Invoke tasks are defined in the [tasks.py](tasks.py) file and they are +executed from the command line like: + + inv[oke] task [options] + +Run `invoke` without arguments for help. All tasks can be listed using +`invoke --list` and each task\'s usage with `invoke --help task`. + +## Different Git workflows + +Git commands used below always expect that `origin` is the project main +repository. If that\'s not the case, and instead `origin` is your +personal fork, you probably still want to push to the main repository. +In that case you need to add `upstream` or similar to `git push` +commands before running them. + +# Testing + +Make sure that adequate unit and acceptance tests are executed using +supported interpreters and operating systems before releases are +created. Unit and acceptance tests can be executed by running +[utest/run.py](utest/run.py) and [atest/run.py](atest/run.py) scripts, +respectively. + +# Preparation + +1. Check that you are on the master branch and have nothing left to + commit, pull, or push: + + git branch + git status + git pull --rebase + git push + +2. Clean up: + + invoke clean + +3. Set version information to a shell variable to ease copy-pasting + further commands. Add `aN`, `bN` or `rcN` postfix if creating a + pre-release: + + VERSION= + + For example, `VERSION=3.0.1` or `VERSION=3.1a2`. + +# Release notes + +1. Set GitHub user information into shell variables to ease + copy-pasting the following command: + + GITHUB_USERNAME= + GITHUB_PASSWORD= + + Alternatively, supply the credentials when running that command. + +2. Generate a template for the release notes: + + invoke release-notes -w -v $VERSION -u $GITHUB_USERNAME -p $GITHUB_PASSWORD + + The `-v $VERSION` option can be omitted if [version is already + set](#set-version). Omit the `-w` option if you just want to get + release notes printed to the console, not written to a file. + + When generating release notes for a preview release like `3.0.2rc1`, + the list of issues is only going to contain issues with that label + (e.g. `rc1`) or with a label of an earlier preview release (e.g. + `alpha1`, `beta2`). + +3. Fill the missing details in the generated release notes template. + +4. Make sure that issues have correct information: + + - All issues should have type (bug, enhancement or task) and + priority set. Notice that issues with the task type are + automatically excluded from the release notes. + - Issue priorities should be consistent. + - Issue titles should be informative. Consistency is good here + too, but no need to overdo it. + + If information needs to be added or edited, its better to edit it in + the issue tracker than in the generated release notes. This allows + re-generating the list of issues later if more issues are added. + +5. Add, commit and push: + + git add docs/PythonLibCore-$VERSION.rst + git commit -m "Release notes for $VERSION" docs/PythonLibCore-$VERSION.rst + git push + +6. Update later if necessary. Writing release notes is typically the + biggest task when generating releases, and getting everything done + in one go is often impossible. + +# Set version + +1. Set version information in + [src/robotlibcore.py](src/robotlibcore.py): + + invoke set-version $VERSION + +2. Commit and push changes: + + git commit -m "Updated version to $VERSION" src/robotlibcore.py + git push + +# Tagging + +1. Create an annotated tag and push it: + + git tag -a v$VERSION -m "Release $VERSION" + git push --tags + +2. Add short release notes to GitHub\'s [releases + page](https://github.com/robotframework/PythonLibCore/releases) with + a link to the full release notes. + +# Creating distributions + +1. Checkout the earlier created tag if necessary: + + git checkout v$VERSION + + This isn\'t necessary if continuing right after [tagging](#tagging). + +2. Cleanup (again). This removes temporary files as well as `build` and + `dist` directories: + + invoke clean + +3. Create source distribution and universal (i.e. Python 2 and 3 + compatible) [wheel](http://pythonwheels.com): + + python setup.py sdist bdist_wheel --universal + ls -l dist + + Distributions can be tested locally if needed. + +4. Upload distributions to PyPI: + + twine upload dist/* + +5. Verify that project the page at + [PyPI](https://pypi.org/project/robotframework-pythonlibcore/) looks + good. + +6. Test installation (add `--pre` with pre-releases): + + pip install --upgrade robotframework-pythonlibcore + +# Post actions + +1. Back to master if needed: + + git checkout master + +2. Set dev version based on the previous version: + + invoke set-version dev + git commit -m "Back to dev version" src/robotlibcore.py + git push + + For example, `1.2.3` is changed to `1.2.4.dev1` and `2.0.1a1` to + `2.0.1a2.dev1`. + +3. Close the [issue tracker + milestone](https://github.com/robotframework/PythonLibCore/milestones). + Create also new milestone for the next release unless one exists + already. + +# Announcements + +1. [robotframework-users](https://groups.google.com/group/robotframework-users) + and + [robotframework-announce](https://groups.google.com/group/robotframework-announce) + lists. The latter is not needed with preview releases but should be + used at least with major updates. Notice that sending to it requires + admin rights. + +2. Twitter. Either Tweet something yourself and make sure it\'s + re-tweeted by [\@robotframework](http://twitter.com/robotframework), + or send the message directly as [\@robotframework]{.title-ref}. This + makes the note appear also at . + + Should include a link to more information. Possibly a link to the + full release notes or an email to the aforementioned mailing lists. + +3. Slack community. The `#general` channel is probably best. + +4. Possibly also [Robot Framework + LinkedIn](https://www.linkedin.com/groups/Robot-Framework-3710899) + group. diff --git a/BUILD.rst b/BUILD.rst deleted file mode 100644 index da04047..0000000 --- a/BUILD.rst +++ /dev/null @@ -1,238 +0,0 @@ -Creating PythonLibCore releases -=============================== - -These instructions cover steps needed to create new releases of PythonLibCore. -Many individual steps are automated, but we don't want to automate -the whole procedure because it would be hard to react if something goes -terribly wrong. When applicable, the steps are listed as commands that can -be copied and executed on the command line. - -.. contents:: - :depth: 1 - -Preconditions -------------- - -Operating system and Python requirements -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Generating releases has only been tested on Linux, but it ought to work the -same way also on OSX and other unixes. Generating releases on Windows may -work but is not tested, supported, or recommended. - -Creating releases is only supported with Python 3.6 or newer. - -The ``pip`` and ``invoke`` commands below are also expected to run on Python -3.6+. Alternatively, it's possible to use the ``python3.6 -m pip`` approach -to run these commands. - -Python dependencies -~~~~~~~~~~~~~~~~~~~ - -Many steps are automated using the generic `Invoke `_ -tool with a help by our `rellu `_ -utilities, but also other tools and modules are needed. A pre-condition is -installing all these, and that's easiest done using `pip -`_ and the provided ``_ -file:: - - pip install -r requirements-build.txt - -Using Invoke -~~~~~~~~~~~~ - -Invoke tasks are defined in the ``_ file and they are executed from -the command line like:: - - inv[oke] task [options] - -Run ``invoke`` without arguments for help. All tasks can be listed using -``invoke --list`` and each task's usage with ``invoke --help task``. - -Different Git workflows -~~~~~~~~~~~~~~~~~~~~~~~ - -Git commands used below always expect that ``origin`` is the project main -repository. If that's not the case, and instead ``origin`` is your personal -fork, you probably still want to push to the main repository. In that case -you need to add ``upstream`` or similar to ``git push`` commands before -running them. - -Testing -------- - -Make sure that adequate unit and acceptance tests are executed using -supported interpreters and operating systems before releases are created. -Unit and acceptance tests can be executed by running ``_ and -``_ scripts, respectively. - -Preparation ------------ - -1. Check that you are on the master branch and have nothing left to commit, - pull, or push:: - - git branch - git status - git pull --rebase - git push - -2. Clean up:: - - invoke clean - -3. Set version information to a shell variable to ease copy-pasting further - commands. Add ``aN``, ``bN`` or ``rcN`` postfix if creating a pre-release:: - - VERSION= - - For example, ``VERSION=3.0.1`` or ``VERSION=3.1a2``. - -Release notes -------------- - -1. Set GitHub user information into shell variables to ease copy-pasting the - following command:: - - GITHUB_USERNAME= - GITHUB_PASSWORD= - - Alternatively, supply the credentials when running that command. - -2. Generate a template for the release notes:: - - invoke release-notes -w -v $VERSION -u $GITHUB_USERNAME -p $GITHUB_PASSWORD - - The ``-v $VERSION`` option can be omitted if `version is already set - `__. Omit the ``-w`` option if you just want to get release - notes printed to the console, not written to a file. - - When generating release notes for a preview release like ``3.0.2rc1``, - the list of issues is only going to contain issues with that label - (e.g. ``rc1``) or with a label of an earlier preview release (e.g. - ``alpha1``, ``beta2``). - -2. Fill the missing details in the generated release notes template. - -3. Make sure that issues have correct information: - - - All issues should have type (bug, enhancement or task) and priority set. - Notice that issues with the task type are automatically excluded from - the release notes. - - Issue priorities should be consistent. - - Issue titles should be informative. Consistency is good here too, but - no need to overdo it. - - If information needs to be added or edited, its better to edit it in the - issue tracker than in the generated release notes. This allows re-generating - the list of issues later if more issues are added. - -4. Add, commit and push:: - - git add docs/PythonLibCore-$VERSION.rst - git commit -m "Release notes for $VERSION" docs/PythonLibCore-$VERSION.rst - git push - -5. Update later if necessary. Writing release notes is typically the biggest - task when generating releases, and getting everything done in one go is - often impossible. - -Set version ------------ - -1. Set version information in ``_:: - - invoke set-version $VERSION - -2. Commit and push changes:: - - git commit -m "Updated version to $VERSION" src/robotlibcore.py - git push - -Tagging -------- - -1. Create an annotated tag and push it:: - - git tag -a v$VERSION -m "Release $VERSION" - git push --tags - -2. Add short release notes to GitHub's `releases page - `_ - with a link to the full release notes. - -Creating distributions ----------------------- - -1. Checkout the earlier created tag if necessary:: - - git checkout v$VERSION - - This isn't necessary if continuing right after tagging_. - -2. Cleanup (again). This removes temporary files as well as ``build`` and - ``dist`` directories:: - - invoke clean - -3. Create source distribution and universal (i.e. Python 2 and 3 compatible) - `wheel `_:: - - python setup.py sdist bdist_wheel --universal - ls -l dist - - Distributions can be tested locally if needed. - -4. Upload distributions to PyPI:: - - twine upload dist/* - -5. Verify that project the page at `PyPI - `_ - looks good. - -6. Test installation (add ``--pre`` with pre-releases):: - - pip install --upgrade robotframework-pythonlibcore - -Post actions ------------- - -1. Back to master if needed:: - - git checkout master - -2. Set dev version based on the previous version:: - - invoke set-version dev - git commit -m "Back to dev version" src/robotlibcore.py - git push - - For example, ``1.2.3`` is changed to ``1.2.4.dev1`` and ``2.0.1a1`` - to ``2.0.1a2.dev1``. - -3. Close the `issue tracker milestone - `_. - Create also new milestone for the next release unless one exists already. - -Announcements -------------- - -1. `robotframework-users `_ - and - `robotframework-announce `_ - lists. The latter is not needed with preview releases but should be used - at least with major updates. Notice that sending to it requires admin rights. - -2. Twitter. Either Tweet something yourself and make sure it's re-tweeted - by `@robotframework `_, or send the - message directly as `@robotframework`. This makes the note appear also - at http://robotframework.org. - - Should include a link to more information. Possibly a link to the full - release notes or an email to the aforementioned mailing lists. - -3. Slack community. The ``#general`` channel is probably best. - -4. Possibly also `Robot Framework LinkedIn - `_ group. From 877f34b2f06e3fa49d7477dfae271d0b46147a05 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 21 Mar 2024 21:42:02 +0200 Subject: [PATCH 130/148] Make transltaiton more flaxible #139 --- atest/SmallLibrary.py | 26 ++++++++++++++++++++++---- atest/translation.json | 7 +++++++ src/robotlibcore.py | 6 ++++-- utest/test_translations.py | 22 ++++++++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/atest/SmallLibrary.py b/atest/SmallLibrary.py index 55a9540..e576368 100644 --- a/atest/SmallLibrary.py +++ b/atest/SmallLibrary.py @@ -14,26 +14,44 @@ def __init__(self, translation: Optional[Path] = None): 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 + + @keyword + def not_translated(seld, a: int) -> int: + """This is not replaced.""" + print(f"{a} {type(a)}") + return a + 1 + + @keyword + def doc_not_translated(seld, a: int) -> int: + """This is not replaced also.""" + print(f"{a} {type(a)}") + return a + 1 + + @keyword + def kw_not_translated(seld, a: int) -> int: + """This is replaced too but name is not.""" + print(f"{a} {type(a)}") + return a + 1 diff --git a/atest/translation.json b/atest/translation.json index 36795c5..dbdab73 100644 --- a/atest/translation.json +++ b/atest/translation.json @@ -14,5 +14,12 @@ "__intro__": { "name": "__intro__", "doc": "New __intro__ documentation is here." + }, + "doc_not_translated": { + "name": "this_is_replaced" + } + , + "kw_not_translated": { + "doc": "Here is new doc" } } diff --git a/src/robotlibcore.py b/src/robotlibcore.py index b42f8e6..6eec23d 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -84,7 +84,8 @@ def add_library_components(self, library_components: List, translation: Optional def __get_keyword_name(self, func: Callable, name: str, translation: dict): if name in translation: - return translation[name]["name"] + if new_name := translation[name].get("name"): + return new_name return func.robot_name or name def __replace_intro_doc(self, translation: dict): @@ -236,7 +237,8 @@ def build(cls, function, translation: Optional[dict] = None): @classmethod def get_doc(cls, function, translation: dict): if kw := cls._get_kw_transtation(function, translation): - return kw["doc"] + if "doc" in kw: + return kw["doc"] return inspect.getdoc(function) or "" @classmethod diff --git a/utest/test_translations.py b/utest/test_translations.py index a482a52..cff47c6 100644 --- a/utest/test_translations.py +++ b/utest/test_translations.py @@ -34,3 +34,25 @@ def test_init_and_lib_docs(lib: SmallLibrary): assert init.documentation == "Replaces init docs with this one." doc = lib.get_keyword_documentation("__intro__") assert doc == "New __intro__ documentation is here." + + +def test_not_translated(lib: SmallLibrary): + keywords = lib.keywords_spec + assert "not_translated" in keywords + doc = lib.get_keyword_documentation("not_translated") + assert doc == "This is not replaced." + + +def test_doc_not_translated(lib: SmallLibrary): + keywords = lib.keywords_spec + assert "doc_not_translated" not in keywords + assert "this_is_replaced" in keywords + doc = lib.get_keyword_documentation("this_is_replaced") + assert doc == "This is not replaced also." + + +def test_kw_not_translated_but_doc_is(lib: SmallLibrary): + keywords = lib.keywords_spec + assert "kw_not_translated" in keywords + doc = lib.get_keyword_documentation("kw_not_translated") + assert doc == "Here is new doc" From ebad43f8367cd683fee04399683615f003ed8226 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 21 Mar 2024 21:46:46 +0200 Subject: [PATCH 131/148] Lint fixes --- src/robotlibcore.py | 4 ++-- utest/test_translations.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 6eec23d..f0fe0b3 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -83,7 +83,7 @@ def add_library_components(self, library_components: List, translation: Optional self.attributes[name] = self.attributes[kw_name] = kw def __get_keyword_name(self, func: Callable, name: str, translation: dict): - if name in translation: + if name in translation: # noqa: SIM102 if new_name := translation[name].get("name"): return new_name return func.robot_name or name @@ -236,7 +236,7 @@ def build(cls, function, translation: Optional[dict] = None): @classmethod def get_doc(cls, function, translation: dict): - if kw := cls._get_kw_transtation(function, translation): + if kw := cls._get_kw_transtation(function, translation): # noqa: SIM102 if "doc" in kw: return kw["doc"] return inspect.getdoc(function) or "" diff --git a/utest/test_translations.py b/utest/test_translations.py index cff47c6..2d009b0 100644 --- a/utest/test_translations.py +++ b/utest/test_translations.py @@ -28,6 +28,7 @@ def test_translations_docs(lib: SmallLibrary): kw = keywords["name_changed_again"] assert kw.documentation == "This is also replaced.\n\nnew line." + def test_init_and_lib_docs(lib: SmallLibrary): keywords = lib.keywords_spec init = keywords["__init__"] From 5bfd92b41795ec1999dfc89b140c20c4e109a222 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 21 Mar 2024 22:00:28 +0200 Subject: [PATCH 132/148] Update docs #139 --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/README.md b/README.md index 8e0c52f..a88b802 100644 --- a/README.md +++ b/README.md @@ -148,3 +148,87 @@ Then Library can be imported in Robot Framework side like this: ``` robotframework Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py ``` + +# Translation + +PLC supports translation of keywords names and documentation, but arguments names, tags and types +can not be currently translated. Translation is provided as a file containing +[Json](https://www.json.org/json-en.html) and as a +[Path](https://docs.python.org/3/library/pathlib.html) object. Translation is provided in +`translation` argument in the `HybridCore` or `DynamicCore` `__init__`. Providing translation +file is optional, also it is not mandatory to provide translation to all keyword. + +The keys of json are the methods names, not the keyword names, which implements keyword. Value +of key is json object which contains two keys: `name` and `doc`. `name` key contains the keyword +translated name and `doc` contains keyword translated documentation. Providing +`doc` and `name` is optional, example translation json file can only provide translations only +to keyword names or only to documentatin. But it is always recomended to provide translation to +both `name` and `doc`. + +Library class documentation and instance documetation has special keys, `__init__` key will +replace instance documentation and `__intro__` will replace libary class documentation. + +## Example + +If there is library like this: +```python +from pathlib import Path + +from robotlibcore import DynamicCore, keyword + +class SmallLibrary(DynamicCore): + """Library documentation.""" + + def __init__(self, translation: Path): + """__init__ documentation.""" + 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 +``` + +And when there is translation file like: +```json +{ + "normal_keyword": { + "name": "other_name", + "doc": "This is new doc" + }, + "name_changed": { + "name": "name_changed_again", + "doc": "This is also replaced.\n\nnew line." + }, + "__init__": { + "name": "__init__", + "doc": "Replaces init docs with this one." + }, + "__intro__": { + "name": "__intro__", + "doc": "New __intro__ documentation is here." + }, +} +``` +Then `normal_keyword` is translated to `other_name`. Also this keyword documentions is +translted to `This is new doc`. The keyword is `name_changed` is translted to +`name_changed_again` keyword and keyword documentation is translted to +`This is also replaced.\n\nnew line.`. The library class documentation is translated +to `Replaces init docs with this one.` and class documentation is translted to +`New __intro__ documentation is here.` From 1708927f9dfc61df8817308be30534e5b420d590 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 21 Mar 2024 22:02:11 +0200 Subject: [PATCH 133/148] Use RF 7 in CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f0c5860..2577265 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: python-version: [3.8, 3.11.1] - rf-version: [5.0.1, 6.1.0] + rf-version: [5.0.1, 7.0.0] steps: - uses: actions/checkout@v4 From b02ea0aac0334725a87d90acfe88521f17f746e6 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 22 Mar 2024 22:07:23 +0200 Subject: [PATCH 134/148] Release notes for 4.4.0 --- docs/PythonLibCore-4.4.0.rst | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/PythonLibCore-4.4.0.rst diff --git a/docs/PythonLibCore-4.4.0.rst b/docs/PythonLibCore-4.4.0.rst new file mode 100644 index 0000000..ff5a7b1 --- /dev/null +++ b/docs/PythonLibCore-4.4.0.rst @@ -0,0 +1,74 @@ +========================= +Python Library Core 4.4.0 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.4.0 is +a new release with enhancement to support keyword translation. Python Library +Core can translate keyword names and keyword documentation. It is also +possible to translate library init and class documentation. + +All issues targeted for Python Library Core v4.4.0 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.4.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core supports Robot Framework 5.0.1 or older and Python +3.8+. Python Library Core 4.4.0 was released on Friday March 22, 2024. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.4.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +Add translation for for keywords in PLC (`#139`_) +------------------------------------------------- +Robot Framework core has supported translations since release 6.0. Now also Python Lib Core +provides support to translate library keyword and documentation. Also it is possible to +translate library init and class level documentation. Keyword or library init argument names, argument +types and argument default values are not translated. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#139`_ + - enhancement + - critical + - Add translation for for keywords in PLC + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#139: https://github.com/robotframework/PythonLibCore/issues/139 From 16a2a2a4dc218c485df0e14a39a5fb57775be722 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 22 Mar 2024 22:08:31 +0200 Subject: [PATCH 135/148] Updated version to 4.4.0 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index f0fe0b3..47668bd 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -30,7 +30,7 @@ from robot.errors import DataError from robot.utils import Importer -__version__ = "4.3.0" +__version__ = "4.4.0" class PythonLibCoreException(Exception): # noqa: N818 From 8b756a4bd119d660109437023789bfada21bdc78 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 22 Mar 2024 22:13:45 +0200 Subject: [PATCH 136/148] Fix setup.py because of README format change --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d0fb2d4..c92d9e4 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ """.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.rst')) as f: +with open(join(CURDIR, 'README.md')) as f: LONG_DESCRIPTION = f.read() DESCRIPTION = ('Tools to ease creating larger test libraries for ' @@ -37,6 +37,7 @@ license = 'Apache License 2.0', description = DESCRIPTION, long_description = LONG_DESCRIPTION, + long_description_content_type = "text/markdown", keywords = 'robotframework testing testautomation library development', platforms = 'any', classifiers = CLASSIFIERS, From b7be0d94ef28772c90e477740720e2b134fd96c4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 6 Apr 2024 01:09:46 +0300 Subject: [PATCH 137/148] fix leaking keywords names Fixes #146 --- atest/SmallLibrary.py | 14 +++++++++----- atest/translation.json | 4 ++++ src/robotlibcore.py | 28 ++++++++++++++++++++-------- utest/test_translations.py | 8 ++++++++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/atest/SmallLibrary.py b/atest/SmallLibrary.py index e576368..3a93661 100644 --- a/atest/SmallLibrary.py +++ b/atest/SmallLibrary.py @@ -4,6 +4,13 @@ from robot.api import logger from robotlibcore import DynamicCore, keyword +class KeywordClass: + + @keyword(name="Execute SomeThing") + def execute_something(self): + """This is old""" + print("Name is here") + class SmallLibrary(DynamicCore): """Library documentation.""" @@ -12,10 +19,7 @@ def __init__(self, translation: Optional[Path] = None): 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()) + DynamicCore.__init__(self, [KeywordClass()], translation.absolute()) @keyword(tags=["tag1", "tag2"]) def normal_keyword(self, arg: int, other: str) -> str: @@ -32,7 +36,7 @@ def not_keyword(self, data: str) -> str: print(data) return data - @keyword(name="This Is New Name", tags=["tag1", "tag2"]) + @keyword(name="Name ChanGed", tags=["tag1", "tag2"]) def name_changed(self, some: int, other: int) -> int: """This one too""" print(f"{some} {type(some)}, {other} {type(other)}") diff --git a/atest/translation.json b/atest/translation.json index dbdab73..a3b2585 100644 --- a/atest/translation.json +++ b/atest/translation.json @@ -21,5 +21,9 @@ , "kw_not_translated": { "doc": "Here is new doc" + }, + "execute_something": { + "name": "tee_jotain", + "doc": "Uusi kirja." } } diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 47668bd..e652daf 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -57,35 +57,47 @@ def _translation(translation: Optional[Path] = None): 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) - self.add_library_components(library_components, translation_data) - self.add_library_components([self], translation_data) + 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): + 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) + 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): - if name in translation: # noqa: SIM102 - if new_name := translation[name].get("name"): - return new_name + 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): diff --git a/utest/test_translations.py b/utest/test_translations.py index 2d009b0..b9b9e3b 100644 --- a/utest/test_translations.py +++ b/utest/test_translations.py @@ -57,3 +57,11 @@ def test_kw_not_translated_but_doc_is(lib: SmallLibrary): assert "kw_not_translated" in keywords doc = lib.get_keyword_documentation("kw_not_translated") assert doc == "Here is new doc" + + +def test_rf_name_not_in_keywords(): + translation = Path(__file__).parent.parent / "atest" / "translation.json" + lib = SmallLibrary(translation=translation) + kw = lib.keywords + assert "Execute SomeThing" not in kw, f"Execute SomeThing should not be present: {kw}" + assert len(kw) == 6, f"Too many keywords: {kw}" From 134ca05105f7e610365d6ea82f7ed918aa8e99e1 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 6 Apr 2024 01:25:16 +0300 Subject: [PATCH 138/148] Release notes for 4.4.1 --- docs/PythonLibCore-4.4.1.rst | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/PythonLibCore-4.4.1.rst diff --git a/docs/PythonLibCore-4.4.1.rst b/docs/PythonLibCore-4.4.1.rst new file mode 100644 index 0000000..2f34057 --- /dev/null +++ b/docs/PythonLibCore-4.4.1.rst @@ -0,0 +1,70 @@ +========================= +Python Library Core 4.4.1 +========================= + + +.. default-role:: code + + +`Python Library Core`_ is a generic component making it easier to create +bigger `Robot Framework`_ test libraries. Python Library Core 4.4.1 is +a new release with a bug fix to not leak keywords names if @keyword +decorator defines custom name. + +All issues targeted for Python Library Core v4.4.1 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --pre --upgrade pip install robotframework-pythonlibcore + +to install the latest available release or use + +:: + + pip install pip install robotframework-pythonlibcore==4.4.1 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +Python Library Core 4.4.1 was released on Saturday April 6, 2024. + +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Robot Framework: http://robotframework.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-robotlibcore +.. _issue tracker: https://github.com/robotframework/PythonLibCore/issues?q=milestone%3Av4.4.1 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +If @keyword deco has custom name, original name leaks to keywords (`#146`_) +--------------------------------------------------------------------------- +If @keyword deco has custom name, then original and not translated method name +leaks to keywords. This issue is now fixed. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#146`_ + - bug + - critical + - If @keyword deco has custom name, original name leaks to keywords + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#146: https://github.com/robotframework/PythonLibCore/issues/146 From b1fb3d67ad934263e7bd1ec8acc3ec06127add7d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 6 Apr 2024 01:26:32 +0300 Subject: [PATCH 139/148] Updated version to 4.4.1 --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index e652daf..0c9cab1 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -30,7 +30,7 @@ from robot.errors import DataError from robot.utils import Importer -__version__ = "4.4.0" +__version__ = "4.4.1" class PythonLibCoreException(Exception): # noqa: N818 From d5123c73c1c8d34f87226a99f66e1faee9d99f91 Mon Sep 17 00:00:00 2001 From: Jani Mikkonen Date: Thu, 16 May 2024 00:12:21 +0300 Subject: [PATCH 140/148] Fix documentation for building the docs BUILD.md mentioned requirements-build.txt only req file was called requirements-dev.txt --- BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index 3939625..e94bc9e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -27,9 +27,9 @@ tool with a help by our [rellu](https://github.com/robotframework/rellu) utilities, but also other tools and modules are needed. A pre-condition is installing all these, and that\'s easiest done using [pip](http://pip-installer.org) and the provided -[requirements-build.txt](requirements-build.txt) file: +[requirements-dev.txt](requirements-dev.txt) file: - pip install -r requirements-build.txt + pip install -r requirements-dev.txt ## Using Invoke From c035a37c05de8e3e7c25e9fd415b439a8b1828ca Mon Sep 17 00:00:00 2001 From: Jani Mikkonen Date: Thu, 16 May 2024 00:21:44 +0300 Subject: [PATCH 141/148] Restructure robotlibcore into smalle chunks pr splits robotlibcore.py into smaller source files to rework packaging to be placed into directory inside site-packages instead of single file into root of site-packages Fixes #149 --- BUILD.md | 6 +- setup.py | 17 +- src/robotlibcore.py | 429 --------------------- src/robotlibcore/__init__.py | 42 ++ src/robotlibcore/core/__init__.py | 19 + src/robotlibcore/core/dynamic.py | 88 +++++ src/robotlibcore/core/hybrid.py | 121 ++++++ src/robotlibcore/keywords/__init__.py | 19 + src/robotlibcore/keywords/builder.py | 149 +++++++ src/robotlibcore/keywords/specification.py | 25 ++ src/robotlibcore/plugin/__init__.py | 17 + src/robotlibcore/plugin/parser.py | 73 ++++ src/robotlibcore/utils/__init__.py | 28 ++ src/robotlibcore/utils/exceptions.py | 25 ++ src/robotlibcore/utils/translations.py | 36 ++ tasks.py | 2 +- 16 files changed, 655 insertions(+), 441 deletions(-) delete mode 100644 src/robotlibcore.py create mode 100644 src/robotlibcore/__init__.py create mode 100644 src/robotlibcore/core/__init__.py create mode 100644 src/robotlibcore/core/dynamic.py create mode 100644 src/robotlibcore/core/hybrid.py create mode 100644 src/robotlibcore/keywords/__init__.py create mode 100644 src/robotlibcore/keywords/builder.py create mode 100644 src/robotlibcore/keywords/specification.py create mode 100644 src/robotlibcore/plugin/__init__.py create mode 100644 src/robotlibcore/plugin/parser.py create mode 100644 src/robotlibcore/utils/__init__.py create mode 100644 src/robotlibcore/utils/exceptions.py create mode 100644 src/robotlibcore/utils/translations.py diff --git a/BUILD.md b/BUILD.md index e94bc9e..89daf8c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -130,13 +130,13 @@ respectively. # Set version 1. Set version information in - [src/robotlibcore.py](src/robotlibcore.py): + [src/robotlibcore/__init__.py](src/robotlibcore/__init__.py): invoke set-version $VERSION 2. Commit and push changes: - git commit -m "Updated version to $VERSION" src/robotlibcore.py + git commit -m "Updated version to $VERSION" src/robotlibcore/__init__.py git push # Tagging @@ -192,7 +192,7 @@ respectively. 2. Set dev version based on the previous version: invoke set-version dev - git commit -m "Back to dev version" src/robotlibcore.py + git commit -m "Back to dev version" src/robotlibcore/__init__.py git push For example, `1.2.3` is changed to `1.2.4.dev1` and `2.0.1a1` to diff --git a/setup.py b/setup.py index c92d9e4..44f2e79 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ #!/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 CLASSIFIERS = """ Development Status :: 5 - Production/Stable @@ -21,10 +22,11 @@ Topic :: Software Development :: Testing Framework :: Robot Framework """.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 ' 'Robot Framework using Python.') @@ -43,6 +45,5 @@ classifiers = CLASSIFIERS, python_requires = '>=3.8, <4', package_dir = {'': 'src'}, - packages = find_packages('src'), - py_modules = ['robotlibcore'], + packages = ["robotlibcore","robotlibcore.core", "robotlibcore.keywords", "robotlibcore.plugin", "robotlibcore.utils"] ) diff --git a/src/robotlibcore.py b/src/robotlibcore.py deleted file mode 100644 index 0c9cab1..0000000 --- a/src/robotlibcore.py +++ /dev/null @@ -1,429 +0,0 @@ -# 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 diff --git a/src/robotlibcore/__init__.py b/src/robotlibcore/__init__.py new file mode 100644 index 0000000..3286c2d --- /dev/null +++ b/src/robotlibcore/__init__.py @@ -0,0 +1,42 @@ +# 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.4.1" + +__all__ = [ + "DynamicCore", + "HybridCore", + "KeywordBuilder", + "KeywordSpecification", + "PluginParser", + "keyword", + "NoKeywordFound", + "PluginError", + "PythonLibCoreException", + "Module", +] diff --git a/src/robotlibcore/core/__init__.py b/src/robotlibcore/core/__init__.py new file mode 100644 index 0000000..7072136 --- /dev/null +++ b/src/robotlibcore/core/__init__.py @@ -0,0 +1,19 @@ +# 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"] diff --git a/src/robotlibcore/core/dynamic.py b/src/robotlibcore/core/dynamic.py new file mode 100644 index 0000000..9e02005 --- /dev/null +++ b/src/robotlibcore/core/dynamic.py @@ -0,0 +1,88 @@ +# 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 diff --git a/src/robotlibcore/core/hybrid.py b/src/robotlibcore/core/hybrid.py new file mode 100644 index 0000000..f70f659 --- /dev/null +++ b/src/robotlibcore/core/hybrid.py @@ -0,0 +1,121 @@ +# 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__: + 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) diff --git a/src/robotlibcore/keywords/__init__.py b/src/robotlibcore/keywords/__init__.py new file mode 100644 index 0000000..6febe2c --- /dev/null +++ b/src/robotlibcore/keywords/__init__.py @@ -0,0 +1,19 @@ +# 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"] diff --git a/src/robotlibcore/keywords/builder.py b/src/robotlibcore/keywords/builder.py new file mode 100644 index 0000000..d81c677 --- /dev/null +++ b/src/robotlibcore/keywords/builder.py @@ -0,0 +1,149 @@ +# 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) diff --git a/src/robotlibcore/keywords/specification.py b/src/robotlibcore/keywords/specification.py new file mode 100644 index 0000000..5a85365 --- /dev/null +++ b/src/robotlibcore/keywords/specification.py @@ -0,0 +1,25 @@ +# 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 diff --git a/src/robotlibcore/plugin/__init__.py b/src/robotlibcore/plugin/__init__.py new file mode 100644 index 0000000..7e92ab7 --- /dev/null +++ b/src/robotlibcore/plugin/__init__.py @@ -0,0 +1,17 @@ +# 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"] diff --git a/src/robotlibcore/plugin/parser.py b/src/robotlibcore/plugin/parser.py new file mode 100644 index 0000000..6233d0f --- /dev/null +++ b/src/robotlibcore/plugin/parser.py @@ -0,0 +1,73 @@ +# 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 diff --git a/src/robotlibcore/utils/__init__.py b/src/robotlibcore/utils/__init__.py new file mode 100644 index 0000000..609b6b4 --- /dev/null +++ b/src/robotlibcore/utils/__init__.py @@ -0,0 +1,28 @@ +# 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", "_translation", "_translated_keywords"] diff --git a/src/robotlibcore/utils/exceptions.py b/src/robotlibcore/utils/exceptions.py new file mode 100644 index 0000000..c832387 --- /dev/null +++ b/src/robotlibcore/utils/exceptions.py @@ -0,0 +1,25 @@ +# 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 diff --git a/src/robotlibcore/utils/translations.py b/src/robotlibcore/utils/translations.py new file mode 100644 index 0000000..35c32f6 --- /dev/null +++ b/src/robotlibcore/utils/translations.py @@ -0,0 +1,36 @@ +# 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")] diff --git a/tasks.py b/tasks.py index 3e98212..8d85add 100644 --- a/tasks.py +++ b/tasks.py @@ -10,7 +10,7 @@ REPOSITORY = "robotframework/PythonLibCore" -VERSION_PATH = Path("src/robotlibcore.py") +VERSION_PATH = Path("src/robotlibcore/__init__.py") RELEASE_NOTES_PATH = Path("docs/PythonLibCore-{version}.rst") RELEASE_NOTES_TITLE = "Python Library Core {version}" RELEASE_NOTES_INTRO = """ From b2c4e72c4dbcfee0eab8cfe4a64a71f2db0eb295 Mon Sep 17 00:00:00 2001 From: Jani Mikkonen Date: Fri, 17 May 2024 10:52:15 +0300 Subject: [PATCH 142/148] Fix set-version task with correct quotes used code Version pattern now uses double quote instead of single quote. Fixes #152 --- tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 8d85add..0db4d90 100644 --- a/tasks.py +++ b/tasks.py @@ -11,6 +11,7 @@ REPOSITORY = "robotframework/PythonLibCore" VERSION_PATH = Path("src/robotlibcore/__init__.py") +VERSION_PATTERN = '__version__ = "(.*)"' RELEASE_NOTES_PATH = Path("docs/PythonLibCore-{version}.rst") RELEASE_NOTES_TITLE = "Python Library Core {version}" RELEASE_NOTES_INTRO = """ @@ -67,7 +68,7 @@ def set_version(ctx, version): # noqa: ARG001 to the next suitable development version. For example, 3.0 -> 3.0.1.dev1, 3.1.1 -> 3.1.2.dev1, 3.2a1 -> 3.2a2.dev1, 3.2.dev1 -> 3.2.dev2. """ - version = Version(version, VERSION_PATH) + version = Version(version, VERSION_PATH, VERSION_PATTERN) version.write() print(version) From f5f07fde5a050413639ab45d09f523fd2b758be4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 2 Aug 2024 11:55:25 +0300 Subject: [PATCH 143/148] Ruff update and lint fixes --- requirements-dev.txt | 2 +- src/robotlibcore/core/hybrid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7d36f77..4858cc4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ pytest-cov pytest-mockito robotstatuschecker black >= 23.7.0 -ruff >= 0.0.286 +ruff >= 0.5.5 robotframework-tidy invoke >= 2.2.0 twine diff --git a/src/robotlibcore/core/hybrid.py b/src/robotlibcore/core/hybrid.py index f70f659..cb0cc6c 100644 --- a/src/robotlibcore/core/hybrid.py +++ b/src/robotlibcore/core/hybrid.py @@ -88,7 +88,7 @@ def __get_members(self, component): raise TypeError( msg, ) - if type(component) != component.__class__: + if type(component) is component.__class__: msg = ( "Libraries must be modules or new-style class instances, " f"got old-style class {component.__class__.__name__} instead." From bbba333e30159b5bc9f2a08020e725616067b27e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 2 Aug 2024 11:52:29 +0300 Subject: [PATCH 144/148] Use uv to install deps --- .github/workflows/CI.yml | 9 +++++---- requirements-dev.txt | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2577265..11f54d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.11.1] - rf-version: [5.0.1, 7.0.0] + python-version: [3.8, 3.12] + rf-version: [5.0.1, 7.0.1] steps: - uses: actions/checkout@v4 @@ -26,10 +26,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install uv + uv pip install -r requirements-dev.txt --python ${{ matrix.python-version }} --system - name: Install RF ${{ matrix.rf-version }} run: | - pip install -U --pre robotframework==${{ matrix.rf-version }} + uv pip install -U robotframework==${{ matrix.rf-version }} --python ${{ matrix.python-version }} --system - name: Run ruff run: | ruff check ./src tasks.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 4858cc4..fe02ea1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +uv pytest pytest-cov pytest-mockito From fbe96f137e5787f4e39406d7c897c7168ecd2883 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 2 Aug 2024 11:59:01 +0300 Subject: [PATCH 145/148] Add ignore --- src/robotlibcore/core/hybrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore/core/hybrid.py b/src/robotlibcore/core/hybrid.py index cb0cc6c..2caa8b2 100644 --- a/src/robotlibcore/core/hybrid.py +++ b/src/robotlibcore/core/hybrid.py @@ -88,7 +88,7 @@ def __get_members(self, component): raise TypeError( msg, ) - if type(component) is component.__class__: + 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." From 6ba85665b302d6bc5e361009c70eec2a9e92eea2 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 2 Aug 2024 12:03:00 +0300 Subject: [PATCH 146/148] Fix atest --- atest/tests.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atest/tests.robot b/atest/tests.robot index 3c66808..a12b35f 100644 --- a/atest/tests.robot +++ b/atest/tests.robot @@ -14,7 +14,7 @@ Keyword Names Method Custom Name Cust Omna Me - IF $LIBRARY == "ExtendExistingLibrary" Keyword In Extending Library + IF "$LIBRARY" == "ExtendExistingLibrary" Keyword In Extending Library Method Without @keyword Are Not Keyowrds [Documentation] FAIL GLOB: No keyword with name 'Not Keyword' found.* From ac375752c80a95b28d13e4c744a381f4dfbb4862 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 7 Sep 2024 00:54:40 +0300 Subject: [PATCH 147/148] Liting with ruff --- tasks.py | 2 +- utest/test_keyword_builder.py | 1 + utest/test_plugin_api.py | 1 + utest/test_robotlibcore.py | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 0db4d90..90ebdf3 100644 --- a/tasks.py +++ b/tasks.py @@ -166,5 +166,5 @@ def utest(ctx): @task(utest, atest) -def test(ctx): # noqa: ARG001 +def test(ctx): pass diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 4222aea..9943c1c 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -3,6 +3,7 @@ import pytest from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from moc_library import MockLibrary + from robotlibcore import KeywordBuilder diff --git a/utest/test_plugin_api.py b/utest/test_plugin_api.py index 67226d6..9209d8b 100644 --- a/utest/test_plugin_api.py +++ b/utest/test_plugin_api.py @@ -1,5 +1,6 @@ import pytest from helpers import my_plugin_test + from robotlibcore import Module, PluginError, PluginParser diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 52689ad..b2497aa 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -5,6 +5,7 @@ from DynamicLibrary import DynamicLibrary from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary from HybridLibrary import HybridLibrary + from robotlibcore import HybridCore, NoKeywordFound From 90a62a07cc03c6760b68a5c797c2572c13a757b9 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Thu, 6 Mar 2025 11:45:32 +0100 Subject: [PATCH 148/148] add pip install command to readme file --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index a88b802..af276a7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,13 @@ public API. The example in below demonstrates how the PythonLibCore can be used with a library. +## 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 ``` python