##// END OF EJS Templates
Added new option in %%cython cell magic to specify the module's name.
rossant -
Show More
@@ -1,325 +1,332
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', '--module_name', action='append', default=[],
151 help="Specify a name for the Cython module."
152 )
153 @magic_arguments.argument(
150 154 '-L', dest='library_dirs', metavar='dir', action='append', default=[],
151 155 help="Add a path to the list of libary directories (can be specified "
152 156 "multiple times)."
153 157 )
154 158 @magic_arguments.argument(
155 159 '-I', '--include', action='append', default=[],
156 160 help="Add a path to the list of include directories (can be specified "
157 161 "multiple times)."
158 162 )
159 163 @magic_arguments.argument(
160 164 '-+', '--cplus', action='store_true', default=False,
161 165 help="Output a C++ rather than C file."
162 166 )
163 167 @magic_arguments.argument(
164 168 '-f', '--force', action='store_true', default=False,
165 169 help="Force the compilation of a new module, even if the source has been "
166 170 "previously compiled."
167 171 )
168 172 @magic_arguments.argument(
169 173 '-a', '--annotate', action='store_true', default=False,
170 174 help="Produce a colorized HTML version of the source."
171 175 )
172 176 @cell_magic
173 177 def cython(self, line, cell):
174 178 """Compile and import everything from a Cython code cell.
175 179
176 180 The contents of the cell are written to a `.pyx` file in the
177 181 directory `IPYTHONDIR/cython` using a filename with the hash of the
178 182 code. This file is then cythonized and compiled. The resulting module
179 183 is imported and all of its symbols are injected into the user's
180 184 namespace. The usage is similar to that of `%%cython_pyximport` but
181 185 you don't have to pass a module name::
182 186
183 187 %%cython
184 188 def f(x):
185 189 return 2.0*x
186 190
187 191 To compile OpenMP codes, pass the required `--compile-args`
188 192 and `--link-args`. For example with gcc::
189 193
190 194 %%cython --compile-args=-fopenmp --link-args=-fopenmp
191 195 ...
192 196 """
193 197 args = magic_arguments.parse_argstring(self.cython, line)
194 198 code = cell if cell.endswith('\n') else cell+'\n'
195 199 lib_dir = os.path.join(get_ipython_cache_dir(), 'cython')
196 200 quiet = True
197 201 key = code, sys.version_info, sys.executable, Cython.__version__
198 202
199 203 if not os.path.exists(lib_dir):
200 204 os.makedirs(lib_dir)
201 205
202 206 if args.force:
203 207 # Force a new module name by adding the current time to the
204 208 # key which is hashed to determine the module name.
205 209 key += time.time(),
206 210
211 if args.module_name:
212 module_name = str(args.module_name[0])
213 else:
207 214 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
208 215 module_path = os.path.join(lib_dir, module_name + self.so_ext)
209 216
210 217 have_module = os.path.isfile(module_path)
211 218 need_cythonize = not have_module
212 219
213 220 if args.annotate:
214 221 html_file = os.path.join(lib_dir, module_name + '.html')
215 222 if not os.path.isfile(html_file):
216 223 need_cythonize = True
217 224
218 225 if need_cythonize:
219 226 c_include_dirs = args.include
220 227 if 'numpy' in code:
221 228 import numpy
222 229 c_include_dirs.append(numpy.get_include())
223 230 pyx_file = os.path.join(lib_dir, module_name + '.pyx')
224 231 pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding())
225 232 with io.open(pyx_file, 'w', encoding='utf-8') as f:
226 233 f.write(code)
227 234 extension = Extension(
228 235 name = module_name,
229 236 sources = [pyx_file],
230 237 include_dirs = c_include_dirs,
231 238 library_dirs = args.library_dirs,
232 239 extra_compile_args = args.compile_args,
233 240 extra_link_args = args.link_args,
234 241 libraries = args.lib,
235 242 language = 'c++' if args.cplus else 'c',
236 243 )
237 244 build_extension = self._get_build_extension()
238 245 try:
239 246 opts = dict(
240 247 quiet=quiet,
241 248 annotate = args.annotate,
242 249 force = True,
243 250 )
244 251 build_extension.extensions = cythonize([extension], **opts)
245 252 except CompileError:
246 253 return
247 254
248 255 if not have_module:
249 256 build_extension.build_temp = os.path.dirname(pyx_file)
250 257 build_extension.build_lib = lib_dir
251 258 build_extension.run()
252 259 self._code_cache[key] = module_name
253 260
254 261 module = imp.load_dynamic(module_name, module_path)
255 262 self._import_all(module)
256 263
257 264 if args.annotate:
258 265 try:
259 266 with io.open(html_file, encoding='utf-8') as f:
260 267 annotated_html = f.read()
261 268 except IOError as e:
262 269 # File could not be opened. Most likely the user has a version
263 270 # of Cython before 0.15.1 (when `cythonize` learned the
264 271 # `force` keyword argument) and has already compiled this
265 272 # exact source without annotation.
266 273 print('Cython completed successfully but the annotated '
267 274 'source could not be read.', file=sys.stderr)
268 275 print(e, file=sys.stderr)
269 276 else:
270 277 return display.HTML(self.clean_annotated_html(annotated_html))
271 278
272 279 @property
273 280 def so_ext(self):
274 281 """The extension suffix for compiled modules."""
275 282 try:
276 283 return self._so_ext
277 284 except AttributeError:
278 285 self._so_ext = self._get_build_extension().get_ext_filename('')
279 286 return self._so_ext
280 287
281 288 def _clear_distutils_mkpath_cache(self):
282 289 """clear distutils mkpath cache
283 290
284 291 prevents distutils from skipping re-creation of dirs that have been removed
285 292 """
286 293 try:
287 294 from distutils.dir_util import _path_created
288 295 except ImportError:
289 296 pass
290 297 else:
291 298 _path_created.clear()
292 299
293 300 def _get_build_extension(self):
294 301 self._clear_distutils_mkpath_cache()
295 302 dist = Distribution()
296 303 config_files = dist.find_config_files()
297 304 try:
298 305 config_files.remove('setup.cfg')
299 306 except ValueError:
300 307 pass
301 308 dist.parse_config_files(config_files)
302 309 build_extension = build_ext(dist)
303 310 build_extension.finalize_options()
304 311 return build_extension
305 312
306 313 @staticmethod
307 314 def clean_annotated_html(html):
308 315 """Clean up the annotated HTML source.
309 316
310 317 Strips the link to the generated C or C++ file, which we do not
311 318 present to the user.
312 319 """
313 320 r = re.compile('<p>Raw output: <a href="(.*)">(.*)</a>')
314 321 html = '\n'.join(l for l in html.splitlines() if not r.match(l))
315 322 return html
316 323
317 324 __doc__ = __doc__.format(
318 325 CYTHON_DOC = ' '*8 + CythonMagics.cython.__doc__,
319 326 CYTHON_INLINE_DOC = ' '*8 + CythonMagics.cython_inline.__doc__,
320 327 CYTHON_PYXIMPORT_DOC = ' '*8 + CythonMagics.cython_pyximport.__doc__,
321 328 )
322 329
323 330 def load_ipython_extension(ip):
324 331 """Load the extension in IPython."""
325 332 ip.register_magics(CythonMagics)
General Comments 0
You need to be logged in to leave comments. Login now