-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Add support of typing.Union[XYZ, None]
#6333
Conversation
Cython/Compiler/ExprNodes.py
Outdated
def analyse_as_type(self, env): | ||
return py_none_type | ||
|
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.
Doesn't this allow code like cdef None x
or x: None = 25
? It seems to me that Optional[]
or Union[]
might just be special enough to specifically look for None
in their type arguments rather than treating None
as a type generally. None
really isn't a type, it's a value that is allowed by the type system for practical syntax purposes (because writing NoneType
would be awkward).
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.
Doesn't this allow code like
cdef None x
orx: None = 25
?
cdef None
is not allowed even in this PR - I can add test for that if needed:
(cython311) root@c113a6388b51:~/dev/cython/test# cat aa.pyx
cdef None x
(cython311) root@c113a6388b51:~/dev/cython/test# cython -3 aa.pyx
Error compiling Cython file:
------------------------------------------------------------
...
cdef None x
^
------------------------------------------------------------
aa.pyx:1:5: 'None' is not a type identifier
And x: None
is already allowed in master (I suspect it fallbacks to object
)
It seems to me that Optional[] or Union[] might just be special enough to specifically look for None in their type arguments rather than treating None as a type generally.
We need to have None
type here to distinguish two cases:
None
value in type definition (e.g.Union[None, ExtentionType]
) and- unknown type (e.g.
Union[ExtentionType, NonExistingType]
) - in this case we are havingNone
value inspecialize_here()
function:
None really isn't a type
I don't fully agree. There is None
type but is not part of public CAPI [1] of python (and this PR it uses just internal representation of this type - not using python CAPI):
>>> type(None)
<class 'NoneType'>
In general, I am using None
type only to distinguish None
in type from unknown type. (Moreover it makes sense to me to use type for None
) If there is better/other solution I am fine to change the approach.
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.
If we find None
in code, we don't replace it with None
as value, but with a NoneNode
. That makes it easy to distinguish from the value None
(as in "no node").
As you correctly wrote, the Python type is called NoneType
, not None
. The fact that Python uses None
in the type system instead is simply that for users, typing NoneType
is very unusual. Many programmers probably have never seen it but know None
only as a constant value. Thus, None
is ubiquitous and easy to read and understand as part of a type annotation – it allows None
as a value, thus making the typed value optional.
However, that doesn't mean that None
is a type, or that we have to represent it as a type in Cython. Just check the type declaration node for node.is_none
to see if we found None
in the source code.
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.
However, that doesn't mean that
None
is a type, or that we have to represent it as a type in Cython. Just check the type declaration node fornode.is_none
to see if we foundNone
in the source code.
The core of this PR is following part:
def specialize_here(self, pos, env, template_values=None):
if py_none_type in template_values:
self.contains_none = True
Here, we are detecting whether annotation contains None
(E.g. Union[XYZ, None]
). I double checked it and without introducing NoneType
, this code gets literal None
value for both cases:
Union[int, None]
Union[int, DummyType]
Hence, without further changes I cannot distinguish following cases:
- non-recognized type - in this case we should evaluate annotation as
object
None
value - in this case the type is candidate for evaluation of optional type - e.g.Union[]
Note
To replicate it, just use master and put debug print/breakpoint in following line:
cython/Cython/Compiler/PyrexTypes.py
Line 4775 in 5805f4c
def specialize_here(self, pos, env, template_values=None): |
Both cases
import typing
def main(a: typing.Optional[Bar]):
pass
and
import typing
def main(a: typing.Optional[None]):
pass
contains value [None]
in variable template_values
.
Hence, when I want to avoid introducing NoneType
we need to introduce major structural changes how annotation is evaluated at least in following function (which feeds specilize_here()
:
cython/Cython/Compiler/Nodes.py
Line 1254 in 5805f4c
def _analyse_template_types(self, env, base_type): |
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.
we need to introduce major structural changes how annotation is evaluated at least in following function
Or implement the PR in totally different way, but in that case I have no idea how...
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.
this code gets literal
None
value for both cases:* `Union[int, None]` * `Union[int, DummyType]`
Then we need to make both distinguishable. A way to do that is to introduce NoneType
as type and replace a literal None
in type annotations with it.
I'm unsure whether that also applies to the func() -> None
return type case, where the actual meaning is "has no return value" rather than "returns the constant value None". But it definitely applies to type declarations like the above.
Is there a reason, BTW, why we cannot represent Union[xyz, None]
as Optional[xyz]
, which we already support?
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.
Then we need to make both distinguishable. A way to do that is to introduce NoneType as type and replace a literal None in type annotations with it.
Yes, I think this PR is doing pretty that.
I'm unsure whether that also applies to the func() -> None return type case, where the actual meaning is "has no return value" rather than "returns the constant value None". But it definitely applies to type declarations like the above.
I am not sure if it is important in context of Cython. In this PR Cython generates function which returns PyObject *
value which is correct (I suppose)
Is there a reason, BTW, why we cannot represent Union[xyz, None] as Optional[xyz], which we already support?
Honestly, the only reason is to more complete support of PEP 484.
One observation from investigating the none-type bool issue. The following is currently an error on master:
I suspect that changes on with this PR. I'm not yet quite sure exactly how it should change but it's probably worth considering. |
So the specific failure that
That's obviously intended to be a none test, but is being interpreted as "if array is the type given by None". I think the easiest fix is to change
so it only gets evaluated like this in annotation typing sort of environments.
I retract this comment based on what @scoder says, because it doesn't really match |
Yes indeed this PR allows @cython.cfunc
def f() -> int:
return I checked the generated C code and the generated function returns @cython.cfunc
def f() -> None:
return But this is easy fix we need to change following line: cython/Cython/Compiler/Nodes.py Line 6917 in b086ed9
to if return_type is None: (or maybe the fix from comment #6333 (comment) will fix it - not sure right now) |
It seems to work. The only side-effect is that we are returning different error for following case: def union_pytypes(Union[int, None] i):
pass Will error with Where by def union_pytypes(Optional[int] i):
pass Will error with Not huge problem since |
I'd say "only" rather than "mainly". It feels wrong to allow them as C style type declarations at all. |
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.
I'm still not convinced that we need to add NoneType
as a type. The allows_none()
method could just return true for Optional
and otherwise run through the template arguments to see if any of the nodes has node.is_none
. Isn't that enough?
Regarding the general approach,
Or the other way round, transforming I'm not saying that we should do this since I can't say what overall effects this has. But I wanted to spell it out as a possible alternative since we already kind-of mentioned it above. |
At runtime, |
this PR is already doing it - see: The core idea of this PR is to
|
Co-authored-by: scoder <stefan_ml@behnel.de>
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.
Thanks for pointing me to the Union -> Optional
transformation. I had missed that.
while modifier_node.is_subscript: | ||
modifier_type = modifier_node.base.analyse_as_type(env) | ||
if (modifier_type and modifier_type.python_type_constructor_name | ||
and modifier_type.modifier_name): | ||
modifiers.append(modifier_type.modifier_name) | ||
modifiers.append('typing.Optional' if modifier_type.allows_none() else modifier_type.modifier_name) |
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.
What happens if a user writes Optional[Union[int, None]]
? We'd probably get a duplicate Optional
in that case, right? Although I'd guess that that won't even hurt us…
Co-authored-by: scoder <stefan_ml@behnel.de>
This still does not solve the problem that cython internally uses literal
Without
which will be used in case cython cannot find a type inside the annotation. Not sure what is better solution. |
I don't think this is right. I think it only uses the
I think this suggestion is saying: In
You could just say |
OK my apologies! Now I got the point of @scoder after reading hint by @da-woods ! It's a huge facepalm for me that I was not able to see it before! It simplified the code by a factor too! I checked locally the tests and the changes seemed to be OK. I will wait what CI will tell. Big thank you to @da-woods in the meanwhile... |
Thanks, yes, that makes the implementation much shorter. |
This PR adds support for alternative definition of optional [1] type.
Partially implements #6254
[1] https://docs.python.org/3/library/typing.html#typing.Optional