Add PyiWriter to Cython and new Cython directive 'write_stub_file'#5744
Add PyiWriter to Cython and new Cython directive 'write_stub_file'#5744Vizonex wants to merge 0 commit intocython:masterfrom Vizonex:master
Conversation
da-woods
left a comment
There was a problem hiding this comment.
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.
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.
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.
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 * | |||
Cython/Compiler/TypeStubGenerator.py
Outdated
| return py_name | ||
|
|
||
|
|
||
| def translate_annotations(node) -> list[str]: |
There was a problem hiding this comment.
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.
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.
I'll be sure to fix all of this then. Thank you for your input
scoder
left a comment
There was a problem hiding this comment.
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.
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.
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.
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. |
da-woods
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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): |
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.
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.
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.
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.
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.
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 |
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.
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.
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.
just annotation.is_name - I don't think you need to do hasattr and is_name
|
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.
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.
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.
| # 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.
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.
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.
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 |
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. |
scoder
left a comment
There was a problem hiding this comment.
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
| _specific_post_parse, | ||
| TrackNumpyAttributes(), | ||
| InterpretCompilerDirectives(context, context.compiler_directives), | ||
| PyiWriter(context), |
There was a problem hiding this comment.
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.
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.
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.
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.
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 |
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.
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
.soor.pydthat 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 thisPyiWriterobject matures a bit as I have tried to resolve missing type hint annotations labeled asobjectwith 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,.pyfile.New Directive Argument Was Added
I have added in a new cython directive called
cython: write_stub_fileto 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.pyxUsing the new
cython: write_stub_filedirective I have created and setting it as True will create the output namedtest.pyiThere 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.