-
Notifications
You must be signed in to change notification settings - Fork 112
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
Feat: Added type stub generation for dynamic functions #478
Conversation
I saw that |
bdaf3b6
to
a6d70bc
Compare
Great work @mbway , thank you very much !
The return types in |
It should do, I can't see anything I've used that wouldn't be in 3.7. Except that I did miss one instance where I used
if that information were accessible in a structured way that could be encoded using
I used |
Ok, great!
I tried to change the return type of from sqlalchemy import Integer
Integer().python_type unfortunately we have to instantiate the type to get the python_type because it's a property. So I'm not sure we can use it in our case. |
Any idea why the tests are failing? |
Because of
but I don't know how to fix this :) Maybe because mypy is not able to see that |
Or maybe because of this? (in try:
# SQLAlchemy >= 2
from sqlalchemy.sql._typing import _TypeEngineArgument
except ImportError:
# SQLAlchemy < 2
_TypeEngineArgument = Any # type: ignore |
So are the failures mypy errors unrelated to my changes? I can take a look. I thought it type checked the whole package OK for me when I ran it locally. |
When I run it locally I get the same error as in the CI and I don't get any error using the master branch, so it seems related to this PR. It's probably something about custom types but I don't have time to look deeper into this for now. |
the test failures seem to be unrelated typing issues, possibly uncovered by introducing |
I noticed that in my test container pypy took a long time to complete the tests and it seems that is also the case on the CI. It's strange that the reason for using pypy is faster speeds but it runs these tests 6 times slower |
I still suspect we missed something in the custom type annotations. But as far as I'm concerned we can merge, it's already a very nice improvement. Just tell me if you to investigate further or if you are ok to merge.
Yeah, I noticed that too. And sometimes the Pypy jobs in CI are failing because of a segmentation fault, which I was never able to reproduce. That's why I tried to use Conda to setup Pypy but it didn't solve this issue, so I don't know what to do... |
Like what? The tests are passing 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.
It looks very good to me!
I just had 2 ideas related to this work but it can go in another PR if you prefer, WDYT?
def _replace_indent(text: str, indent: str) -> str: | ||
lines = [] | ||
for i, line in enumerate(text.splitlines()): | ||
if i == 0 or not line.strip(): | ||
lines.append(line) | ||
else: | ||
lines.append(f"{indent}{line}") | ||
return "\n".join(lines) |
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 about also formatting the docstrings using textwrap
? So the docstrings of these functions are formatted like the others (i.e. line length = 100).
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 have made the change to wrap the text. The wrapping is done before the indenting so it's wrapped to 104 columns but this can be fixed if it matters.
sure. I'll make these changes now |
@@ -4,16 +4,22 @@ | |||
from typing import Union | |||
|
|||
|
|||
def _wrap_docstring(docstring: str) -> 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.
this handles wrapping plus the explicit newlines in the docstrings
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.
nice! 🤩
Thank you very much @mbway for this brilliant PR!!! |
Oh I had a last question, do you think we could solve #396 using |
I'm not 100% sure what #396 is referring to. If it is only a typing issue where you want the type checker to understand that ST_Transform(..., type_=Raster) returns a raster and without And if you want ST_Intersection(geom, geom) to be allowed (type checks) but not ST_Intersection(geom, geom, geom) (for example), then that can also be done with but in both cases this is only information for the type checker and not at runtime. It may be possible to inspect overloaded signatures at runtime but it would be unusual to do so. |
I just realize something: shouldn't we also add |
The point of #396 is also to make SQLAlchemy return the proper type, so if we write |
Description
From the discussion in Fixes: #476, I have added a script to automatically generate type stubs for
geoalchemy2.functions
I made quite a few false starts but now the functions are type hinted as being instances of private classes (one class for each function) that inherits from
GenericFunction
and provides a__call__
method according to the function signature described in_FUNCTIONS
. This allows type-safe access to the attributes likename
as well as being able to call the functions and infer the return types:gives
In future, it would be simple to add function overloads like so:
The Journey I took to get the final result
Initially I generated regular function signatures for the dynamic
ST_*
functions, but then realised that there are additional attributes attached to these functions such asname
.I then took a look at typeshed because the stubs for
zip
are a class with many overloaded__new__
methods so I gave that a go. However in the case ofzip
the return values from__new__
are instances of thezip
class so that technique is not applicable for the case of theST_*
functions.I then looked at this issue which describes a workaround for providing attributes to a callable. The issue was using
Protocol
but luckily it also seems to work with other base classes such assqlalchemy.sql.functions.GenericFunction
. I tried this approach first that does not requireParamSpec
(which was only added in python 3.10) and it does work, but if in future anyone wants to add specific overloads, for exampleST_3DUnion
then PyCharm and mypy are able to 'see through' the
_generic_function
decorator (when using theParamSpec
approach) and provide feedback on both the function arguments and the attributes provided bysqlalchemy.sql.functions.GenericFunction
, although PyCharm support is patchy. MyPy seems to understand everything though at least.gives:
The issue with doing the seemingly simpler approach of
is that for the above, mypy prints:
because it thinks that
ST_Polygon
is a class and__call__
applies to instances of the class instead of the class itself (which is why I tried using__new__
initially). Decorating__call__
with@staticmethod
or@classmethod
does not seem to work.edit: I realised that the difference with
@_generic_function
is that it was casting the function to be an instance of the protocol class instead of the class itself, and that can be done without a decorator (which doesn't matter in this case as the code is auto-generated anyway so extra boilerplate is not an issue).With this change the code is simpler although slightly more verbose and should work well with overloading if there is a desire to add it
Checklist
This pull request is:
Fixes: #<issue number>
in the description if it solves an existing issue(which must include a complete example of the issue).
main
branch and pass with the provided fix.Fixes: #<issue number>
in the description if it solves an existing issue(which must include a complete example of the feature).