-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add PyiWriter to Cython and new Cython directive 'write_stub_file' #5744
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
Conversation
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've left a few comments based on a very quick skim through, but this obviously needs a detailed look.
It'll also need some testing, although I don't know the best way to do that since I don't really understand the pyi
process. Ideally I think we'd just test using some tool that can load pyi files (and which we didn't write...)
Cython/Compiler/TypeStubGenerator.py
Outdated
if node.directives['write_stub_file']: | ||
result = self.write(node, True) | ||
# TODO See if we should or shouldn't report back about our stubfiles being written off.... | ||
print(f"Writing {result}.pyi ...") |
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 can't assume that Cython runs on a version of Python that supports fstrings yet
Cython/Compiler/TypeStubGenerator.py
Outdated
annotation = "" | ||
# TODO Maybe have a flag to check if were currently using | ||
# a class inside of here as an extra check? | ||
if arg.name == "self": |
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 don't necessarily think we should base the behaviour on the argument name - self
is a convention rather than a rule
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 agree, I just don't know how to implement it yet, but I'll soon figure it out.
Cython/Compiler/TypeStubGenerator.py
Outdated
@@ -0,0 +1,485 @@ | |||
from .Compiler import Version | |||
from .Compiler.Nodes import * |
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'd avoid import *
Cython/Compiler/TypeStubGenerator.py
Outdated
return py_name | ||
|
||
|
||
def translate_annotations(node) -> list[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.
We also can't assume that we run on a version of Python that supports type annotations
Cython/Compiler/TypeStubGenerator.py
Outdated
|
||
|
||
|
||
def visit_DefNode(self,node:DefNode): |
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.
Further to my comment about not supporting annotations, I'm also not keen on node : DefNode
- it's absolutely explicit from the function already so it isn't adding useful information.
Right now it's irrelevant (since we can't have it) but it's definitely the type of annotation I'd be keen to avoid in future
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'll be sure to fix all of this then. Thank you for your input
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. Yes, testing is needed here.
There are also a lot of code formatting issues and some spelling mistakes.
Cython/Compiler/TypeStubGenerator.py
Outdated
print("writing file %s.pyi ..." % node.full_module_name) | ||
with open_new_file(os.path.join(node.full_module_name + '.pyi')) as w: |
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.
Wouldn't this write files like pkg1.pkg2.module.pyi
? They should rather be created in the package folders.
Cython/Compiler/TypeStubGenerator.py
Outdated
print("writing file %s.pyi ..." % node.full_module_name) | ||
with open_new_file(os.path.join(node.full_module_name + '.pyi')) as w: | ||
w.write("\n".join(result.lines)) | ||
print("pyi file written") |
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 seems unnecessary. If it didn't fail, it succeeded, obviously.
print("pyi file written") |
Not sure what else I need to add, except for better translations for Pyrex types to python types and a brand new test suite. |
I got the I have tested everything using the Below is a file that I recently did to be sure the PyiWriter works and that all the other underlying visitors work with it without a problem. # cython: language_level = 3
# cython: embedsignature = True
# cython: embedsignature.python = True
# cython: write_stub_file = True
# This function doesn't make sense but I put it here for demonstration purposes only...
def test_data(int a):
if a == 1:
return float(a)
else:
return str(a)
cdef class RoundRobin:
cdef Py_ssize_t current , limit
def __cinit__(self, Py_ssize_t limit):
self.limit = limit
self.current = 0
cpdef Py_ssize_t next(self):
cdef Py_ssize_t result = self.current
self.current += 1
if self.current >= self.limit:
self.current = 0
return result
# This should not be added in with the pyi stub because it's C
cdef Py_ssize_t get_limit(self):
return self.limit # Python stub file generated by Cython 3.0.2
def test_data(a: int): ...
class RoundRobin:
def __init__(self, limit: int): ...
def next(self) -> int: ... |
A good start to the testing would just be to enable the stub generator generally on the Cython test suite (like we do with the annotated html). That wouldn't actually test any of the functionality, but it would confirm that it can process a lot of Cython code without crashing. Changes would probably need to be in runtest.py. I'd probably put TypeStubGenerator.py through something like black to standardize the formatting. It's a lot better now but not perfect. We don't generally do this for existing files, but it's a good idea for new files I think. Again, I haven't have a chance to have a detailed look yet so I'm sure there's more comments, but that bit of testing is definitely a good place to start |
Cython/Compiler/TypeStubGenerator.py
Outdated
from .Nodes import CNameDeclaratorNode | ||
from .ExprNodes import CallNode, NameNode, ImportNode, TupleNode, AttributeNode | ||
from ..CodeWriter import DeclarationWriter | ||
from .ParseTreeTransforms import CythonTransform |
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.
CythonTransform
is in Visitor
I'd probably put TypeStubGenerator.py through something like black to standardize the formatting. It's a lot better now but not perfect. We don't generally do this for existing files, but it's a good idea for new files I think.
We should just run black selectively on all newly added files, as part of the code checks. It has a mode that fails on necessary format changes.
|
Sounds like a great idea actually. That would be a good way to verify that it works. I'll see what I can come up with. |
Did not realize that the imports were accidently deleted but I have that fixed now so this should all work as normal now. |
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.
Partial review... more comments will come when I have time to look further
Cython/Compiler/TypeStubGenerator.py
Outdated
import os | ||
import sys | ||
|
||
cython.declare(PyrexTypes=object, Naming=object, ExprNodes=object, Nodes=object, |
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.
Probably not useful unless you intend this file to be compiled with Cython. I'd think it shouldn't be at this stage - it's not a "core" feature so doesn't really need speeding up. If you do intend it to be compiled then the file needs adding to setup.py
.
Cython/Compiler/TypeStubGenerator.py
Outdated
cython.declare(PyrexTypes=object, Naming=object, ExprNodes=object, Nodes=object, | ||
Options=object, UtilNodes=object, LetNode=object, | ||
LetRefNode=object, TreeFragment=object, EncodedString=object, | ||
error=object, warning=object, copy=object, _unicode=object) |
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.
Note that error
and warning
aren't even imported, so declaring them as object probably isn't helpful (but again, I don't think you need this line)
Cython/Compiler/TypeStubGenerator.py
Outdated
|
||
|
||
# TODO Save this implementation commented out if required.... | ||
if sys.version_info >= (3, 9): |
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 don't assume that the version of Python that Cython is run in is necessarily the same as the version of Python the generated files are used in. Can this go in the stub files instead? (is this supported by stub files?)
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.
A try-import might work in the stub file.
Cython/Compiler/TypeStubGenerator.py
Outdated
def translate_pyrex_type(self, ctype): | ||
# TODO implement Pyrex to cython shadow typehints converter... | ||
|
||
if isinstance(ctype, PyrexTypes.BuiltinObjectType): |
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 ctype.is_builtin_type
Cython/Compiler/TypeStubGenerator.py
Outdated
if isinstance(ctype, PyrexTypes.BuiltinObjectType): | ||
return ctype.py_type_name() | ||
|
||
if isinstance(ctype, PyrexTypes.CVoidType): |
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.
Most of these are defined as global constants in PyrexTypes. This one you can test with
if ctype is c_void_type:
Cython/Compiler/TypeStubGenerator.py
Outdated
if self.translation_table.get(base.name): | ||
return self.translation_table[base.name] | ||
|
||
elif base.name == "object": |
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 suspicious of most of these comparisons to base.name
- again, it should be possible to identify without checking the name (as above)
Cython/Compiler/TypeStubGenerator.py
Outdated
else: | ||
typing_module = "typing_extensions" | ||
|
||
class PyiWriter(CythonTransform, DeclarationWriter): |
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 don't think you use the directives tracking of CythonTransform
. I wonder if this should inherit from TreeVisitor
since I don't think it's intended to change the tree - just to go over it. That'd eliminate any possibility of accidentally changing the tree.
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 also don't know how much of DeclarationWriter
you use - possibly just things like write/indent
. I wonder if it would be worth factoring those out so you can inherit without bringing in all the visit_
functions?
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.
Yeah, DeclarationWriter
deals a lot with Cython's cdef
declarations, so splitting out a Python compatible thing seems like a good idea. It would probably do a bit more than just indentation, maybe not that much more.
Cython/Compiler/TypeStubGenerator.py
Outdated
def __init__(self, context): | ||
super(PyiWriter, self).__init__(context=context) | ||
super(DeclarationWriter, self).__init__() | ||
self.context = context |
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.
Unused probably.
Cython/Compiler/TypeStubGenerator.py
Outdated
if isinstance(decorator, NameNode): | ||
self.endline("%s" % decorator.name) | ||
else: | ||
assert isinstance(decorator, AttributeNode) , "Decorator was not an attribute node..." |
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'd be worried how this interacts with https://peps.python.org/pep-0614/. There's definitely cases where decorators aren't attributes or names now.
I'm not quite sure how decorators mix with stub files though.
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.
Recursion again, if you want to properly serialise the stub files. But yeah, not sure if that's really intended here.
Cython/Compiler/TypeStubGenerator.py
Outdated
|
||
|
||
def annotation_Str(self, annotation): | ||
return annotation.name if hasattr(annotation,"name") and annotation.is_name else annotation.string.unicode_value |
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.
just annotation.is_name
- I don't think you need to do hasattr
and is_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.
Recursion again.
are we ok to merge this pull request or do I still need to write in a unittest for it? |
Cython/Compiler/TypeStubGenerator.py
Outdated
elif base.name == "bint": | ||
return "bool" | ||
|
||
ctype = PyrexTypes.simple_c_type(base.signed, base.longness, base.name) # type: ignore |
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.
Perhaps I'm missing something, but base
is already a type, so I don't understand what this line does
Cython/Compiler/TypeStubGenerator.py
Outdated
): | ||
|
||
# Try checking our table first... | ||
if self.translation_table.get(base.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.
if base.name in self.translation_table
Cython/Compiler/TypeStubGenerator.py
Outdated
# Optimized original code by having there be one function to take | ||
# the place of two of them I could see what Scoder meant when | ||
# said the original pull request needed to be cleaned up... |
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.
# Optimized original code by having there be one function to take | |
# the place of two of them I could see what Scoder meant when | |
# said the original pull request needed to be cleaned up... |
Don't think this comment is needed for future understanding of the code
Cython/Compiler/TypeStubGenerator.py
Outdated
def write_class(self, node, class_name): | ||
self.endline() | ||
self.put("class %s" % class_name) | ||
if getattr(node,"bases",None) and isinstance(node.bases, TupleNode) and node.bases.args: |
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 don't think node.bases
needs a getattr
- it's a child of both PyClassDefNode
and CClassDefNode
Cython/Compiler/TypeStubGenerator.py
Outdated
self.put("class %s" % class_name) | ||
if getattr(node,"bases",None) and isinstance(node.bases, TupleNode) and node.bases.args: | ||
self.put("(") | ||
self.put(",".join([name.name for name in node.bases.args])) |
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.
Not everything in a tuple node will be a name. E.g.:
import module
class MyClass(module.BaseClass):
pass
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.
Recursion by visiting the args nodes would probably solve this.
Cython/Compiler/TypeStubGenerator.py
Outdated
self.endline("):") | ||
else: | ||
self.endline(":") | ||
self.class_func_count = 0 |
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.
Classes can be nested
My view is that we definitely need some testing before we consider merging it. The testing doesn't have to be comprehensive but right now we have no evidence it works at all. Like I said before, I think it would be worth enabling the directive for every I think setting up proper tests for this may be quite hard, and maybe we shouldn't leave it to you. I'd think we want to either look for specific generated code in the pyi files with a regex search, or we'd want to re-parse the pyi files into Cython and look for specific nodes using something similar to |
The other thing to say with testing: if you have files that you've used to test it manually yourself it might still be useful to add them as part of the review process so we can think about how to automate it |
@da-woods Sounds good sorry I didn't get back sooner but sounds like a great plan I will have to figure out a way to start verifying off pyi files since I cannot seem to find any python libraries that specifically check stub files yet but I will keep looking for an implementation that does. I will also find a way to test it as well since I'm currently working on a unittest for it. |
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.
Please run black over TypeStubGenerator.py
to properly format it.
There are way too many getattr()
calls. They usually indicate something being wrong in the logic and nodes turning up in these places that should probably be handled elsewhere.
And much of this can be taken from the AutoDocTransform
, specifically its is_format_python
mode. We'd probably need to extract most of it into a plain class rather than the existing transform, so that it can be reused in a tree visitor.
Cython/Compiler/Pipeline.py
Outdated
@@ -188,6 +188,7 @@ def create_pipeline(context, mode, exclude_classes=()): | |||
_specific_post_parse, | |||
TrackNumpyAttributes(), | |||
InterpretCompilerDirectives(context, context.compiler_directives), | |||
PyiWriter(context), |
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 is this done at this early stage and not at the end of the pipeline? Especially after AdjustDefByDirectives
and AnalyseExpressionsTransform
?
Cython/Compiler/TypeStubGenerator.py
Outdated
else: | ||
typing_module = "typing_extensions" | ||
|
||
class PyiWriter(CythonTransform, DeclarationWriter): |
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.
Yeah, DeclarationWriter
deals a lot with Cython's cdef
declarations, so splitting out a Python compatible thing seems like a good idea. It would probably do a bit more than just indentation, maybe not that much more.
Cython/Compiler/TypeStubGenerator.py
Outdated
self.visitchildren(node) | ||
self.dedent() | ||
|
||
def translate_pyrex_type(self, ctype): |
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 would have expected a difference between argument types and return types regarding the conversion. Are they really always the same?
Cython/Compiler/TypeStubGenerator.py
Outdated
self.put("class %s" % class_name) | ||
if getattr(node,"bases",None) and isinstance(node.bases, TupleNode) and node.bases.args: | ||
self.put("(") | ||
self.put(",".join([name.name for name in node.bases.args])) |
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.
Recursion by visiting the args nodes would probably solve this.
Cython/Compiler/TypeStubGenerator.py
Outdated
if isinstance(decorator, NameNode): | ||
self.endline("%s" % decorator.name) | ||
else: | ||
assert isinstance(decorator, AttributeNode) , "Decorator was not an attribute node..." |
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.
Recursion again, if you want to properly serialise the stub files. But yeah, not sure if that's really intended here.
Cython/Compiler/TypeStubGenerator.py
Outdated
|
||
|
||
def annotation_Str(self, annotation): | ||
return annotation.name if hasattr(annotation,"name") and annotation.is_name else annotation.string.unicode_value |
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.
Recursion again.
Cython/Compiler/TypeStubGenerator.py
Outdated
# TODO Change how init is being handled... | ||
if func_name == '__cinit__': | ||
func_name = '__init__' |
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.
Well, yeah, you can have both __cinit__
and __init__
in the same class.
I think a stub file does end up being a valid Python file. So either putting it into Python or Cython itself would at least show that you've written a valid file. |
Let me make sure I get the file formatted using black real fast... |
cc @dalcinl for vis |
@Vizonex You should test your work against mpi4py
Your code currently crashes Cython:
|
@dalcinl I will be sure to use the repository you mentioned for testing. And I also think some of the visitors in my code should use a little more recursion which is what scoder suggested I do a bit earlier. The attributes themselves I think need better attention than what they have currently been given. |
I had an Idea for writing decorators by making a new class object that could chain attributes together for the decorators but I am not sure how I am going to approach this problem. Here is an example of what the problem is... @very.long.wrapper
def func(): Oh I see there is an example in the Expression Writer class Nevermind.... |
Decorators: https://typing.readthedocs.io/en/latest/source/stubs.html#decorators
To me that says you can be fairly eager to drop anything unrecognised. I'd probably do something like:
|
So should I make the visitor visit call nodes as well because I am also worried about it accidently being mixed with cython directives. But I will figure out if there's already a check inplace for that. |
You should be able to identify cython directives with |
I just looked at it and it seems that there is a better solution. So would something like this be a bit better? def write_decorator(self, decorator):
attribte = decorator.as_cython_attribute()
if attribte:
self.putline("@%s" % attribte) |
Probably
(i.e. you don't want to write out Cython directives) |
I'm gonna be sure I get to fixing |
I think the reason why I stopped trying was because everything lacked readability (I blame VS code's intellisense/pyright tools for that). I may resume my attempts on this project soon but I think that writing stub files or maybe even programming external tools for writing tree visitors and finding a way for to figure out node variables and their different class members might be helpful. I'll probably close this Pull Request soon and come up with a new one in the future. For those depending on me to program stub file generation into cython please don't worry, I haven't given up on this. I've just been busy with other stuff that I completely forgot about this project. |
sagemathgh-39257: Create pyi stub files for cython modules in arith and algebras Add pyi stub files for some cython modules in `arith` and `algebras` to add typing support (and IDE autocompletion) for these cython modules. They are not supposed to be 100% correct but serve at least as a starting point for further refinements. At some point they might be autogenerated by cython, but this could easily still take a few years (see cython/cython#3150 and cython/cython#5744 for the current progress) URL: sagemath#39257 Reported by: Tobias Diez Reviewer(s): Dima Pasechnik
The Demand for such a tool to exist is very high and I wanted to try my very hardest to deliver. Visual Studio Code and some other coding Ides that I may not be aware off suffer from not being able to have immediate type hint access when a compiled python file such as
.so
or.pyd
that are compiled by Cython. My goal was to finish what the author ax487 had started and I have made some optimizations of my own to his compiler as well as added the bonus of type hint completeness to the arguments. I would've added the ability to recover return type hint annotations but we can save that for the future as thisPyiWriter
object matures a bit as I have tried to resolve missing type hint annotations labeled asobject
with no success just yet. However as long as you have provided the necessary type hints needed to your modules that shouldn't be a problem or issue and it should not be overwritten as long as the annotations or declarations are written to the.pyx
,.py
file.New Directive Argument Was Added
I have added in a new cython directive called
cython: write_stub_file
to invoke the PyiWriter to write the stubfiles where the pyx file is being compiled. And here is a neat little example to illustrate this.We will call this file
test.pyx
Using the new
cython: write_stub_file
directive I have created and setting it as True will create the output namedtest.pyi
There maybe a few things missing like an output folder command but I'm pretty sure all of you could help me come up with those missing pieces. I am honored and humbled to be apart of one of the most revolutionary additions ever to Cython as of currently.