-
Notifications
You must be signed in to change notification settings - Fork 115
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
Adds literal node #865
base: master
Are you sure you want to change the base?
Adds literal node #865
Conversation
This pull request introduces 1 alert when merging 9338386 into fd06224 - view on LGTM.com new alerts:
|
Okay I clearly missed a bunch of tests, working on that. Not sure what's up with mypy not thinking Literal exists. |
The literal thing was because setup.cfg sets the mypy version to 3.6 by default. I had a legitimate problem where I need to import from |
This pull request introduces 1 alert when merging 6d302ab into fd06224 - view on LGTM.com new alerts:
|
Looks like I've ironed out all the python version issues. |
This pull request introduces 1 alert when merging 3c115d7 into fd06224 - view on LGTM.com new alerts:
|
This pull request introduces 1 alert when merging 1592df4 into fd06224 - view on LGTM.com new alerts:
|
Thanks very much @jordan-schneider. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I'm the one who originally opened #422. So I'm very happy you are doing this!
I'm obviously not an official member of OmegaConf, but I thought I would try to help you with a code review anyway.
Some questions:
- What happens if I annotate a node with just
Literal
without any type arguments forLiteral
? - Don't we have to add something like
typing_extensions;python_version<'3.8'
torequirements/base.txt
? Or istyping_extensions
a dependency anyway?
omegaconf/_utils.py
Outdated
or ( | ||
"typing_extensions.Literal" in str(type_) and not isinstance(type_, str) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, why is this needed? Isn't Literal
always from typing_extensions
in this else
branch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what's happening here is that:
python 3.8 and later has typing.Literal
which has an __origin__
attribute
python 3.7 typing_extensions.Literal
has an __origin__
attribute
python 3.6 and earlier typing_extensions.Literal
does not have an __origin__
attribute or any equivalent attribute
AND
A = Literal[5]
A is Literal # False
isinstance(A, Literal) # TypeError: isinstance() arg 2 must be a type or tuple of types
issubclass(type(A), Literal) # TypeError: issubclass() arg 2 must be a class or tuple of classes
so as far as I can tell, for 3.6 and earlier the only way to check for Literalness is using the string. This is gross but I don't know of a better way.
This logic is unnecessarily complicated though. How do you feel about
def is_literal_annotation(type_: Any) -> bool:
origin = getattr(type_, "__origin__", None)
return origin is Literal or ("typing_extensions.Literal" in str(type_) and not isinstance(type_, str))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an is_literal_type()
function here which also work with Python 3.6. The solution to the problem seems very stupid, you have to import a hidden class _Literal
:
Python 3.6.13 |Anaconda, Inc.| (default, Jun 4 2021, 14:25:59)
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing_extensions import Literal, _Literal
>>> a = Literal[0]
>>> type(a) is Literal
False
>>> type(a) is _Literal
True
I have no idea why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before 3.6 typing_extensions creates the internal _Literal
class using a typing._FinalTypingBase
mixin https://github.com/python/typing/blob/master/typing_extensions/src/typing_extensions.py#L344 , and then Literal is an instantiation of that class. So type(a) returns the outer _Literal class instead of the instantiation or something. Anyway this seems less bad than the string hack, so I'll go with this.
omegaconf/nodes.py
Outdated
if hasattr(self.literal_type, "__args__"): | ||
self.fields = list(self.literal_type.__args__) # pragma: no cover | ||
elif hasattr(self.literal_type, "__values__"): # pragma: no cover | ||
self.fields = list(self.literal_type.__values__) # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make these branches explicitly correspond to python versions. So I think
if hasattr(self.literal_type, "__args__"): | |
self.fields = list(self.literal_type.__args__) # pragma: no cover | |
elif hasattr(self.literal_type, "__values__"): # pragma: no cover | |
self.fields = list(self.literal_type.__values__) # pragma: no cover | |
if sys.version_info >= (3, 7): | |
self.fields = list(self.literal_type.__args__) # pragma: no cover | |
else: | |
self.fields = list(self.literal_type.__values__) # pragma: no cover |
but I'm not really sure about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you feel this is better? Philosophically I tend to think that directly checking is the right way to do things, in case e.g. the attribute name every switches back or something equally cursed. This also has the benefit that mypy/pylance/other syntax parsers can correctly infer that the attribute exists, which is left implicit if you're going by version.
If this is about readability, I'd be happy to add comments saying why the check is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I was just thinking that at some point in the future, omegaconf might want to drop support for Python 3.6 and then it would be useful that you can directly see that this if-else check has become unnecessary.
But, yes, a comment is likely already sufficient.
Co-authored-by: Thomas MK <[email protected]>
Co-authored-by: Thomas MK <[email protected]>
|
…hon 3.6 and earlier. Adds explicit dependency on typing_extensions. is_literal_type no longer uses string checking hack.
This pull request introduces 1 alert when merging a0e2b35 into 1d622e7 - view on LGTM.com new alerts:
|
All of the CI lints are failing due to
Which I can't replicate locally. If I remove that |
@jordan-schneider maybe upgrade the version of mypy that you have installed locally? |
The CI should pass if you rebase this branch against the |
Co-authored-by: Thomas MK <[email protected]>
Co-authored-by: Thomas MK <[email protected]>
…hon 3.6 and earlier. Adds explicit dependency on typing_extensions. is_literal_type no longer uses string checking hack.
This pull request introduces 1 alert when merging d321a4a into c0197e5 - view on LGTM.com new alerts:
|
This pull request introduces 1 alert when merging b69fd38 into c0197e5 - view on LGTM.com new alerts:
|
I think it was mostly outdated local versions of tools. The pre-commit version of black and the current version used by CircleCI also disagreed, so I updated the pre-commit version as well. |
Are there plans to merge this? It would be super useful! |
Thanks for the nudge, @codekansas!
Yes. We haven't been able to finalize this PR yet as the OmegaConf team's bandwidth is limited. It is on the roadmap. |
@Jasha10 nudge again to enquire about plans/status since some time has passed |
+1 this feature would be great and feels more pythonic than |
+1 |
1 similar comment
+1 |
+1, still relevant |
+1 |
1 similar comment
+1 |
Another gentle nudge, this would be very nice to have! |
Adds literal node, solving #422
I tried to implement this as a wrapper around
EnumNode
, but this turned out to be impossible.Literal
can acceptint, str, bool
andEnum
values, but enum keys must be strings. So for examplethere is no way to map both of these values to separate enum keys without also causing a collision somewhere else.
This PR validates a
LiteralNode
value by insisting on both=
equality and type equality. Both are needed, otherwise python's promiscuous primitive equalities cause problems e.g.1.0
is valid underLiteral[True]
.I tried to add tests wherever
EnumNode
had tests, but I'm unfamiliar with the codebase. Please let me know if additional tests are needed.