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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions atest/robot/keywords/type_conversion/custom_converters.robot
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ Invalid converters
Non-type annotation
Check Test Case ${TESTNAME}

From registered type
Check Test Case ${TESTNAME}

Using library decorator
Check Test Case ${TESTNAME}

Expand Down
29 changes: 28 additions & 1 deletion atest/testdata/keywords/type_conversion/CustomConverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
except ImportError:
from typing_extensions import TypedDict

from robot.api.deco import not_keyword
from robot.api.deco import not_keyword, register_converter

not_keyword(TypedDict)

Expand Down Expand Up @@ -43,6 +43,30 @@ def parse_bool(value: Union[str, int, bool]):
return value not in ["false", "", "epätosi", "\u2639", False, 0]


class AutoConvertedNumber:
"""type placeholder"""


@register_converter(AutoConvertedNumber)
def autonumber_from_str(value):
try:
return ["zero", "one", "two", "three", "four"].index(value.lower())
except ValueError:
raise ValueError(f"Don't know number {value!r}.")


class AutoConvertedSpecialNumber:
"""type placeholder"""


@register_converter(AutoConvertedSpecialNumber, str, type_name='special number')
def specialnumber_from_str(value, type_info):
try:
return dict(two=2, three=3, five=5, seven=7, eleven=11)[value.lower()]
except KeyError:
raise ValueError(f"Number {value!r} isn't a {type_info.name}.")


class UsDate(date):
@classmethod
def from_string(cls, value) -> date:
Expand Down Expand Up @@ -148,6 +172,9 @@ def number(argument: Number, expected: int = 0):
if argument != expected:
raise AssertionError(f"Expected value to be {expected!r}, got {argument!r}.")

def special_number(argument: AutoConvertedSpecialNumber, expected: int = 0):
if argument != expected:
raise AssertionError(f"Expected value to be {expected!r}, got {argument!r}.")

def true(argument: bool):
assert argument is True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from CustomConverters import AutoConvertedNumber, AutoConvertedSpecialNumber


class CustomConvertersWithRegisteredType:
"""
Library does not state any converters explicitly.
Conversion is done from registered type.
"""

def from_registered_type(self, value: AutoConvertedNumber, expected: int):
assert value == expected, f"value [{value}] == expected [{expected}]"

def from_registered_type_special(self, value: AutoConvertedSpecialNumber, expected: int):
assert value == expected, f"value [{value}] == expected [{expected}]"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Library CustomConverters.py
Library CustomConverters.StatefulLibrary
Library CustomConverters.StatefulGlobalLibrary
Library CustomConvertersWithRegisteredType.py
Library CustomConvertersWithLibraryDecorator.py
Library CustomConvertersWithDynamicLibrary.py
Library InvalidCustomConverters.py
Expand Down Expand Up @@ -68,6 +69,8 @@ Failing conversion
True ${1.0} type=boolean arg_type=float
Class with hints as converter
... ${1.2} type=ClassWithHintsAsConverter arg_type=float
Special number
... four type=AutoConvertedSpecialNumber error=Number 'four' isn't a special number.

`None` as strict converter
Strict ${{CustomConverters.Strict()}}
Expand Down Expand Up @@ -103,6 +106,11 @@ Non-type annotation
Non type annotation x x
Non type annotation ${2}

From registered type
From registered type zero 0
From registered type four 4
From registered type special seven 7

Using library decorator
Using library decorator one 1
Using library decorator expected=2 value=two
Expand Down
27 changes: 27 additions & 0 deletions src/robot/api/deco.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from typing import Any, Callable, Literal, overload, Sequence, TypeVar, Union

from .interfaces import TypeHints
from robot.libdocpkg.standardtypes import STANDARD_TYPE_DOCS
from robot.running.arguments import TypeConverter

F = TypeVar("F", bound=Callable[..., Any])
K = TypeVar("K", bound=Callable[..., Any])
Expand Down Expand Up @@ -201,3 +203,28 @@ def decorator(cls: L) -> L:
return cls

return decorator


@not_keyword
def register_converter(target_type, *args, type_name=None):
def decorator(func):
class ConverterProxy(TypeConverter):
type = target_type
if args: value_types = args
doc = func.__doc__ or target_type.__doc__ or target_type.__name__
type_name = target_type.__name__

def __init__(self, type_info, custom_converters=None, languages=None):
super().__init__(type_info, custom_converters, languages)
if type_name: self.type_info.name = type_name

def _convert(self, value):
if func.__code__.co_argcount == 2:
return func(value, self.type_info)
else:
return func(value)

STANDARD_TYPE_DOCS[target_type] = ConverterProxy.doc
TypeConverter.register(ConverterProxy)
return func
return decorator