##// END OF EJS Templates
The 'name' argument to the %%cython cell magic is no longer a list....
Cyrille Rossant -
Show More
@@ -1,332 +1,332 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 =====================
4 4 Cython related magics
5 5 =====================
6 6
7 7 Usage
8 8 =====
9 9
10 10 ``%%cython``
11 11
12 12 {CYTHON_DOC}
13 13
14 14 ``%%cython_inline``
15 15
16 16 {CYTHON_INLINE_DOC}
17 17
18 18 ``%%cython_pyximport``
19 19
20 20 {CYTHON_PYXIMPORT_DOC}
21 21
22 22 Author:
23 23 * Brian Granger
24 24
25 25 Parts of this code were taken from Cython.inline.
26 26 """
27 27 #-----------------------------------------------------------------------------
28 28 # Copyright (C) 2010-2011, IPython Development Team.
29 29 #
30 30 # Distributed under the terms of the Modified BSD License.
31 31 #
32 32 # The full license is in the file COPYING.txt, distributed with this software.
33 33 #-----------------------------------------------------------------------------
34 34
35 35 from __future__ import print_function
36 36
37 37 import imp
38 38 import io
39 39 import os
40 40 import re
41 41 import sys
42 42 import time
43 43
44 44 try:
45 45 reload
46 46 except NameError: # Python 3
47 47 from imp import reload
48 48
49 49 try:
50 50 import hashlib
51 51 except ImportError:
52 52 import md5 as hashlib
53 53
54 54 from distutils.core import Distribution, Extension
55 55 from distutils.command.build_ext import build_ext
56 56
57 57 from IPython.core import display
58 58 from IPython.core import magic_arguments
59 59 from IPython.core.magic import Magics, magics_class, cell_magic
60 60 from IPython.utils import py3compat
61 61 from IPython.utils.path import get_ipython_cache_dir
62 62
63 63 import Cython
64 64 from Cython.Compiler.Errors import CompileError
65 65 from Cython.Build.Dependencies import cythonize
66 66
67 67
68 68 @magics_class
69 69 class CythonMagics(Magics):
70 70
71 71 def __init__(self, shell):
72 72 super(CythonMagics,self).__init__(shell)
73 73 self._reloads = {}
74 74 self._code_cache = {}
75 75
76 76 def _import_all(self, module):
77 77 for k,v in module.__dict__.items():
78 78 if not k.startswith('__'):
79 79 self.shell.push({k:v})
80 80
81 81 @cell_magic
82 82 def cython_inline(self, line, cell):
83 83 """Compile and run a Cython code cell using Cython.inline.
84 84
85 85 This magic simply passes the body of the cell to Cython.inline
86 86 and returns the result. If the variables `a` and `b` are defined
87 87 in the user's namespace, here is a simple example that returns
88 88 their sum::
89 89
90 90 %%cython_inline
91 91 return a+b
92 92
93 93 For most purposes, we recommend the usage of the `%%cython` magic.
94 94 """
95 95 locs = self.shell.user_global_ns
96 96 globs = self.shell.user_ns
97 97 return Cython.inline(cell, locals=locs, globals=globs)
98 98
99 99 @cell_magic
100 100 def cython_pyximport(self, line, cell):
101 101 """Compile and import a Cython code cell using pyximport.
102 102
103 103 The contents of the cell are written to a `.pyx` file in the current
104 104 working directory, which is then imported using `pyximport`. This
105 105 magic requires a module name to be passed::
106 106
107 107 %%cython_pyximport modulename
108 108 def f(x):
109 109 return 2.0*x
110 110
111 111 The compiled module is then imported and all of its symbols are
112 112 injected into the user's namespace. For most purposes, we recommend
113 113 the usage of the `%%cython` magic.
114 114 """
115 115 module_name = line.strip()
116 116 if not module_name:
117 117 raise ValueError('module name must be given')
118 118 fname = module_name + '.pyx'
119 119 with io.open(fname, 'w', encoding='utf-8') as f:
120 120 f.write(cell)
121 121 if 'pyximport' not in sys.modules:
122 122 import pyximport
123 123 pyximport.install(reload_support=True)
124 124 if module_name in self._reloads:
125 125 module = self._reloads[module_name]
126 126 reload(module)
127 127 else:
128 128 __import__(module_name)
129 129 module = sys.modules[module_name]
130 130 self._reloads[module_name] = module
131 131 self._import_all(module)
132 132
133 133 @magic_arguments.magic_arguments()
134 134 @magic_arguments.argument(
135 135 '-c', '--compile-args', action='append', default=[],
136 136 help="Extra flags to pass to compiler via the `extra_compile_args` "
137 137 "Extension flag (can be specified multiple times)."
138 138 )
139 139 @magic_arguments.argument(
140 140 '--link-args', action='append', default=[],
141 141 help="Extra flags to pass to linker via the `extra_link_args` "
142 142 "Extension flag (can be specified multiple times)."
143 143 )
144 144 @magic_arguments.argument(
145 145 '-l', '--lib', action='append', default=[],
146 146 help="Add a library to link the extension against (can be specified "
147 147 "multiple times)."
148 148 )
149 149 @magic_arguments.argument(
150 '-n', '--name', action='append', default=[],
150 '-n', '--name',
151 151 help="Specify a name for the Cython module."
152 152 )
153 153 @magic_arguments.argument(
154 154 '-L', dest='library_dirs', metavar='dir', action='append', default=[],
155 155 help="Add a path to the list of libary directories (can be specified "
156 156 "multiple times)."
157 157 )
158 158 @magic_arguments.argument(
159 159 '-I', '--include', action='append', default=[],
160 160 help="Add a path to the list of include directories (can be specified "
161 161 "multiple times)."
162 162 )
163 163 @magic_arguments.argument(
164 164 '-+', '--cplus', action='store_true', default=False,
165 165 help="Output a C++ rather than C file."
166 166 )
167 167 @magic_arguments.argument(
168 168 '-f', '--force', action='store_true', default=False,
169 169 help="Force the compilation of a new module, even if the source has been "
170 170 "previously compiled."
171 171 )
172 172 @magic_arguments.argument(
173 173 '-a', '--annotate', action='store_true', default=False,
174 174 help="Produce a colorized HTML version of the source."
175 175 )
176 176 @cell_magic
177 177 def cython(self, line, cell):
178 178 """Compile and import everything from a Cython code cell.
179 179
180 180 The contents of the cell are written to a `.pyx` file in the
181 181 directory `IPYTHONDIR/cython` using a filename with the hash of the
182 182 code. This file is then cythonized and compiled. The resulting module
183 183 is imported and all of its symbols are injected into the user's
184 184 namespace. The usage is similar to that of `%%cython_pyximport` but
185 185 you don't have to pass a module name::
186 186
187 187 %%cython
188 188 def f(x):
189 189 return 2.0*x
190 190
191 191 To compile OpenMP codes, pass the required `--compile-args`
192 192 and `--link-args`. For example with gcc::
193 193
194 194 %%cython --compile-args=-fopenmp --link-args=-fopenmp
195 195 ...
196 196 """
197 197 args = magic_arguments.parse_argstring(self.cython, line)
198 198 code = cell if cell.endswith('\n') else cell+'\n'
199 199 lib_dir = os.path.join(get_ipython_cache_dir(), 'cython')
200 200 quiet = True
201 201 key = code, sys.version_info, sys.executable, Cython.__version__
202 202
203 203 if not os.path.exists(lib_dir):
204 204 os.makedirs(lib_dir)
205 205
206 206 if args.force:
207 207 # Force a new module name by adding the current time to the
208 208 # key which is hashed to determine the module name.
209 209 key += time.time(),
210 210
211 211 if args.name:
212 module_name = str(args.name[0])
212 module_name = str(args.name)
213 213 else:
214 214 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
215 215 module_path = os.path.join(lib_dir, module_name + self.so_ext)
216 216
217 217 have_module = os.path.isfile(module_path)
218 218 need_cythonize = not have_module
219 219
220 220 if args.annotate:
221 221 html_file = os.path.join(lib_dir, module_name + '.html')
222 222 if not os.path.isfile(html_file):
223 223 need_cythonize = True
224 224
225 225 if need_cythonize:
226 226 c_include_dirs = args.include
227 227 if 'numpy' in code:
228 228 import numpy
229 229 c_include_dirs.append(numpy.get_include())
230 230 pyx_file = os.path.join(lib_dir, module_name + '.pyx')
231 231 pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding())
232 232 with io.open(pyx_file, 'w', encoding='utf-8') as f:
233 233 f.write(code)
234 234 extension = Extension(
235 235 name = module_name,
236 236 sources = [pyx_file],
237 237 include_dirs = c_include_dirs,
238 238 library_dirs = args.library_dirs,
239 239 extra_compile_args = args.compile_args,
240 240 extra_link_args = args.link_args,
241 241 libraries = args.lib,
242 242 language = 'c++' if args.cplus else 'c',
243 243 )
244 244 build_extension = self._get_build_extension()
245 245 try:
246 246 opts = dict(
247 247 quiet=quiet,
248 248 annotate = args.annotate,
249 249 force = True,
250 250 )
251 251 build_extension.extensions = cythonize([extension], **opts)
252 252 except CompileError:
253 253 return
254 254
255 255 if not have_module:
256 256 build_extension.build_temp = os.path.dirname(pyx_file)
257 257 build_extension.build_lib = lib_dir
258 258 build_extension.run()
259 259 self._code_cache[key] = module_name
260 260
261 261 module = imp.load_dynamic(module_name, module_path)
262 262 self._import_all(module)
263 263
264 264 if args.annotate:
265 265 try:
266 266 with io.open(html_file, encoding='utf-8') as f:
267 267 annotated_html = f.read()
268 268 except IOError as e:
269 269 # File could not be opened. Most likely the user has a version
270 270 # of Cython before 0.15.1 (when `cythonize` learned the
271 271 # `force` keyword argument) and has already compiled this
272 272 # exact source without annotation.
273 273 print('Cython completed successfully but the annotated '
274 274 'source could not be read.', file=sys.stderr)
275 275 print(e, file=sys.stderr)
276 276 else:
277 277 return display.HTML(self.clean_annotated_html(annotated_html))
278 278
279 279 @property
280 280 def so_ext(self):
281 281 """The extension suffix for compiled modules."""
282 282 try:
283 283 return self._so_ext
284 284 except AttributeError:
285 285 self._so_ext = self._get_build_extension().get_ext_filename('')
286 286 return self._so_ext
287 287
288 288 def _clear_distutils_mkpath_cache(self):
289 289 """clear distutils mkpath cache
290 290
291 291 prevents distutils from skipping re-creation of dirs that have been removed
292 292 """
293 293 try:
294 294 from distutils.dir_util import _path_created
295 295 except ImportError:
296 296 pass
297 297 else:
298 298 _path_created.clear()
299 299
300 300 def _get_build_extension(self):
301 301 self._clear_distutils_mkpath_cache()
302 302 dist = Distribution()
303 303 config_files = dist.find_config_files()
304 304 try:
305 305 config_files.remove('setup.cfg')
306 306 except ValueError:
307 307 pass
308 308 dist.parse_config_files(config_files)
309 309 build_extension = build_ext(dist)
310 310 build_extension.finalize_options()
311 311 return build_extension
312 312
313 313 @staticmethod
314 314 def clean_annotated_html(html):
315 315 """Clean up the annotated HTML source.
316 316
317 317 Strips the link to the generated C or C++ file, which we do not
318 318 present to the user.
319 319 """
320 320 r = re.compile('<p>Raw output: <a href="(.*)">(.*)</a>')
321 321 html = '\n'.join(l for l in html.splitlines() if not r.match(l))
322 322 return html
323 323
324 324 __doc__ = __doc__.format(
325 325 CYTHON_DOC = ' '*8 + CythonMagics.cython.__doc__,
326 326 CYTHON_INLINE_DOC = ' '*8 + CythonMagics.cython_inline.__doc__,
327 327 CYTHON_PYXIMPORT_DOC = ' '*8 + CythonMagics.cython_pyximport.__doc__,
328 328 )
329 329
330 330 def load_ipython_extension(ip):
331 331 """Load the extension in IPython."""
332 332 ip.register_magics(CythonMagics)
@@ -1,62 +1,70 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for the Cython magics extension."""
3 3
4 4 import os
5 5 import nose.tools as nt
6 6
7 7 from IPython.testing import decorators as dec
8 8 from IPython.utils import py3compat
9 9
10 10 code = py3compat.str_to_unicode("""def f(x):
11 11 return 2*x
12 12 """)
13 13
14 14 try:
15 15 import Cython
16 16 except:
17 17 __test__ = False
18 18
19 19 ip = get_ipython()
20 20
21 21
22 22 def setup():
23 23 ip.extension_manager.load_extension('cythonmagic')
24 24
25 25
26 26 def test_cython_inline():
27 27 ip.ex('a=10; b=20')
28 28 result = ip.run_cell_magic('cython_inline','','return a+b')
29 29 nt.assert_equal(result, 30)
30 30
31 31
32 32 @dec.skip_win32
33 33 def test_cython_pyximport():
34 34 module_name = '_test_cython_pyximport'
35 35 ip.run_cell_magic('cython_pyximport', module_name, code)
36 36 ip.ex('g = f(10)')
37 37 nt.assert_equal(ip.user_ns['g'], 20.0)
38 38 ip.run_cell_magic('cython_pyximport', module_name, code)
39 39 ip.ex('h = f(-10)')
40 40 nt.assert_equal(ip.user_ns['h'], -20.0)
41 41 try:
42 42 os.remove(module_name+'.pyx')
43 43 except OSError:
44 44 pass
45 45
46 46
47 47 def test_cython():
48 48 ip.run_cell_magic('cython', '', code)
49 49 ip.ex('g = f(10)')
50 50 nt.assert_equal(ip.user_ns['g'], 20.0)
51 51
52 52
53 def test_cython_name():
54 # The Cython module named 'mymodule' defines the function f.
55 ip.run_cell_magic('cython', '--name=mymodule', code)
56 # This module can now be imported in the interactive namespace.
57 ip.ex('import mymodule; g = mymodule.f(10)')
58 nt.assert_equal(ip.user_ns['g'], 20.0)
59
60
53 61 @dec.skip_win32
54 62 def test_extlibs():
55 63 code = py3compat.str_to_unicode("""
56 64 from libc.math cimport sin
57 65 x = sin(0.0)
58 66 """)
59 67 ip.user_ns['x'] = 1
60 68 ip.run_cell_magic('cython', '-l m', code)
61 69 nt.assert_equal(ip.user_ns['x'], 0)
62 70
General Comments 0
You need to be logged in to leave comments. Login now