Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/numpy/testing/_private/extbuild.py: 17%

90 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-23 06:43 +0000

1""" 

2Build a c-extension module on-the-fly in tests. 

3See build_and_import_extensions for usage hints 

4 

5""" 

6 

7import os 

8import pathlib 

9import subprocess 

10import sys 

11import sysconfig 

12import textwrap 

13 

14__all__ = ['build_and_import_extension', 'compile_extension_module'] 

15 

16 

17def build_and_import_extension( 

18 modname, functions, *, prologue="", build_dir=None, 

19 include_dirs=[], more_init=""): 

20 """ 

21 Build and imports a c-extension module `modname` from a list of function 

22 fragments `functions`. 

23 

24 

25 Parameters 

26 ---------- 

27 functions : list of fragments 

28 Each fragment is a sequence of func_name, calling convention, snippet. 

29 prologue : string 

30 Code to precede the rest, usually extra ``#include`` or ``#define`` 

31 macros. 

32 build_dir : pathlib.Path 

33 Where to build the module, usually a temporary directory 

34 include_dirs : list 

35 Extra directories to find include files when compiling 

36 more_init : string 

37 Code to appear in the module PyMODINIT_FUNC 

38 

39 Returns 

40 ------- 

41 out: module 

42 The module will have been loaded and is ready for use 

43 

44 Examples 

45 -------- 

46 >>> functions = [("test_bytes", "METH_O", \"\"\" 

47 if ( !PyBytesCheck(args)) { 

48 Py_RETURN_FALSE; 

49 } 

50 Py_RETURN_TRUE; 

51 \"\"\")] 

52 >>> mod = build_and_import_extension("testme", functions) 

53 >>> assert not mod.test_bytes(u'abc') 

54 >>> assert mod.test_bytes(b'abc') 

55 """ 

56 body = prologue + _make_methods(functions, modname) 

57 init = """PyObject *mod = PyModule_Create(&moduledef); 

58 """ 

59 if not build_dir: 

60 build_dir = pathlib.Path('.') 

61 if more_init: 

62 init += """#define INITERROR return NULL 

63 """ 

64 init += more_init 

65 init += "\nreturn mod;" 

66 source_string = _make_source(modname, init, body) 

67 try: 

68 mod_so = compile_extension_module( 

69 modname, build_dir, include_dirs, source_string) 

70 except Exception as e: 

71 # shorten the exception chain 

72 raise RuntimeError(f"could not compile in {build_dir}:") from e 

73 import importlib.util 

74 spec = importlib.util.spec_from_file_location(modname, mod_so) 

75 foo = importlib.util.module_from_spec(spec) 

76 spec.loader.exec_module(foo) 

77 return foo 

78 

79 

80def compile_extension_module( 

81 name, builddir, include_dirs, 

82 source_string, libraries=[], library_dirs=[]): 

83 """ 

84 Build an extension module and return the filename of the resulting 

85 native code file. 

86 

87 Parameters 

88 ---------- 

89 name : string 

90 name of the module, possibly including dots if it is a module inside a 

91 package. 

92 builddir : pathlib.Path 

93 Where to build the module, usually a temporary directory 

94 include_dirs : list 

95 Extra directories to find include files when compiling 

96 libraries : list 

97 Libraries to link into the extension module 

98 library_dirs: list 

99 Where to find the libraries, ``-L`` passed to the linker 

100 """ 

101 modname = name.split('.')[-1] 

102 dirname = builddir / name 

103 dirname.mkdir(exist_ok=True) 

104 cfile = _convert_str_to_file(source_string, dirname) 

105 include_dirs = include_dirs + [sysconfig.get_config_var('INCLUDEPY')] 

106 

107 return _c_compile( 

108 cfile, outputfilename=dirname / modname, 

109 include_dirs=include_dirs, libraries=[], library_dirs=[], 

110 ) 

111 

112 

113def _convert_str_to_file(source, dirname): 

114 """Helper function to create a file ``source.c`` in `dirname` that contains 

115 the string in `source`. Returns the file name 

116 """ 

117 filename = dirname / 'source.c' 

118 with filename.open('w') as f: 

119 f.write(str(source)) 

120 return filename 

121 

122 

123def _make_methods(functions, modname): 

124 """ Turns the name, signature, code in functions into complete functions 

125 and lists them in a methods_table. Then turns the methods_table into a 

126 ``PyMethodDef`` structure and returns the resulting code fragment ready 

127 for compilation 

128 """ 

129 methods_table = [] 

130 codes = [] 

131 for funcname, flags, code in functions: 

132 cfuncname = "%s_%s" % (modname, funcname) 

133 if 'METH_KEYWORDS' in flags: 

134 signature = '(PyObject *self, PyObject *args, PyObject *kwargs)' 

135 else: 

136 signature = '(PyObject *self, PyObject *args)' 

137 methods_table.append( 

138 "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags)) 

139 func_code = """ 

140 static PyObject* {cfuncname}{signature} 

141 {{ 

142 {code} 

143 }} 

144 """.format(cfuncname=cfuncname, signature=signature, code=code) 

145 codes.append(func_code) 

146 

147 body = "\n".join(codes) + """ 

148 static PyMethodDef methods[] = { 

149 %(methods)s 

150 { NULL } 

151 }; 

152 static struct PyModuleDef moduledef = { 

153 PyModuleDef_HEAD_INIT, 

154 "%(modname)s", /* m_name */ 

155 NULL, /* m_doc */ 

156 -1, /* m_size */ 

157 methods, /* m_methods */ 

158 }; 

159 """ % dict(methods='\n'.join(methods_table), modname=modname) 

160 return body 

161 

162 

163def _make_source(name, init, body): 

164 """ Combines the code fragments into source code ready to be compiled 

165 """ 

166 code = """ 

167 #include <Python.h> 

168 

169 %(body)s 

170 

171 PyMODINIT_FUNC 

172 PyInit_%(name)s(void) { 

173 %(init)s 

174 } 

175 """ % dict( 

176 name=name, init=init, body=body, 

177 ) 

178 return code 

179 

180 

181def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], 

182 library_dirs=[]): 

183 if sys.platform == 'win32': 

184 compile_extra = ["/we4013"] 

185 link_extra = ["/LIBPATH:" + os.path.join(sys.base_prefix, 'libs')] 

186 elif sys.platform.startswith('linux'): 

187 compile_extra = [ 

188 "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"] 

189 link_extra = [] 

190 else: 

191 compile_extra = link_extra = [] 

192 pass 

193 if sys.platform == 'win32': 

194 link_extra = link_extra + ['/DEBUG'] # generate .pdb file 

195 if sys.platform == 'darwin': 

196 # support Fink & Darwinports 

197 for s in ('/sw/', '/opt/local/'): 

198 if (s + 'include' not in include_dirs 

199 and os.path.exists(s + 'include')): 

200 include_dirs.append(s + 'include') 

201 if s + 'lib' not in library_dirs and os.path.exists(s + 'lib'): 

202 library_dirs.append(s + 'lib') 

203 

204 outputfilename = outputfilename.with_suffix(get_so_suffix()) 

205 build( 

206 cfile, outputfilename, 

207 compile_extra, link_extra, 

208 include_dirs, libraries, library_dirs) 

209 return outputfilename 

210 

211 

212def build(cfile, outputfilename, compile_extra, link_extra, 

213 include_dirs, libraries, library_dirs): 

214 "use meson to build" 

215 

216 build_dir = cfile.parent / "build" 

217 os.makedirs(build_dir, exist_ok=True) 

218 so_name = outputfilename.parts[-1] 

219 with open(cfile.parent / "meson.build", "wt") as fid: 

220 includes = ['-I' + d for d in include_dirs] 

221 link_dirs = ['-L' + d for d in library_dirs] 

222 fid.write(textwrap.dedent(f"""\ 

223 project('foo', 'c') 

224 shared_module('{so_name}', '{cfile.parts[-1]}', 

225 c_args: {includes} + {compile_extra}, 

226 link_args: {link_dirs} + {link_extra}, 

227 link_with: {libraries}, 

228 name_prefix: '', 

229 name_suffix: 'dummy', 

230 ) 

231 """)) 

232 if sys.platform == "win32": 

233 subprocess.check_call(["meson", "setup", 

234 "--buildtype=release", 

235 "--vsenv", ".."], 

236 cwd=build_dir, 

237 ) 

238 else: 

239 subprocess.check_call(["meson", "setup", "--vsenv", ".."], 

240 cwd=build_dir 

241 ) 

242 subprocess.check_call(["meson", "compile"], cwd=build_dir) 

243 os.rename(str(build_dir / so_name) + ".dummy", cfile.parent / so_name) 

244 

245def get_so_suffix(): 

246 ret = sysconfig.get_config_var('EXT_SUFFIX') 

247 assert ret 

248 return ret