-
Notifications
You must be signed in to change notification settings - Fork 3
/
cysetuptools.py
408 lines (318 loc) · 13.6 KB
/
cysetuptools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
import subprocess
import os
import os.path as op
import sys
import shlex
import argparse
import setuptools
PY3 = sys.version_info[0] == 3
if PY3:
import configparser
else:
import ConfigParser as configparser
DEFAULTS_SECTION = 'cython-defaults'
MODULE_SECTION_PREFIX = 'cython-module:'
CYTHON_EXT = '.pyx'
C_EXT = '.c'
CPP_EXT = '.cpp'
def setup(cythonize=True, **kwargs):
"""
Drop-in replacement for :func:`setuptools.setup`, adding Cython niceties.
Cython modules are described in setup.cfg, for example::
[cython-module: foo.bar]
sources = foo.pyx
bar.cpp
include_dirs = eval(__import__('numpy').get_include())
/usr/include/foo
language = c++
pkg_config_packages = opencv
You still need to provide a ``setup.py``::
from cysetuptools import setup
setup()
The modules sections support the following entries:
sources
The list of Cython and C/C++ source files that are compiled to build
the module.
libraries
A list of libraries to link with the module.
include_dirs
A list of directories to find include files. This entry supports
python expressions with ``eval()``; in the example above this is used
to retrieve the numpy include directory.
library_dirs
A list of directories to find libraries. This entry supports
python expressions with ``eval()`` like ``include_dirs``.
extra_compile_args
Extra arguments passed to the compiler.
extra_link_args
Extra arguments passed to the linker.
language
Typically "c" or "c++".
pkg_config_packages
A list of ``pkg-config`` package names to link with the module.
pkg_config_dirs
A list of directories to add to the pkg-config search paths (extends
the ``PKG_CONFIG_PATH`` environment variable).
Defaults can also be specified in the ``[cython-defaults]`` section, for
example::
[cython-defaults]
include_dirs = /usr/include/bar
[cython-module: foo.one]
sources = foo/one.pyx
[cython-module: foo.two]
sources = foo/two.pyx
include_dirs = /usr/include/foo
Here, ``foo.one`` and ``foo.two`` both will have ``/usr/include/bar`` in
their ``include_dirs``. List parameters in defaults are extended, so in the
example above, module ``foo.two`` ``include_dirs`` will be
``['/usr/include/bar', '/usr/include/foo']``.
There are two approaches when distributing Cython modules: with or without
the C files. Both approaches have their advantages and inconvenients:
* not distributing the C files means they are generated on the fly when
compiling the modules. Cython needs to be installed on the system,
and it makes your package a bit more future proof, as Cython evolves
to support newer Python versions. It also introduces some variance in
the builds, as they now depend on the Cython version installed on the
system;
* when you distribute the C files with your package, the modules can be
compiled directly with the host compiler, no Cython required. It also
makes your tarball heavier, as Cython generates quite verbose code.
It might also be good for performance-critical code, when you want to
make sure the generated code is optimal, regardless of version of
Cython installed on the host system.
In the first case, you can make Cython available to pip for compilation by
adding it to your ``setup.cfg``::
[options]
install_requires = cython
This way people who just want to install your package won't need to have
Cython installed in their system/venv.
It is up to you to choose one option or the other. The *cythonize* argument
controls the default mode of operation: set it to ``True`` if you don't
distribute C files with your package (the default), and ``False`` if you
do.
Packages that distribute C files may use the ``CYTHONIZE`` environment
variable to create or update the C files::
CYTHONIZE=1 python setup.py build_ext --inplace
You can also enable profiling for the Cython modules with the
``PROFILE_CYTHON`` environment variable::
PROFILE_CYTHON=1 python setup.py build_ext --inplace
Debugging symbols can be added with::
DEBUG=1 python setup.py build_ext --inplace
"""
this_dir = op.dirname(__file__)
setup_cfg_file = op.join(this_dir, 'setup.cfg')
cythonize = _str_to_bool(os.environ.get('CYTHONIZE', cythonize))
profile_cython = _str_to_bool(os.environ.get('PROFILE_CYTHON', False))
debug = _str_to_bool(os.environ.get('DEBUG', False))
if op.exists(setup_cfg_file):
# Create Cython Extension objects
with open(setup_cfg_file) as fp:
parsed_setup_cfg = parse_setup_cfg(fp, cythonize=cythonize)
cython_ext_modules = create_cython_ext_modules(
parsed_setup_cfg,
profile_cython=profile_cython,
debug=debug
)
if cythonize:
try:
from Cython.Build import cythonize
except ImportError:
pass
else:
cython_ext_modules = cythonize(cython_ext_modules)
ext_modules = kwargs.setdefault('ext_modules', [])
ext_modules.extend(cython_ext_modules)
setuptools.setup(**kwargs)
def create_cython_ext_modules(cython_modules, profile_cython=False,
debug=False):
"""
Create :class:`~distutils.extension.Extension` objects from
*cython_modules*.
*cython_modules* must be a dict, as returned by :func:`parse_setup_cfg`.
If *profile_cython* is true, Cython modules are compiled to support Python
proiflers.
Debug symbols are included if *debug* is true.
"""
if profile_cython:
from Cython.Distutils import Extension
else:
from distutils.extension import Extension
ret = []
for name, mod_data in cython_modules.items():
kwargs = {'name': name}
kwargs.update(mod_data)
if profile_cython:
cython_directives = kwargs.setdefault('cython_directives', {})
cython_directives['profile'] = True
if debug:
for args_name in ('extra_compile_args', 'extra_link_args'):
args = kwargs.setdefault(args_name, [])
if '-g' not in args:
args.append('-g')
ext = Extension(**kwargs)
ret.append(ext)
return ret
def parse_setup_cfg(fp, cythonize=False, pkg_config=None, base_dir=''):
"""
Parse the cython specific bits in a setup.cfg file.
*fp* must be a file-like object opened for reading.
*pkg_config* may be a callable taking a list of library names and returning
a ``pkg-config`` like string (e.g. ``-I/foo -L/bar -lbaz``). The default is
to use an internal function that actually runs ``pkg-config`` (normally
used for testing).
*base_dir* can be used to make relative paths absolute.
"""
if pkg_config is None:
pkg_config = _run_pkg_config
config = configparser.SafeConfigParser()
config.readfp(fp)
return _expand_cython_modules(config, cythonize, pkg_config, base_dir)
class _StoreOrderedArgs(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if 'ordered_args' not in namespace:
setattr(namespace, 'ordered_args', [])
namespace.ordered_args.append((self.dest, values))
def extract_args(args_str, args):
"""
Extract *args* from arguments string *args_str*.
Return a *(extracted_args, remaining_args_str)* tuple, where
*extracted_args* is a dict containing the extracted arguments, and
*remaining_args_str* a string containing the remaining arguments.
"""
parser = argparse.ArgumentParser()
for arg in args:
parser.add_argument(arg, action=_StoreOrderedArgs)
args_list = shlex.split(args_str)
try:
args_ns, other_args = parser.parse_known_args(args_list)
except SystemExit:
raise Exception('args parsing failed')
extracted_args = {}
for arg_name, value in getattr(args_ns, 'ordered_args', []):
arg_values = extracted_args.setdefault(arg_name, [])
arg_values.append(value)
return extracted_args, ' '.join(other_args)
def _expand_cython_modules(config, cythonize, pkg_config, base_dir):
ret = {}
for section in config.sections():
if section.startswith(MODULE_SECTION_PREFIX):
module_name = section[len(MODULE_SECTION_PREFIX):].strip()
module_dict = _expand_one_cython_module(config, section, cythonize,
pkg_config, base_dir)
ret[module_name] = module_dict
return ret
def _expand_one_cython_module(config, section, cythonize, pkg_config,
base_dir):
(pc_include_dirs,
pc_extra_compile_args,
pc_library_dirs,
pc_libraries,
pc_extra_link_args) = _expand_pkg_config_pkgs(config, section, pkg_config)
module = {}
module['language'] = _get_config_opt(config, section, 'language', None)
module['extra_compile_args'] = \
_get_config_list(config, section, 'extra_compile_args') + \
pc_extra_compile_args
module['extra_link_args'] = \
_get_config_list(config, section, 'extra_link_args') + \
pc_extra_link_args
module['sources'] = _expand_sources(config, section, module['language'],
cythonize)
include_dirs = _get_config_list(config, section, 'include_dirs')
include_dirs += pc_include_dirs
include_dirs = _eval_strings(include_dirs)
include_dirs = _make_paths_absolute(include_dirs, base_dir)
library_dirs = _get_config_list(config, section, 'library_dirs')
library_dirs += pc_library_dirs
library_dirs = _eval_strings(library_dirs)
library_dirs = _make_paths_absolute(library_dirs, base_dir)
libraries = _get_config_list(config, section, 'libraries')
module['include_dirs'] = include_dirs
module['library_dirs'] = library_dirs
module['libraries'] = libraries + pc_libraries
all_conf_items = config.items(section)
try:
all_conf_items += config.items(DEFAULTS_SECTION)
except configparser.NoSectionError:
pass
for key, value in all_conf_items:
if key != 'pkg_config_packages' and key not in module:
module[key] = value
return module
def _make_paths_absolute(paths, base_dir):
return [op.join(base_dir, p) if not p.startswith('/') else p
for p in paths]
def _eval_strings(values):
ret = []
for value in values:
if value.startswith('eval(') and value.endswith(')'):
ret.append(eval(value[5:-1]))
else:
ret.append(value)
return ret
def _expand_pkg_config_pkgs(config, section, pkg_config):
pkg_names = _get_config_list(config, section, 'pkg_config_packages')
if not pkg_names:
return [], [], [], [], []
original_pkg_config_path = os.environ.get('PKG_CONFIG_PATH', '')
pkg_config_path = original_pkg_config_path.split(":")
pkg_config_path.extend(_get_config_list(config, section,
'pkg_config_dirs'))
env = os.environ.copy()
env['PKG_CONFIG_PATH'] = ":".join(pkg_config_path)
extra_compile_args = pkg_config(pkg_names, '--cflags', env)
extra_link_args = pkg_config(pkg_names, '--libs', env)
extracted_args, extra_compile_args = extract_args(extra_compile_args,
['-I'])
include_dirs = extracted_args.get('I', [])
extracted_args, extra_link_args = extract_args(extra_link_args,
['-L', '-l'])
library_dirs = extracted_args.get('L', [])
libraries = extracted_args.get('l', [])
extra_compile_args = shlex.split(extra_compile_args)
extra_link_args = shlex.split(extra_link_args)
return (include_dirs, extra_compile_args, library_dirs, libraries,
extra_link_args)
def _run_pkg_config(pkg_names, command, env):
return subprocess.check_output(['pkg-config', command] + pkg_names,
env=env).decode('utf8')
def _expand_sources(config, section, language, cythonize):
if cythonize:
ext = CYTHON_EXT
elif language == 'c++':
ext = CPP_EXT
else:
ext = C_EXT
sources = _get_config_list(config, section, 'sources')
return [_replace_cython_ext(s, ext) for s in sources]
def _replace_cython_ext(filename, target_ext):
root, ext = op.splitext(filename)
if ext == CYTHON_EXT:
return root + target_ext
return filename
def _get_default(config, option, default):
try:
return config.get(DEFAULTS_SECTION, option)
except (configparser.NoOptionError, configparser.NoSectionError):
return default
def _get_config_opt(config, section, option, default):
try:
return config.get(section, option)
except configparser.NoOptionError:
return _get_default(config, option, default)
def _get_config_list(config, section, option):
defaults_value = _get_default(config, option, '')
try:
value = config.get(section, option)
except configparser.NoOptionError:
value = ''
return ('%s %s' % (defaults_value, value)).split()
def _str_to_bool(value):
if isinstance(value, bool):
return value
value = value.lower()
if value in ('1', 'on', 'true', 'yes'):
return True
elif value in ('0', 'off', 'false', 'no'):
return False
raise ValueError('invalid boolean string %r' % value)