-
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
Changes from all commits
76c2017
72d4462
5757f9b
af9a7fd
bef28b4
e3dbc42
9dd9bed
69be64e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
# IDE | ||
.vscode/ | ||
.idea/ | ||
|
||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from pathlib import Path | ||
|
||
from geoalchemy2._functions_helpers import _generate_stubs | ||
|
||
""" | ||
this script is outside the geoalchemy2 package because the 'geoalchemy2.types' | ||
package interferes with the 'types' module in the standard library | ||
""" | ||
|
||
script_dir = Path(__file__).resolve().parent | ||
|
||
|
||
if __name__ == "__main__": | ||
(script_dir / "geoalchemy2/functions.pyi").write_text(_generate_stubs()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
from textwrap import TextWrapper | ||
from typing import Optional | ||
from typing import Tuple | ||
from typing import Union | ||
|
||
|
||
def _wrap_docstring(docstring: str) -> str: | ||
wrapper = TextWrapper(width=100) | ||
lines = [] | ||
for long_line in docstring.splitlines(keepends=False): | ||
lines.extend(wrapper.wrap(long_line)) | ||
return "\n".join(lines) | ||
|
||
|
||
def _get_docstring(name: str, doc: Union[None, str, Tuple[str, str]], type_: Optional[type]) -> str: | ||
doc_string_parts = [] | ||
|
||
if isinstance(doc, tuple): | ||
doc_string_parts.append(_wrap_docstring(doc[0])) | ||
doc_string_parts.append("see https://postgis.net/docs/{0}.html".format(doc[1])) | ||
elif doc is not None: | ||
doc_string_parts.append(_wrap_docstring(doc)) | ||
doc_string_parts.append("see https://postgis.net/docs/{0}.html".format(name)) | ||
|
||
if type_ is not None: | ||
return_type_str = "{0}.{1}".format(type_.__module__, type_.__name__) | ||
doc_string_parts.append("Return type: :class:`{0}`.".format(return_type_str)) | ||
|
||
return "\n\n".join(doc_string_parts) | ||
|
||
|
||
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) | ||
Comment on lines
+32
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about also formatting the docstrings using There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
|
||
def _generate_stubs() -> str: | ||
"""Generates type stubs for the dynamic functions described in `geoalchemy2/_functions.py`.""" | ||
from geoalchemy2._functions import _FUNCTIONS | ||
from geoalchemy2.functions import ST_AsGeoJSON | ||
|
||
header = '''\ | ||
# this file is automatically generated | ||
from typing import Any | ||
from typing import List | ||
|
||
from sqlalchemy.sql import functions | ||
from sqlalchemy.sql.elements import ColumnElement | ||
|
||
import geoalchemy2.types | ||
|
||
class GenericFunction(functions.GenericFunction): ... | ||
|
||
class TableRowElement(ColumnElement): | ||
inherit_cache: bool = ... | ||
"""The cache is disabled for this class.""" | ||
|
||
def __init__(self, selectable: bool) -> None: ... | ||
@property | ||
def _from_objects(self) -> List[bool]: ... # type: ignore[override] | ||
''' | ||
stub_file_parts = [header] | ||
|
||
functions = _FUNCTIONS.copy() | ||
functions.insert(0, ("ST_AsGeoJSON", str, ST_AsGeoJSON.__doc__)) | ||
|
||
for name, type_, doc_parts in functions: | ||
doc = _replace_indent(_get_docstring(name, doc_parts, type_), " ") | ||
|
||
if type_ is None: | ||
type_str = "Any" | ||
elif type_.__module__ == "builtins": | ||
type_str = type_.__name__ | ||
else: | ||
type_str = f"{type_.__module__}.{type_.__name__}" | ||
|
||
signature = f'''\ | ||
class _{name}(functions.GenericFunction): | ||
""" | ||
{doc} | ||
""" | ||
|
||
def __call__(self, *args: Any, **kwargs: Any) -> {type_str}: ... | ||
|
||
{name}: _{name} | ||
''' | ||
stub_file_parts.append(signature) | ||
|
||
return "\n".join(stub_file_parts) |
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! 🤩