-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Automatic argument conversion based on Python 3 function annotations #2890
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
Comments
One thing to consider is that do we want to handle list and dicts somehow specially. If we implement #2699, we need a utility to parse strings to lists/dicts, and using the same utility if someone uses list/dict as the argument type sounds like a good idea. In practice that would mean that keyword like def example(arg: dict):
# ... would get a dictionary as an argument, not a string looking like a dictionary, when used like
|
Another thing to consider is how to handle non-string values when a type is specified. Should we validate that the argument is of correct type? If not, should we convert the argument or make it an error? |
I think for this case it would make sense to:
I would go with option 1 for the sake of backwards compatibility with already available tests. |
I think that if we try to convert an argument and it fails, we should make it an error similarly if there are other problems with arguments. I think this is how we handle conversion with Java libraries where this functionality already exists. It's a good point that is backwards incompatible, but I doubt such conflict is common. I actually doubt there are many libraries that use annotations in the first place because they couldn't be used at all until RF 3.0.3 (#2627). The bigger questions is, what arguments should we try to convert. For example, if we have a keyword def kw(arg: int):
pass and it is passed a string, we naturally convert it. But what should we do if it is given a float like 1.5? Should we pass it as-is, convert it to int, or make it an error? I think it would be best to convert also in this case. The user has clearly declared that the argument should be an int, and we should guarantee that. |
Convert in any case, as the signature expects a specific format. |
Regarding the list / dict typing: I've dealt with this before using the List and Dict from the typing module and while you have to use an internal to get the types inside of lists / dicts, it proved to be a workable solution for the project I was working on at the time. I've included a runnable snippet below, perhaps this might offer some inspiration. p.s. Using that internal actually breaks somewhere between Python 3.3 and 3.5.2, as it moved from import inspect
from typing import List, Dict
def f(list_arg: List[int], dict_arg: Dict[str, int], int_arg: int, default_arg):
pass
signature = inspect.signature(f)
for name in list(signature.parameters.keys()):
type_ = signature.parameters[name].annotation
if type_ is inspect._empty:
print(name, "defaults to", str)
elif issubclass(type_, list):
print(name, "is a list of", getattr(type_, "__args__", str))
elif issubclass(type_, dict):
print(name, "is a dict of", getattr(type_, "__args__", (str, str)))
else:
print(name, "is a", type_) Output:
|
Thanks @jeroenroosen, that's very interesting! If we want to do validation for list/dict items we need to do something like this, but at least in the beginning just making sure keywords accepting lists/dicts get correct types might be enough. I've just got named-only argument support finalized and plan to look at this topic next. |
Just realized we could do type conversion also based on argument default values. For example, if we have a keyword with a signature |
This is but problematic with code that defaults to Take this for example:
will output:
In cases like these I would expect types to not change if the method is called without arguments, but change if called with something like a float. |
I'm not too familiar with RF internals to make a proper judgement of this, but wouldn't this be quite inconsistent? Edit: @basic-settings beat me to it :) |
How to handle How to handle numbers is another important question. I guess we could argue that if a keyword is defined like Perhaps it would be safest to do conversion only if the passed argument is a string. The main motivation of this enhancement is to automatically allow using |
Another design decision related to handling numbers. Let's assume we have keywords def int_type(arg: int):
# ...
def int_default(arg=0):
arg = float(arg)
# ... and they are used like
How should this situation be handled? In the former case it feels pretty logical to consider this an error, but with the latter case that would be a backwards incompatible change. Possible solutions I see:
What do others think about this? |
+1 for passing I would argue that if a method does not expose type hinting then conversion shouldn't be performed at all and arguments are sent as defined in the test scripts. I've seen devs validating It could also mean that the method doesn't really care if the type is |
I wrote this earlier but got a little distracted before posting, so I ended up repeating you a little:
|
We seem to agree that only strings should be converted and that |
Next question, which types should be convert automatically and how? I'm currently thinking the following. Notice that all these expect the original value to be a string. Also,
In addition to these we probably could consider at least |
I would personally chose the more explicit (strict) approach to convert to the specified type and if that fails then error out. More precise, for point 1. if My reasoning for the prevention of type conversion on point 1. to For example:
Invalid argument is not the most explicit error description and even more discerning is that the example below works fine due to expansion:
Hence the dynamic library (in our case) will do strict type checking and prevent calling the method. I decided not to perform type conversion and just We could also create a slight overhead with a |
+1 for simple types such as For boolean, I would also be in favour of converting The problem I see with For the same reason I think By the way, converting to enums is quite easy to do (and relevant for some of our in-house keywords) so I'd love to see that included: if isinstance(arg_type_from_signature, EnumMeta):
return arg_type_from_signature[str_value] |
Conversion of What's also interesting from a language perspective, you can change to Here are some more interesting use cases for
|
I brought up I was more thinking among the lines of ConfigParser.getboolean, as those conversions feel pretty natural to me. |
Thanks for the feedback! Some new comments:
|
My prototype code worked so well that I decided to push it into a remote branch for others to see and test. Easier to discuss about the details when we have something concrete that actually works. I won't have time to continue working with this more during the weekend, other than possibly answering questions via mobile. Next week I ought to have some time, but I also need to continue our woodshed project at the summer cottage. =) |
I added enum support as well. |
Thanks @jeroenroosen! I added few comments to your PR but in general the enum support looks nice and is definitely worth adding. I'm still on holiday next week and unless it gets rainy I won't be coding much. I'll return back to this more actively the week after. |
Been implementing type conversion based on default values (#2932) and decided to do some changes to the underlying conversion logic:
I was also thinking should we make |
- Support implicit conversion based on argument default values (#2932) - Fix using varargs when converting arguments - Add support for bytearray, frozenset and NoneType - Don't convert to float if int conversion fails when type information is got explicitly from annotations
- In ArgumentSpec defaults and kw-only-defaults are stored in same mapping. Earlier they were stored separately and defaults were a list. This is backwards incompatible compared to earlier releases, but this is also internal code and the change happens in a major release. - In ArgumentSpec 'annotations' was changed to 'types' to communicate what we store there. 'kwonlydefaults' and 'reqkwargs' were removed these changes only affect code added after the latest release. - Non-type function annotations are ignored. This may still change. - General clean-up made easier by the above changes. To some extend related to type conversion #2890 and #2932.
Converters are now classes. This design also makes it easy to support custom converters if we want to.
- Old "enum" module is supported along with newer "enum34" and the standard module in Python 3.4+. Old code didn't work with the old module. - Tests requiring enum are tagged with "require-enum". - Enum requirement is documented in atest/README.rst and "enum34" was conditionally (< 3.0) added to atest/requirements.txt. - At the same time made lxml installation in atest/requirement.txt conditional (CPython and PyPy). Related to type conversion issues #2890 and #2947.
Thought this issue is ready, except for writing documentation and possibly slightly enhanced error reporting, but then executed tests on Python 3.7 and noticed that the Unless the old behavior of the |
- collections.abc.Sequence is now mapped directly to list, not to list or tuple. - collections.abc.Iterable is not supported anymore. - Explicit tests for Integral, Real and ByteString. - Test cleanup.
PEP-563 added |
Uh oh!
There was an error while loading. Please reload this page.
If a library is implemented using Java, we automatically convert arguments to correct types based on the types in the keyword signature. Similar support would be very handy also with Python based libraries, but Python 2 doesn't have a built-in way to specify type information.
Python 3 supports function annotations that can be used for various purposes, for example, to provide type hints. We could use annotations to get type information and convert arguments based on that. In practice that would mean that keyword like
would get the argument as an integer when used like
Example 42
in the test data. This would avoid the need to addargument = int(argument)
to the keyword or usingExample ${42}
in the data.A problem with Python 3 only solution is that those still using Python 2 don't benefit from it, but this kind of very useful feature would also give a good reason to migrate. Python 2 end-of-life is less than 1.5 years from this day, so spending extra effort to support it doesn't feel beneficial either. A bigger problem is that function annotations are not at all compatible with Python 2, so libraries using them will be incompatible with Python 2 as well. This means Robot's own standard libraries cannot start using this functionality.
Once the support for argument conversion has been added to Python based static libraries, we need to think how to add it to the dynamic library API and to the remote library API. The former has already requested by #2068.
I'd really like to see this already in RF 3.1 and add it to it in high priority. May still be descoped if implementation turns out to be complicated.
UPDATE: We are adding type conversion also based on argument default values (#2932) and explicit information passed via the
@keyword
decorator (#2947). Also the support for dynamic API (#2068) is under development.The text was updated successfully, but these errors were encountered: