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
« 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
5"""
7import os
8import pathlib
9import subprocess
10import sys
11import sysconfig
12import textwrap
14__all__ = ['build_and_import_extension', 'compile_extension_module']
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`.
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
39 Returns
40 -------
41 out: module
42 The module will have been loaded and is ready for use
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
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.
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')]
107 return _c_compile(
108 cfile, outputfilename=dirname / modname,
109 include_dirs=include_dirs, libraries=[], library_dirs=[],
110 )
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
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)
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
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>
169 %(body)s
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
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')
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
212def build(cfile, outputfilename, compile_extra, link_extra,
213 include_dirs, libraries, library_dirs):
214 "use meson to build"
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)
245def get_so_suffix():
246 ret = sysconfig.get_config_var('EXT_SUFFIX')
247 assert ret
248 return ret