From 1189df8b81ef2acee188e2a4782bc4c040f682eb Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 8 Jul 2020 01:01:36 +0300 Subject: [PATCH 1/5] Improve test --- atest/moc_library_py3.py | 6 ++++++ utest/test_keyword_builder.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/atest/moc_library_py3.py b/atest/moc_library_py3.py index 81f729d..afbc1d0 100644 --- a/atest/moc_library_py3.py +++ b/atest/moc_library_py3.py @@ -1,3 +1,6 @@ +from typing import Optional + + class MockLibraryPy3: def named_only(self, *varargs, key1, key2): @@ -11,3 +14,6 @@ def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool: def self_and_keyword_only_types(x: 'MockLibraryPy3', mandatory, *varargs: int, other: bool, **kwargs: int): pass + + def optional_none(self, arg: Optional[str] = None): + pass diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 809b29f..19b755d 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -3,6 +3,7 @@ from robotlibcore import PY2, RF31, KeywordBuilder from moc_library import MockLibrary if not PY2: + from typing import Union from moc_library_py3 import MockLibraryPy3 @@ -99,6 +100,12 @@ def test_types_(lib_py3): @pytest.mark.skipif(PY2, reason='Only for Python 3') -def test_types_(lib_py3): +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 == {'arg': Union[str, None]} From 15d5eb8e42f3e851c7543239fbfaef30b04894c7 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 9 Jul 2020 00:16:17 +0300 Subject: [PATCH 2/5] Clean code --- src/robotlibcore.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index c932f4d..ae66e8e 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -245,9 +245,7 @@ def _get_types(cls, function): types = getattr(function, 'robot_types', ()) if types is None or types: return types - if not types: - types = cls._get_typing_hints(function) - return types + return cls._get_typing_hints(function) @classmethod def _get_typing_hints(cls, function): From c118e116278e5025b37d8bfde87c2f4facc2d6c8 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 9 Jul 2020 00:50:45 +0300 Subject: [PATCH 3/5] Remove Union['x', None] from typing hints Instead just return 'x' as hint --- atest/moc_library_py3.py | 2 +- src/robotlibcore.py | 39 +++++++++++++++++++++++++++++++---- utest/test_keyword_builder.py | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/atest/moc_library_py3.py b/atest/moc_library_py3.py index afbc1d0..c9444ed 100644 --- a/atest/moc_library_py3.py +++ b/atest/moc_library_py3.py @@ -15,5 +15,5 @@ def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool: def self_and_keyword_only_types(x: 'MockLibraryPy3', mandatory, *varargs: int, other: bool, **kwargs: int): pass - def optional_none(self, arg: Optional[str] = None): + def optional_none(self, xxx, arg1: Optional[str] = None, arg2: Optional[str] = None, arg3=False): pass diff --git a/src/robotlibcore.py b/src/robotlibcore.py index ae66e8e..1a05e66 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -23,6 +23,8 @@ import os import sys +from robot.utils import PY_VERSION + try: import typing except ImportError: @@ -255,16 +257,17 @@ def _get_typing_hints(cls, function): hints = typing.get_type_hints(function) except Exception: hints = function.__annotations__ - all_args = cls._args_as_list(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: hints.pop(arg_with_hint) - return hints + default = cls._get_defaults(arg_spec) + return cls._remove_optional_none_type_hints(hints, default) @classmethod - def _args_as_list(cls, function): - arg_spec = cls._get_arg_spec(function) + def _args_as_list(cls, function, arg_spec): function_args = [] function_args.extend(cls._drop_self_from_args(function, arg_spec)) if arg_spec.varargs: @@ -274,6 +277,34 @@ def _args_as_list(cls, function): function_args.append(arg_spec.varkw) return function_args + # 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): + type_hints[arg] = types[0] + return type_hints + + # 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: + return {} + names = arg_spec.args[-len(arg_spec.defaults):] + return zip(names, arg_spec.defaults) + class KeywordSpecification(object): diff --git a/utest/test_keyword_builder.py b/utest/test_keyword_builder.py index 19b755d..eb58c4a 100644 --- a/utest/test_keyword_builder.py +++ b/utest/test_keyword_builder.py @@ -108,4 +108,4 @@ def test_types(lib_py3): @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 == {'arg': Union[str, None]} + assert spec.argument_types == {'arg1': str, 'arg2': str} From fb4bda0ac642aea5317dfb380cf7ca7ef4a41e98 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 9 Jul 2020 00:54:41 +0300 Subject: [PATCH 4/5] Ignore flake8 error --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 1a05e66..3897536 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -287,7 +287,7 @@ def _remove_optional_none_type_hints(cls, type_hints, defaults): type_ = type_hints[arg] if cls._is_union(type_): types = type_.__args__ - if len(types) == 2 and types[1] is type(None): + if len(types) == 2 and types[1] is type(None): # noqa type_hints[arg] = types[0] return type_hints From e88da5a2338e40243da0ffd119b79df2d5682ee9 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 9 Jul 2020 23:29:12 +0300 Subject: [PATCH 5/5] Testing Enum conversion --- atest/DynamicTypesAnnotationsLibrary.py | 11 ++++++++++- atest/tests_types.robot | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 7178527..a53edd2 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -1,4 +1,5 @@ -from typing import List, Union, NewType, Dict +from enum import Enum +from typing import List, Union, NewType, Optional from robot.api import logger @@ -6,6 +7,8 @@ UserId = NewType('UserId', int) +penum = Enum("penum", "ok") + class CustomObject(object): @@ -117,3 +120,9 @@ def keyword_self_and_keyword_only_types(x: 'DynamicTypesAnnotationsLibrary', man **kwargs: int): return (f'{mandatory}: {type(mandatory)}, {varargs}: {type(varargs)}, ' f'{other}: {type(other)}, {kwargs}: {type(kwargs)}') + + @keyword + def enum_conversion(self, param: Optional[penum] = None): + logger.info(f'OK {param}') + logger.info(param.ok) + return f'OK {param}' diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 83e94c8..139d983 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -63,6 +63,17 @@ Varargs and KeywordArgs With Typing Hints 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 + + *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3 Only ${py3} = DynamicTypesLibrary.Is Python 3