8000 Adding minimal support for Cython functions · python/mypy@139c80f · GitHub
[go: up one dir, main page]

Skip to content

Commit 139c80f

Browse files
committed
Adding minimal support for Cython functions
Minimal support for Cython functions in stubgen for generating .pyi files from a C module. Based on #7542.
1 parent f4351ba commit 139c80f

File tree

3 files changed

+133
-21
lines changed

3 files changed

+133
-21
lines changed

mypy/stubgenc.py

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import inspect
99
import os.path
1010
import re
11-
from typing import List, Dict, Tuple, Optional, Mapping, Any, Set
1211
from types import ModuleType
12+
from typing import List, Dict, Tuple, Optional, Mapping, Any, Set, ClassVar, Union
1313

1414
from mypy.moduleinspect import is_c_module
1515
from mypy.stubdoc import (
@@ -92,7 +92,7 @@ def add_typing_import(output: List[str]) -> List[str]:
9292

9393

9494
def is_c_function(obj: object) -> bool:
95-
return inspect.isbuiltin(obj) or type(obj) is type(ord)
95+
return inspect.isbuiltin(obj) or type(obj) is type(ord) or type(obj).__name__ == 'cython_function_or_method'
9696

9797

9898
def is_c_method(obj: object) -> bool:
@@ -139,24 +139,12 @@ def generate_c_function_stub(module: ModuleType,
139139
if class_sigs is None:
140140
class_sigs = {}
141141

142-
ret_type = 'None' if name == '__init__' and class_name else 'Any'
143-
144-
if (name in ('__new__', '__init__') and name not in sigs and class_name and
145-
class_name in class_sigs):
146-
inferred = [FunctionSig(name=name,
147-
args=infer_arg_sig_from_docstring(class_sigs[class_name]),
148-
ret_type=ret_type)] # type: Optional[List[FunctionSig]]
149-
else:
150-
docstr = getattr(obj, '__doc__', None)
151-
inferred = infer_sig_from_docstring(docstr, name)
152-
if not inferred:
153-
if class_name and name not in sigs:
154-
inferred = [FunctionSig(name, args=infer_method_sig(name), ret_type=ret_type)]
155-
else:
156-
inferred = [FunctionSig(name=name,
157-
args=infer_arg_sig_from_docstring(
158-
sigs.get(name, '(*args, **kwargs)')),
159-
ret_type=ret_type)]
142+
inferred = _infer_signature_for_c_function_stub(
143+
class_name=class_name,
144+
class_sigs=class_sigs,
145+
name=name,
146+
obj=obj,
147+
sigs=sigs)
160148

161149
is_overloaded = len(inferred) > 1 if inferred else False
162150
if is_overloaded:
@@ -189,6 +177,82 @@ def generate_c_function_stub(module: ModuleType,
189177
))
190178

191179

180+
def _infer_signature_for_c_function_stub(
181+
class_name: Optional[str],
182+
class_sigs: Optional[Dict[str, str]],
183+
name: str,
184+
obj: object,
185+
sigs: Optional[Dict[str, str]]) -> List[FunctionSig]:
186+
default_ret_type = 'None' if name == '__init__' and class_name else 'Any'
187+
188+
if type(obj).__name__ == 'cython_function_or_method':
189+
# Special-case Cython functions: if binding=True when compiling a Cython binary, it generates Python annotations
190+
# sufficient to use inspect#signature.
191+
sig = _infer_signature_via_inspect(obj=obj, default_ret_type=default_ret_type)
192+
if sig is not None:
193+
return [sig]
194+
# Fall through to parse via doc if inspect.signature() didn't work
195+
196+
if (name in ('__new__', '__init__') and name not in sigs and class_name and
197+
class_name in class_sigs):
198+
return [FunctionSig(name=name,
199+
args=infer_arg_sig_from_docstring(class_sigs[class_name]),
200+
ret_type=default_ret_type)] # type: Optional[List[FunctionSig]]
201+
202+
docstr = getattr(obj, '__doc__', None)
203+
inferred = infer_sig_from_docstring(docstr, name)
204+
if inferred:
205+
return inferred
206+
207+
if class_name and name not in sigs:
208+
return [FunctionSig(name, args=infer_method_sig(name), ret_type=default_ret_type)]
209+
else:
210+
return [FunctionSig(name=name,
211+
args=infer_arg_sig_from_docstring(
212+
sigs.get(name, '(*args, **kwargs)')),
213+
ret_type=default_ret_type)]
214+
215+
216+
def _infer_signature_via_inspect(obj: object, default_ret_type: str) -> Optional[FunctionSig]:
217+
"""
218+
Parses a FunctionSig via annotations found in inspect#signature(). Returns None if inspect.signature() failed to
219+
generate a signature.
220+
"""
221+
222+
try:
223+< 9E88 /span>
signature = inspect.signature(obj)
224+
except (ValueError, TypeError):
225+
# inspect.signature() failed to generate a signature; this can happen for some methods depending on the
226+
# implementation of Python, or if a cython function was not compiled with binding=True.
227+
return None
228+
args = []
229+
230+
def annotation_to_name(annotation) -> Optional[str]:
231+
if annotation == inspect.Signature.empty:
232+
return None
233+
if isinstance(annotation, str):
234+
return annotation
235+
if inspect.isclass(annotation):
236+
return annotation.__name__
237+
if hasattr(annotation, '__str__'):
238+
return annotation.__str__()
239+
# Can't do anything here, so ignore
240+
return None
241+
242+
for arg_param in signature.parameters.values():
243+
args.append(ArgSig(
244+
name=arg_param.name,
245+
type=annotation_to_name(arg_param.annotation),
246+
default=arg_param.default != inspect.Parameter.empty,
247+
))
248+
ret_type = annotation_to_name(signature.return_annotation) or default_ret_type
249+
return FunctionSig(
250+
name=obj.__name__,
251+
args=args,
252+
ret_type=ret_type,
253+
)
254+
255+
192256
def strip_or_import(typ: str, module: ModuleType, imports: List[str]) -> str:
193257
"""Strips unnecessary module names from typ.
194258

mypy/test/teststubgen.py

Lines changed: 48 additions & 1 deletion
817
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import glob
12
import io
23
import os.path
34
import shutil
5+
import subprocess
46
import sys
57
import tempfile
68
import re
@@ -19,7 +21,7 @@
1921
mypy_options, is_blacklisted_path, is_non_library_module
2022
)
2123
from mypy.stubutil import walk_packages, remove_misplaced_type_comments, common_dir_prefix
22-
from mypy.stubgenc import generate_c_type_stub, infer_method_sig, generate_c_function_stub
24+
from mypy.stubgenc import generate_c_type_stub, infer_method_sig, generate_c_function_stub, generate_stub_for_c_module
2325
from mypy.stubdoc import (
2426
parse_signature, parse_all_signatures, build_signature, find_unique_signatures,
2527
infer_sig_from_docstring, infer_prop_type_from_docstring, FunctionSig, ArgSig,
@@ -804,6 +806,51 @@ def __init__(self, arg0: str) -> None:
804806
'def __init__(*args, **kwargs) -> Any: ...'])
805807
assert_equal(set(imports), {'from typing import overload'})
806808

809+
def test_cython(self):
810+
pyx_source = """
811+
#cython: binding=True
812+
813+
import typing
814+
815+
def f(path: str, a: int = 0, b: bool = True) -> typing.List[str]:
816+
return []
+
818+
cdef class MyClass(object):
819+
def run(self, action: str) -> None:
820+
pass
821+
"""
822+
823+
expected_pyi_snippets = [
824+
"""
825+
class MyClass:
826+
@classmethod
827+
def __init__(self, *args, **kwargs) -> None: ...
828+
def run(self, action: str) -> None: ...
829+
""",
830+
"""
831+
def f(path: str, a: int = ..., b: bool = ...) -> typing.List[str]: ...
832+
"""
833+
]
834+
835+
package_name = 'cython_test'
836+
with tempfile.TemporaryDirectory() as tmpdir:
837+
package_dir = os.path.join(tmpdir, package_name)
838+
os.mkdir(package_dir)
839+
pyx = os.path.join(package_dir, f'{package_name}.pyx')
840+
with open(pyx, 'w') as pyx_f:
841+
pyx_f.write(pyx_source)
842+
subprocess.check_output([
843+
'cythonize', '-a', '-i', pyx
844+
])
845+
846+
os.chdir(tmpdir)
847+
outfile = os.path.join(tmpdir, 'out')
848+
generate_stub_for_c_module(f'{package_name}.{package_name}', outfile)
849+
with open(outfile, 'r') as outfile_f:
850+
outfile_txt = outfile_f.read()
851+
for snippet in expected_pyi_snippets:
852+
assert snippet.strip() in outfile_txt, snippet
853+
807854

808855
class ArgSigSuite(unittest.TestCase):
809856
def test_repr(self) -> None:

test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ py>=1.5.2
1515
virtualenv<20
1616
setuptools
1717
importlib-metadata==0.20
18+
Cython --install-option="--no-cython-compile"

0 commit comments

Comments
 (0)
0