Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion atest/DynamicTypesAnnotationsLibrary.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import List, Union, NewType, Dict
from enum import Enum
from typing import List, Union, NewType, Optional

from robot.api import logger

from robotlibcore import DynamicCore, keyword

UserId = NewType('UserId', int)

penum = Enum("penum", "ok")


class CustomObject(object):

Expand Down Expand Up @@ -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}'
6 changes: 6 additions & 0 deletions atest/moc_library_py3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Optional


class MockLibraryPy3:

def named_only(self, *varargs, key1, key2):
Expand All @@ -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, xxx, arg1: Optional[str] = None, arg2: Optional[str] = None, arg3=False):
pass
11 changes: 11 additions & 0 deletions atest/tests_types.robot
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ Varargs and KeywordArgs With Typing Hints
Should Match ${return}
... this_is_mandatory: <class 'str'>, (1, 2, 3, 4): <class 'tuple'>, True: <class 'bool'>, {'key1': 1, 'key2': 2}: <class 'dict'>

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
Expand Down
43 changes: 36 additions & 7 deletions src/robotlibcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import os
import sys

from robot.utils import PY_VERSION

try:
import typing
except ImportError:
Expand Down Expand Up @@ -245,9 +247,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):
Expand All @@ -257,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:
Expand All @@ -276,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): # noqa
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):

Expand Down
9 changes: 8 additions & 1 deletion utest/test_keyword_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 == {'arg1': str, 'arg2': str}