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