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