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

Skip to content

Commit c00e677

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 c00e677

File tree

4 files changed

+145
-22
lines changed

4 files changed

+145
-22
lines changed

mypy/stubdoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def args_kwargs(signature: FunctionSig) -> bool:
202202
return list(sorted(self.signatures, key=lambda x: 1 if args_kwargs(x) else 0))
203203

204204

205-
def infer_sig_from_docstring(docstr: str, name: str) -> Optional[List[FunctionSig]]:
205+
def infer_sig_from_docstring(docstr: Optional[str], name: str) -> Optional[List[FunctionSig]]:
206206
"""Convert function signature to list of TypedFunctionSig
207207
208208
Look for function signatures of function in docstring. Signature is a string of

mypy/stubgenc.py

Lines changed: 90 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, Callable, cast
1313

1414
from mypy.moduleinspect import is_c_module
1515
from mypy.stubdoc import (
@@ -92,7 +92,9 @@ 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 \
96+
type(obj) is type(ord) or \
97+
type(obj).__name__ == 'cython_function_or_method'
9698

9799

98100
def is_c_method(obj: object) -> bool:
@@ -139,24 +141,12 @@ def generate_c_function_stub(module: ModuleType,
139141
if class_sigs is None:
140142
class_sigs = {}
141143

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)]
144+
inferred = _infer_signature_for_c_function_stub(
145+
class_name=class_name,
146+
class_sigs=class_sigs,
147+
name=name,
148+
obj=obj,
149+
sigs=sigs)
160150

161151
is_overloaded = len(inferred) > 1 if inferred else False
162152
if is_overloaded:
@@ -189,6 +179,86 @@ def generate_c_function_stub(module: ModuleType,
189179
))
190180

191181

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

mypy/test/teststubgen.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
import os.path
33
import shutil
4+
import subprocess
45
import sys
56
import tempfile
67
import re
@@ -19,7 +20,9 @@
1920
mypy_options, is_blacklisted_path, is_non_library_module
2021
)
2122
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
23+
from mypy.stubgenc import (
24+
generate_c_type_stub, infer_method_sig, generate_c_function_stub, generate_stub_for_c_module
25+
)
2326
from mypy.stubdoc import (
2427
parse_signature, parse_all_signatures, build_signature, find_unique_signatures,
2528
infer_sig_from_docstring, infer_prop_type_from_docstring, FunctionSig, ArgSig,
@@ -183,6 +186,8 @@ def test_find_unique_signatures(self) -> None:
183186
('func3', '(arg, arg2)')])
184187

185188
def test_infer_sig_from_docstring(self) -> None:
189+
assert_equal(infer_sig_from_docstring(None, 'func'), None)
190+
186191
assert_equal(infer_sig_from_docstring('\nfunc(x) - y', 'func'),
187192
[FunctionSig(name='func', args=[ArgSig(name='x')], ret_type='Any')])
188193

@@ -804,6 +809,53 @@ def __init__(self, arg0: str) -> None:
804809
'def __init__(*args, **kwargs) -> Any: ...'])
805810
assert_equal(set(imports), {'from typing import overload'})
806811

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

808860
class ArgSigSuite(unittest.TestCase):
809861
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 1241
setuptools
1717
importlib-metadata==0.20
18+
Cython

0 commit comments

Comments
 (0)
0