-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to register custom converters for keyword arguments #4088
Comments
This would be a very convenient feature and having it already in RF 5.0 would be great. About the design:
|
thanks for taking this idea on board!
i agree. yesterday i actually attempted to create a decorator for keywords to add this functionality but got caught up in the rabbithole of runtime type-checking. for example: @keyword(converters={list[date]: parse_date_range})
def do_thing(value: str, dates: list[date] | None) -> None:
... how do you match the type of the |
It seems we can simply use equality check with generics: >>> from typing import List, get_type_hints
>>> def f(arg: List[int]):
... pass
...
>>> typ = get_type_hints(f)['arg']
>>> typ == List[int]
True
>>> typ == List[str]
False
>>> typ == List
False Probably should use |
Are you @DetachHead interested to look at implementing this? I have many other tasks for RF 5.0 and getting help here would be great. I'm obviously happy to review PR and help also otherwise. |
One more thing about the design: Library level defaults for converters should probably be read from a class or module level attribute like |
@pekkaklarck i'm down to give it a go |
Cool! Let me know if you need any help. Notice also that I'll shortly remote Python 2 support and that is likely to affect pretty much all code at least on some level. |
@pekkaklarck sorry i haven't really had much time to work on this. i'm still interested to work on it but just don't know when i'll have the time |
No worries. RF 5 work is now in good speed and Python 2 support has been removed. We'll concentrate on new syntax like TRY/EXCEPT first so there's plenty of time for you to get started. Keep us informed if you do! |
I did some prototyping and now the basic functionality works on my machine. More precisely, the test in the original description (that I modified a bit) now passes with this library: from datetime import date, datetime
def parse_date(value: str) -> date:
return datetime.strptime(value, '%m/%d/%Y').date()
ROBOT_LIBRARY_CONVERTERS = {date: parse_date}
def do_thing(value: date):
assert value == date(2021, 9, 15) I need to still add error handling, support for |
We still need to decide is it enough to be able to specify custom converters on the library level or should it be possible also with individual keywords. Specifying them only once is certainly more convenient than needing to do it with each keyword like @keyword(converters={date: parse_date})
def do_thing(value: date):
...
@keyword(converters={date: parse_date})
def do_another_thing(value: date):
... The above isn't even that much better than doing conversion manually: def do_thing(value):
value = parse_date(value)
...
def do_another_thing(value: date):
value = parse_date(value)
... Being able to specify custom converters would have two benefits, though:
I'm starting to doubt the above benefits are big enough compared to how much more complicated implementation would get. Same benefits could also be got by having custom types like this: class UsDate(date):
pass
class FiDate(date):
pass
def parse_us_date(value: str) -> date:
return datetime.strptime(value, '%m/%d/%Y').date()
def parse_fi_date(value: str) -> date:
return datetime.strptime(value, '%Y.%m.%d').date()
ROBOT_LIBRARY_CONVERTERS = {UsDate: parse_us_date, FiDate: parse_fi_date}
def us_example(value: UsDate):
...
def fi_example(value: FiDate):
...
def example(us_date: UsDate, fi_date: FiDate):
... |
Another reason to limit converters only to the library level is that it would ease documenting them in Libdoc outputs like we do with Enums and TypedDicts (#3607). Part of this documentation is creating links to keywords that use these types, and a single type like |
I like the workaround you show above creating a "pseudo" class of UsDate and FiDate that allows one to have a keyword based convertor while still maintaining the simplicity of the library level functionality. I was on the fence with the question of library versus keyword level implementation. I didn't have any real world examples to show support for either way but could see two keywords needing different convertors for the same type. This sounds good to me. |
i believe the main benefit of this feature is type safety. in the second example, there's no type safety in the keyword's implementation because |
One of the main benefits I see to this change is allowing dry-run to ensure valid inputs in tests. @keyword(converters=dict(value=parse_date_phrase))
def do_another_thing(value: date):
value
...
def parse_date_phrase(phrase: str) -> date: ... some test
Do Another Thing two weeks from today In this example, what I really want to happen is that the input string is validated statically. |
@DetachHead, the reason you cannot have
is that then Robot would do automatic conversion to @KotlinIsland Dry-run can work regardless are converters specified for each keyword or for the whole library. Validation is dynamic, not static, though. |
One drawback of only supporting library level converters would be that with module based libs you couldn't use convenient @library(converters={...})
class Lib:
... and instead needed somewhat ugly ROBOT_LIBRARY_CONVERTERS = {...} With just a few keywords @keyword(converters={...})
def kw(...):
... could be nicer. A good solution for this problem would be somehow making it possible to use the LIBRARY = library(...) to enable that. This would require a separate issue, though. |
our use case is to override this functionality so that we can implement our own custom converters like in @KotlinIsland's example. sorry that i never got time to work on this though |
@DetachHead Custom converters will certainly work as my early prototype demonstrated with |
For what is worth I think the drawback you mention, Pekka, is very minor. The important thing is that one can use custom converters. And it's not difficult nor requires a lot of code. It is just not the most visual pleasing syntax which, as you mention can be handle with a separate issue. Great job with this! |
Implementation ought to be mostly ready, incl. tests, but documentation is totally missing.
thanks so much for implementing this! a couple things:
|
Thanks a lot for testing the new functionality @DetachHead! Here are replies to your comments above:
|
perhaps a way to declare converters globally? in our case we have a bunch of libraries and we want our converters to be accessible in all of them so it would be convenient if we could just define them in one spot. maybe the decorator could be the way to do that |
here's another idea i had for globally defining converters, specifically for your own classes: class RobotConvertable(ABC):
@abstractmethod
@classmethod
def from_string(cls: type[Self], value: str) -> Self:
...
class Thing(RobotConvertable):
@classmethod
def from_string(cls: type[Self], value: str) -> Self:
...
@deco.keyword #robot checks each type to see if they extend RobotConvertable
def foo(value: Thing)
... |
I thought about types themselves being converters so that if they contain a classmethod like Now that we have one way to define converters, I seriously doubt adding a new approach would bring much benefits. There would be quite a bit of extra work in implementation, testing and documentation, and for users having two ways could be confusing. If you have a lot of libraries, explicitly defining converters is a little extra work but not that much. Converters themselves can be in a reusable module where they can be imported from. |
found an issue where it doesn't like properties defined on the class: class Foo:
value1: str
def __init__(self, value: str) -> None:
self.value1 = value
ROBOT_LIBRARY_CONVERTERS = {Foo: Foo}
def bar(value: Foo) -> None:
print(value) robot throws the following error:
changing |
Move responsibility mostly to TypeConverter that is responsible on conversion as well. This way information shown by Libdoc ought to be consistent with actual conversion. It also makes it easier to get information about built-in converters to Libdoc later (#4160).
Thanks for the report @DetachHead. I was able to reproduce the problem and have already fixed it locally. Need to still add tests. |
The problem was due to >>> class C:
... a: int
... def __init__(self, b: str):
... pass
...
>>> inspect.signature(C)
<Signature (b: str)>
>>> typing.get_type_hints(C)
{'a': <class 'int'>} As the result type hints and signature didn't match. |
- 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
what's the expected behavior in cases like this? ROBOT_LIBRARY_CONVERTERS = {date: parse_date}
def foo(value: date | str):
... *** test cases ***
positive test
foo 01/01/2020
test invalid dates
foo 32/01/2020 currently in both tests it passes the value as a however in my case i want it to try and run the converter, and just fall back to a string if it fails. |
That logic isn't related to custom converts but to how Unions are handled. If the given argument has any of the accepted types, it is passed as-is. That behavior has its problems but other alternatives had even more problems. One use case for custom converters actually is being able to control this fully yourself. See the following comment for a bit long explanation with links to relevant issues: |
Don't include 'ValueError' prefix in errors reported to user unnecessarily. In practice change the final error from ValueError: Argument 'x' got value 'inv' that cannot be converted to X: ValueError: reported errror to ValueError: Argument 'x' got value 'inv' that cannot be converted to X: reported errror Documentation will also recommend using ValueErrors for reporting errors.
This functionality is now documented in the User Guide. We only generate the User Guide with final releases, but you the reStructuredText source files are rendered pretty well on GitHub. This particular documentation can be found here: It would be great if people interested in this functionality would check the docs and comment if there's something to be enhanced or fixed (incl. typos). |
This issue ought to now be done. How custom types will be shown/stored in Libdoc outputs is still likely to change, but that is covered by #4160. Documentation can still be enhanced and I leave this issue open for few days to wait for comments related to that. |
No comments so far and I close this issue. That doesn't mean comments weren't appreciated in the future! |
Don't include 'ValueError' prefix in errors reported to user unnecessarily. In practice change the final error from ValueError: Argument 'x' got value 'inv' that cannot be converted to X: ValueError: reported errror to ValueError: Argument 'x' got value 'inv' that cannot be converted to X: reported errror Documentation will also recommend using ValueErrors for reporting errors.
Everything worked fine so no code changes were needed.
maybe something like
The text was updated successfully, but these errors were encountered: