Skip to content

Commit

Permalink
Fix Union detection with custom converters. Part of #4088.
Browse files Browse the repository at this point in the history
- Introduce `utils.is_union` based on code used by TypeConverter
- Use it also with custom converters to propertly detect Unions
- Fix also subscripted generics with custom converters
  • Loading branch information
pekkaklarck committed Dec 14, 2021
1 parent ece8d2d commit cb23321
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 20 deletions.
3 changes: 3 additions & 0 deletions atest/robot/keywords/type_conversion/custom_converters.robot
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Class as converter
Custom in Union
Check Test Case ${TESTNAME}

Accept subscripted generics
Check Test Case ${TESTNAME}

Failing conversion
Check Test Case ${TESTNAME}

Expand Down
12 changes: 11 additions & 1 deletion atest/testdata/keywords/type_conversion/CustomConverters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import date, datetime
from typing import Union
from typing import List, Union


class Number:
Expand Down Expand Up @@ -51,6 +51,11 @@ def __init__(self, value: Union[int, str]):
self.value = value


class AcceptSubscriptedGenerics:
def __init__(self, numbers: List[int]):
self.sum = sum(numbers)


class Invalid:
pass

Expand All @@ -75,6 +80,7 @@ def __init__(self, arg, *, kwo):
FiDate: FiDate.from_string,
ClassAsConverter: ClassAsConverter,
ClassWithHintsAsConverter: ClassWithHintsAsConverter,
AcceptSubscriptedGenerics: AcceptSubscriptedGenerics,
Invalid: 666,
TooFewArgs: TooFewArgs,
TooManyArgs: TooManyArgs,
Expand Down Expand Up @@ -115,6 +121,10 @@ def class_with_hints_as_converter(argument: ClassWithHintsAsConverter, expected=
assert argument.value == expected


def accept_subscripted_generics(argument: AcceptSubscriptedGenerics, expected):
assert argument.sum == expected


def number_or_int(number: Union[Number, int]):
assert number == 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ Custom in Union
Int or number 1
Int or number one

Accept subscripted generics
Accept subscripted generics ${{[1, 2, 3]}} ${6}

Failing conversion
[Template] Conversion should fail
Number wrong type=Number error=ValueError: Don't know number 'wrong'.
Expand Down
7 changes: 4 additions & 3 deletions src/robot/running/arguments/customconverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from robot.utils import getdoc, type_name
from robot.utils import getdoc, is_union, type_name

from .argumentparser import PythonArgumentParser

Expand Down Expand Up @@ -83,9 +83,10 @@ def for_converter(cls, type_, converter):
arg_type = spec.types.get(spec.positional[0])
if arg_type is None:
accepts = ()
# FIXME: This Union detection is faulty. Also others have __args__!!
elif hasattr(arg_type, '__args__'):
elif is_union(arg_type):
accepts = arg_type.__args__
elif hasattr(arg_type, '__origin__'):
accepts = (arg_type.__origin__,)
else:
accepts = (arg_type,)
return cls(type_, converter, accepts)
9 changes: 2 additions & 7 deletions src/robot/running/arguments/typeconverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@

from ast import literal_eval
from collections import abc, OrderedDict
try:
from types import UnionType
except ImportError: # Python < 3.10
UnionType = ()
from typing import Union
from datetime import datetime, date, timedelta
from decimal import InvalidOperation, Decimal
Expand All @@ -27,7 +23,7 @@

from robot.libraries.DateTime import convert_date, convert_time
from robot.utils import (FALSE_STRINGS, TRUE_STRINGS, eq, get_error_message,
is_string, safe_str, seq2str, type_name)
is_string, is_union, safe_str, seq2str, type_name)

from .typeinfo import TypeInfo

Expand Down Expand Up @@ -498,8 +494,7 @@ def type_name(self):

@classmethod
def handles(cls, type_):
return (isinstance(type_, (UnionType, tuple))
or getattr(type_, '__origin__', None) is Union)
return is_union(type_, allow_tuple=True)

def _handles_value(self, value):
return True
Expand Down
2 changes: 1 addition & 1 deletion src/robot/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
parse_time)
from .robottypes import (FALSE_STRINGS, TRUE_STRINGS, is_bytes, is_dict_like,
is_falsy, is_integer, is_list_like, is_number, is_pathlike,
is_string, is_truthy, type_name, typeddict_types)
is_string, is_truthy, is_union, type_name, typeddict_types)
from .setter import setter, SetterAwareType
from .sortable import Sortable
from .text import (cut_assign_value, cut_long_message, format_assign_message,
Expand Down
11 changes: 11 additions & 0 deletions src/robot/utils/robottypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
from collections import UserString
from io import IOBase
from os import PathLike
try:
from types import UnionType
except ImportError: # Python < 3.10
UnionType = ()
from typing import Union
try:
from typing import TypedDict
except ImportError: # Python < 3.8
Expand Down Expand Up @@ -67,6 +72,12 @@ def is_dict_like(item):
return isinstance(item, Mapping)


def is_union(item, allow_tuple=False):
return (isinstance(item, UnionType)
or getattr(item, '__origin__', None) is Union
or allow_tuple and isinstance(item, tuple))


def type_name(item, capitalize=False):
if getattr(item, '__origin__', None):
item = item.__origin__
Expand Down
25 changes: 17 additions & 8 deletions utest/utils/test_robottypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any, Dict, List, Optional, Set, Tuple, Union

from robot.utils import (is_bytes, is_falsy, is_dict_like, is_list_like,
is_string, is_truthy, type_name)
is_string, is_truthy, is_union, PY_VERSION, type_name)
from robot.utils.asserts import assert_equal, assert_true


Expand All @@ -26,7 +26,7 @@ def generator():
yield 'generated'


class TestStringsAndBytes(unittest.TestCase):
class TestIsMisc(unittest.TestCase):

def test_strings(self):
for thing in ['string', 'hyvä', '']:
Expand All @@ -38,6 +38,18 @@ def test_bytes(self):
assert_equal(is_bytes(thing), True, thing)
assert_equal(is_string(thing), False, thing)

def test_is_union(self):
assert is_union(Union[int, str])
assert is_union(Union[int, str], allow_tuple=True)
assert not is_union((int, str))
assert is_union((int, str), allow_tuple=True)
if PY_VERSION >= (3, 10):
assert is_union(eval('int | str'))
assert is_union(eval('int | str'), allow_tuple=True)
for not_union in 'string', 3, [int, str], list, List[int]:
assert not is_union(not_union)
assert not is_union(not_union, allow_tuple=True)


class TestListLike(unittest.TestCase):

Expand Down Expand Up @@ -120,14 +132,11 @@ def test_file(self):
assert_equal(type_name(f), 'file')

def test_custom_objects(self):
class NewStyle: pass
class OldStyle: pass
class CamelCase: pass
class lower: pass
for item, exp in [(NewStyle(), 'NewStyle'),
(OldStyle(), 'OldStyle'),
for item, exp in [(CamelCase(), 'CamelCase'),
(lower(), 'lower'),
(NewStyle, 'NewStyle'),
(OldStyle, 'OldStyle')]:
(CamelCase, 'CamelCase')]:
assert_equal(type_name(item), exp)

def test_strip_underscores(self):
Expand Down

0 comments on commit cb23321

Please sign in to comment.