##// END OF EJS Templates
Determine the shared object extension more robustly.
Bradley M. Froehle -
Show More
@@ -1,231 +1,244 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 Cython related magics.
3 Cython related magics.
4
4
5 Author:
5 Author:
6 * Brian Granger
6 * Brian Granger
7
7
8 Parts of this code were taken from Cython.inline.
8 Parts of this code were taken from Cython.inline.
9 """
9 """
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2010-2011, IPython Development Team.
11 # Copyright (C) 2010-2011, IPython Development Team.
12 #
12 #
13 # Distributed under the terms of the Modified BSD License.
13 # Distributed under the terms of the Modified BSD License.
14 #
14 #
15 # The full license is in the file COPYING.txt, distributed with this software.
15 # The full license is in the file COPYING.txt, distributed with this software.
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 from __future__ import print_function
18 from __future__ import print_function
19
19
20 import io
21 import os, sys
22 import imp
20 import imp
21 import io
22 import os
23 import sys
23
24
24 try:
25 try:
25 import hashlib
26 import hashlib
26 except ImportError:
27 except ImportError:
27 import md5 as hashlib
28 import md5 as hashlib
28
29
29 from distutils.core import Distribution, Extension
30 from distutils.core import Distribution, Extension
30 from distutils.command.build_ext import build_ext
31 from distutils.command.build_ext import build_ext
31
32
32 from IPython.core import display
33 from IPython.core import display
33 from IPython.core import magic_arguments
34 from IPython.core import magic_arguments
34 from IPython.core.magic import Magics, magics_class, cell_magic
35 from IPython.core.magic import Magics, magics_class, cell_magic
35 from IPython.testing.skipdoctest import skip_doctest
36 from IPython.testing.skipdoctest import skip_doctest
36 from IPython.utils import py3compat
37 from IPython.utils import py3compat
37
38
38 import Cython
39 import Cython
39 from Cython.Compiler.Errors import CompileError
40 from Cython.Compiler.Errors import CompileError
40 from Cython.Build.Dependencies import cythonize
41 from Cython.Build.Dependencies import cythonize
41
42
42
43
43 @magics_class
44 @magics_class
44 class CythonMagics(Magics):
45 class CythonMagics(Magics):
45
46
46 def __init__(self, shell):
47 def __init__(self, shell):
47 super(CythonMagics,self).__init__(shell)
48 super(CythonMagics,self).__init__(shell)
48 self._reloads = {}
49 self._reloads = {}
49 self._code_cache = {}
50 self._code_cache = {}
50
51
51 def _import_all(self, module):
52 def _import_all(self, module):
52 for k,v in module.__dict__.items():
53 for k,v in module.__dict__.items():
53 if not k.startswith('__'):
54 if not k.startswith('__'):
54 self.shell.push({k:v})
55 self.shell.push({k:v})
55
56
56 @cell_magic
57 @cell_magic
57 def cython_inline(self, line, cell):
58 def cython_inline(self, line, cell):
58 """Compile and run a Cython code cell using Cython.inline.
59 """Compile and run a Cython code cell using Cython.inline.
59
60
60 This magic simply passes the body of the cell to Cython.inline
61 This magic simply passes the body of the cell to Cython.inline
61 and returns the result. If the variables `a` and `b` are defined
62 and returns the result. If the variables `a` and `b` are defined
62 in the user's namespace, here is a simple example that returns
63 in the user's namespace, here is a simple example that returns
63 their sum::
64 their sum::
64
65
65 %%cython_inline
66 %%cython_inline
66 return a+b
67 return a+b
67
68
68 For most purposes, we recommend the usage of the `%%cython` magic.
69 For most purposes, we recommend the usage of the `%%cython` magic.
69 """
70 """
70 locs = self.shell.user_global_ns
71 locs = self.shell.user_global_ns
71 globs = self.shell.user_ns
72 globs = self.shell.user_ns
72 return Cython.inline(cell, locals=locs, globals=globs)
73 return Cython.inline(cell, locals=locs, globals=globs)
73
74
74 @cell_magic
75 @cell_magic
75 def cython_pyximport(self, line, cell):
76 def cython_pyximport(self, line, cell):
76 """Compile and import a Cython code cell using pyximport.
77 """Compile and import a Cython code cell using pyximport.
77
78
78 The contents of the cell are written to a `.pyx` file in the current
79 The contents of the cell are written to a `.pyx` file in the current
79 working directory, which is then imported using `pyximport`. This
80 working directory, which is then imported using `pyximport`. This
80 magic requires a module name to be passed::
81 magic requires a module name to be passed::
81
82
82 %%cython_pyximport modulename
83 %%cython_pyximport modulename
83 def f(x):
84 def f(x):
84 return 2.0*x
85 return 2.0*x
85
86
86 The compiled module is then imported and all of its symbols are
87 The compiled module is then imported and all of its symbols are
87 injected into the user's namespace. For most purposes, we recommend
88 injected into the user's namespace. For most purposes, we recommend
88 the usage of the `%%cython` magic.
89 the usage of the `%%cython` magic.
89 """
90 """
90 module_name = line.strip()
91 module_name = line.strip()
91 if not module_name:
92 if not module_name:
92 raise ValueError('module name must be given')
93 raise ValueError('module name must be given')
93 fname = module_name + '.pyx'
94 fname = module_name + '.pyx'
94 with io.open(fname, 'w', encoding='utf-8') as f:
95 with io.open(fname, 'w', encoding='utf-8') as f:
95 f.write(cell)
96 f.write(cell)
96 if 'pyximport' not in sys.modules:
97 if 'pyximport' not in sys.modules:
97 import pyximport
98 import pyximport
98 pyximport.install(reload_support=True)
99 pyximport.install(reload_support=True)
99 if module_name in self._reloads:
100 if module_name in self._reloads:
100 module = self._reloads[module_name]
101 module = self._reloads[module_name]
101 reload(module)
102 reload(module)
102 else:
103 else:
103 __import__(module_name)
104 __import__(module_name)
104 module = sys.modules[module_name]
105 module = sys.modules[module_name]
105 self._reloads[module_name] = module
106 self._reloads[module_name] = module
106 self._import_all(module)
107 self._import_all(module)
107
108
108 @magic_arguments.magic_arguments()
109 @magic_arguments.magic_arguments()
109 @magic_arguments.argument(
110 @magic_arguments.argument(
110 '-c', '--compile-args', action='append', default=[],
111 '-c', '--compile-args', action='append', default=[],
111 help="Extra flags to pass to compiler via the `extra_compile_args` "
112 help="Extra flags to pass to compiler via the `extra_compile_args` "
112 "Extension flag (can be specified multiple times)."
113 "Extension flag (can be specified multiple times)."
113 )
114 )
114 @magic_arguments.argument(
115 @magic_arguments.argument(
115 '-la', '--link-args', action='append', default=[],
116 '-la', '--link-args', action='append', default=[],
116 help="Extra flags to pass to linker via the `extra_link_args` "
117 help="Extra flags to pass to linker via the `extra_link_args` "
117 "Extension flag (can be specified multiple times)."
118 "Extension flag (can be specified multiple times)."
118 )
119 )
119 @magic_arguments.argument(
120 @magic_arguments.argument(
120 '-l', '--lib', action='append', default=[],
121 '-l', '--lib', action='append', default=[],
121 help="Add a library to link the extension against (can be specified "
122 help="Add a library to link the extension against (can be specified "
122 "multiple times)."
123 "multiple times)."
123 )
124 )
124 @magic_arguments.argument(
125 @magic_arguments.argument(
125 '-I', '--include', action='append', default=[],
126 '-I', '--include', action='append', default=[],
126 help="Add a path to the list of include directories (can be specified "
127 help="Add a path to the list of include directories (can be specified "
127 "multiple times)."
128 "multiple times)."
128 )
129 )
129 @magic_arguments.argument(
130 @magic_arguments.argument(
130 '-f', '--force', action='store_true', default=False,
131 '-f', '--force', action='store_true', default=False,
131 help="Force the compilation of the pyx module even if it hasn't "
132 help="Force the compilation of the pyx module even if it hasn't "
132 "changed"
133 "changed"
133 )
134 )
134 @magic_arguments.argument(
135 @magic_arguments.argument(
135 '-a', '--annotate', action='store_true', default=False,
136 '-a', '--annotate', action='store_true', default=False,
136 help="Produce a colorized HTML version of the source. "
137 help="Produce a colorized HTML version of the source. "
137 "(Implies --force)."
138 "(Implies --force)."
138 )
139 )
139 @cell_magic
140 @cell_magic
140 def cython(self, line, cell):
141 def cython(self, line, cell):
141 """Compile and import everything from a Cython code cell.
142 """Compile and import everything from a Cython code cell.
142
143
143 The contents of the cell are written to a `.pyx` file in the
144 The contents of the cell are written to a `.pyx` file in the
144 directory `IPYTHONDIR/cython` using a filename with the hash of the
145 directory `IPYTHONDIR/cython` using a filename with the hash of the
145 code. This file is then cythonized and compiled. The resulting module
146 code. This file is then cythonized and compiled. The resulting module
146 is imported and all of its symbols are injected into the user's
147 is imported and all of its symbols are injected into the user's
147 namespace. The usage is similar to that of `%%cython_pyximport` but
148 namespace. The usage is similar to that of `%%cython_pyximport` but
148 you don't have to pass a module name::
149 you don't have to pass a module name::
149
150
150 %%cython
151 %%cython
151 def f(x):
152 def f(x):
152 return 2.0*x
153 return 2.0*x
153 """
154 """
154 args = magic_arguments.parse_argstring(self.cython, line)
155 args = magic_arguments.parse_argstring(self.cython, line)
155 code = cell if cell.endswith('\n') else cell+'\n'
156 code = cell if cell.endswith('\n') else cell+'\n'
156 lib_dir = os.path.join(self.shell.ipython_dir, 'cython')
157 lib_dir = os.path.join(self.shell.ipython_dir, 'cython')
157 quiet = True
158 quiet = True
158 key = code, sys.version_info, sys.executable, Cython.__version__
159 key = code, sys.version_info, sys.executable, Cython.__version__
159 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
160 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
160 so_ext = [ ext for ext,_,mod_type in imp.get_suffixes() if mod_type == imp.C_EXTENSION ][0]
161 module_path = os.path.join(lib_dir, module_name+self.so_ext)
161 module_path = os.path.join(lib_dir, module_name+so_ext)
162
162
163 if args.annotate:
163 if args.annotate:
164 args.force = True
164 args.force = True
165
165
166 if not os.path.exists(lib_dir):
166 if not os.path.exists(lib_dir):
167 os.makedirs(lib_dir)
167 os.makedirs(lib_dir)
168
168
169 if args.force or not os.path.isfile(module_path):
169 if args.force or not os.path.isfile(module_path):
170 c_include_dirs = args.include
170 c_include_dirs = args.include
171 if 'numpy' in code:
171 if 'numpy' in code:
172 import numpy
172 import numpy
173 c_include_dirs.append(numpy.get_include())
173 c_include_dirs.append(numpy.get_include())
174 pyx_file = os.path.join(lib_dir, module_name + '.pyx')
174 pyx_file = os.path.join(lib_dir, module_name + '.pyx')
175 pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding())
175 pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding())
176 with io.open(pyx_file, 'w', encoding='utf-8') as f:
176 with io.open(pyx_file, 'w', encoding='utf-8') as f:
177 f.write(code)
177 f.write(code)
178 extension = Extension(
178 extension = Extension(
179 name = module_name,
179 name = module_name,
180 sources = [pyx_file],
180 sources = [pyx_file],
181 include_dirs = c_include_dirs,
181 include_dirs = c_include_dirs,
182 extra_compile_args = args.compile_args,
182 extra_compile_args = args.compile_args,
183 extra_link_args = args.link_args,
183 extra_link_args = args.link_args,
184 libraries = args.lib,
184 libraries = args.lib,
185 )
185 )
186 dist = Distribution()
186 build_extension = self._get_build_extension()
187 config_files = dist.find_config_files()
188 try:
189 config_files.remove('setup.cfg')
190 except ValueError:
191 pass
192 dist.parse_config_files(config_files)
193 build_extension = build_ext(dist)
194 build_extension.finalize_options()
195 try:
187 try:
196 build_extension.extensions = cythonize([extension], quiet=quiet,
188 build_extension.extensions = cythonize([extension], quiet=quiet,
197 annotate=args.annotate, force=args.force)
189 annotate=args.annotate, force=args.force)
198 except CompileError:
190 except CompileError:
199 return
191 return
200 build_extension.build_temp = os.path.dirname(pyx_file)
192 build_extension.build_temp = os.path.dirname(pyx_file)
201 build_extension.build_lib = lib_dir
193 build_extension.build_lib = lib_dir
202 build_extension.run()
194 build_extension.run()
203 self._code_cache[key] = module_name
195 self._code_cache[key] = module_name
204
196
205 module = imp.load_dynamic(module_name, module_path)
197 module = imp.load_dynamic(module_name, module_path)
206 self._import_all(module)
198 self._import_all(module)
207
199
208 if args.annotate:
200 if args.annotate:
209 html_file = os.path.join(lib_dir, module_name + '.html')
201 html_file = os.path.join(lib_dir, module_name + '.html')
210 try:
202 try:
211 with io.open(html_file, encoding='utf-8') as f:
203 with io.open(html_file, encoding='utf-8') as f:
212 annotated_html = f.read()
204 annotated_html = f.read()
213 except IOError as e:
205 except IOError as e:
214 # File could not be opened. Most likely the user has a version
206 # File could not be opened. Most likely the user has a version
215 # of Cython before 0.15.1 (when `cythonize` learned the
207 # of Cython before 0.15.1 (when `cythonize` learned the
216 # `force` keyword argument) and has already compiled this
208 # `force` keyword argument) and has already compiled this
217 # exact source without annotation.
209 # exact source without annotation.
218 print('Cython completed successfully but the annotated '
210 print('Cython completed successfully but the annotated '
219 'source could not be read.', file=sys.stderr)
211 'source could not be read.', file=sys.stderr)
220 print(e, file=sys.stderr)
212 print(e, file=sys.stderr)
221 else:
213 else:
222 return display.HTML(annotated_html)
214 return display.HTML(annotated_html)
223
215
216 @property
217 def so_ext(self):
218 """The extension suffix for compiled modules."""
219 try:
220 return self._so_ext
221 except AttributeError:
222 self._so_ext = self._get_build_extension().get_ext_filename('')
223 return self._so_ext
224
225 def _get_build_extension(self):
226 dist = Distribution()
227 config_files = dist.find_config_files()
228 try:
229 config_files.remove('setup.cfg')
230 except ValueError:
231 pass
232 dist.parse_config_files(config_files)
233 build_extension = build_ext(dist)
234 build_extension.finalize_options()
235 return build_extension
236
224 _loaded = False
237 _loaded = False
225
238
226 def load_ipython_extension(ip):
239 def load_ipython_extension(ip):
227 """Load the extension in IPython."""
240 """Load the extension in IPython."""
228 global _loaded
241 global _loaded
229 if not _loaded:
242 if not _loaded:
230 ip.register_magics(CythonMagics)
243 ip.register_magics(CythonMagics)
231 _loaded = True
244 _loaded = True
General Comments 0
You need to be logged in to leave comments. Login now