##// END OF EJS Templates
Merge pull request #2225 from bfroehle/cython_annotate...
Bradley M. Froehle -
r8171:20925553 merge
parent child Browse files
Show More
@@ -15,9 +15,14 b' Parts of this code were taken from Cython.inline.'
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 import io
18 from __future__ import print_function
19 import os, sys
19
20 import imp
20 import imp
21 import io
22 import os
23 import re
24 import sys
25 import time
21
26
22 try:
27 try:
23 import hashlib
28 import hashlib
@@ -27,16 +32,14 b' except ImportError:'
27 from distutils.core import Distribution, Extension
32 from distutils.core import Distribution, Extension
28 from distutils.command.build_ext import build_ext
33 from distutils.command.build_ext import build_ext
29
34
35 from IPython.core import display
36 from IPython.core import magic_arguments
30 from IPython.core.magic import Magics, magics_class, cell_magic
37 from IPython.core.magic import Magics, magics_class, cell_magic
31 from IPython.testing.skipdoctest import skip_doctest
38 from IPython.testing.skipdoctest import skip_doctest
32 from IPython.core.magic_arguments import (
33 argument, magic_arguments, parse_argstring
34 )
35 from IPython.utils import py3compat
39 from IPython.utils import py3compat
36
40
37 import Cython
41 import Cython
38 from Cython.Compiler.Errors import CompileError
42 from Cython.Compiler.Errors import CompileError
39 from Cython.Compiler.Main import Context, default_options
40 from Cython.Build.Dependencies import cythonize
43 from Cython.Build.Dependencies import cythonize
41
44
42
45
@@ -61,7 +64,7 b' class CythonMagics(Magics):'
61 and returns the result. If the variables `a` and `b` are defined
64 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
65 in the user's namespace, here is a simple example that returns
63 their sum::
66 their sum::
64
67
65 %%cython_inline
68 %%cython_inline
66 return a+b
69 return a+b
67
70
@@ -78,14 +81,14 b' class CythonMagics(Magics):'
78 The contents of the cell are written to a `.pyx` file in the current
81 The contents of the cell are written to a `.pyx` file in the current
79 working directory, which is then imported using `pyximport`. This
82 working directory, which is then imported using `pyximport`. This
80 magic requires a module name to be passed::
83 magic requires a module name to be passed::
81
84
82 %%cython_pyximport modulename
85 %%cython_pyximport modulename
83 def f(x):
86 def f(x):
84 return 2.0*x
87 return 2.0*x
85
88
86 The compiled module is then imported and all of its symbols are injected into
89 The compiled module is then imported and all of its symbols are
87 the user's namespace. For most purposes, we recommend the usage of the
90 injected into the user's namespace. For most purposes, we recommend
88 `%%cython` magic.
91 the usage of the `%%cython` magic.
89 """
92 """
90 module_name = line.strip()
93 module_name = line.strip()
91 if not module_name:
94 if not module_name:
@@ -105,30 +108,47 b' class CythonMagics(Magics):'
105 self._reloads[module_name] = module
108 self._reloads[module_name] = module
106 self._import_all(module)
109 self._import_all(module)
107
110
108 @magic_arguments()
111 @magic_arguments.magic_arguments()
109 @argument(
112 @magic_arguments.argument(
110 '-c', '--compile-args', action='append', default=[],
113 '-c', '--compile-args', action='append', default=[],
111 help="Extra flags to pass to compiler via the `extra_compile_args` Extension flag (can be specified multiple times)."
114 help="Extra flags to pass to compiler via the `extra_compile_args` "
115 "Extension flag (can be specified multiple times)."
116 )
117 @magic_arguments.argument(
118 '-la', '--link-args', action='append', default=[],
119 help="Extra flags to pass to linker via the `extra_link_args` "
120 "Extension flag (can be specified multiple times)."
112 )
121 )
113 @argument(
122 @magic_arguments.argument(
114 '-l', '--lib', action='append', default=[],
123 '-l', '--lib', action='append', default=[],
115 help="Add a library to link the extension against (can be specified multiple times)."
124 help="Add a library to link the extension against (can be specified "
125 "multiple times)."
116 )
126 )
117 @argument(
127 @magic_arguments.argument(
118 '-I', '--include', action='append', default=[],
128 '-I', '--include', action='append', default=[],
119 help="Add a path to the list of include directories (can be specified multiple times)."
129 help="Add a path to the list of include directories (can be specified "
130 "multiple times)."
120 )
131 )
121 @argument(
132 @magic_arguments.argument(
133 '-+', '--cplus', action='store_true', default=False,
134 help="Output a C++ rather than C file."
135 )
136 @magic_arguments.argument(
122 '-f', '--force', action='store_true', default=False,
137 '-f', '--force', action='store_true', default=False,
123 help="Force the compilation of the pyx module even if it hasn't changed"
138 help="Force the compilation of a new module, even if the source has been "
139 "previously compiled."
140 )
141 @magic_arguments.argument(
142 '-a', '--annotate', action='store_true', default=False,
143 help="Produce a colorized HTML version of the source."
124 )
144 )
125 @cell_magic
145 @cell_magic
126 def cython(self, line, cell):
146 def cython(self, line, cell):
127 """Compile and import everything from a Cython code cell.
147 """Compile and import everything from a Cython code cell.
128
148
129 The contents of the cell are written to a `.pyx` file in the
149 The contents of the cell are written to a `.pyx` file in the
130 directory `IPYTHONDIR/cython` using a filename with the hash of the code.
150 directory `IPYTHONDIR/cython` using a filename with the hash of the
131 This file is then cythonized and compiled. The resulting module
151 code. This file is then cythonized and compiled. The resulting module
132 is imported and all of its symbols are injected into the user's
152 is imported and all of its symbols are injected into the user's
133 namespace. The usage is similar to that of `%%cython_pyximport` but
153 namespace. The usage is similar to that of `%%cython_pyximport` but
134 you don't have to pass a module name::
154 you don't have to pass a module name::
@@ -137,22 +157,32 b' class CythonMagics(Magics):'
137 def f(x):
157 def f(x):
138 return 2.0*x
158 return 2.0*x
139 """
159 """
140 args = parse_argstring(self.cython, line)
160 args = magic_arguments.parse_argstring(self.cython, line)
141 code = cell if cell.endswith('\n') else cell+'\n'
161 code = cell if cell.endswith('\n') else cell+'\n'
142 lib_dir = os.path.join(self.shell.ipython_dir, 'cython')
162 lib_dir = os.path.join(self.shell.ipython_dir, 'cython')
143 cython_include_dirs = ['.']
144 force = args.force
145 quiet = True
163 quiet = True
146 ctx = Context(cython_include_dirs, default_options)
147 key = code, sys.version_info, sys.executable, Cython.__version__
164 key = code, sys.version_info, sys.executable, Cython.__version__
148 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
149 so_ext = [ ext for ext,_,mod_type in imp.get_suffixes() if mod_type == imp.C_EXTENSION ][0]
150 module_path = os.path.join(lib_dir, module_name+so_ext)
151
165
152 if not os.path.exists(lib_dir):
166 if not os.path.exists(lib_dir):
153 os.makedirs(lib_dir)
167 os.makedirs(lib_dir)
154
168
155 if force or not os.path.isfile(module_path):
169 if args.force:
170 # Force a new module name by adding the current time to the
171 # key which is hashed to determine the module name.
172 key += time.time(),
173
174 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
175 module_path = os.path.join(lib_dir, module_name + self.so_ext)
176
177 have_module = os.path.isfile(module_path)
178 need_cythonize = not have_module
179
180 if args.annotate:
181 html_file = os.path.join(lib_dir, module_name + '.html')
182 if not os.path.isfile(html_file):
183 need_cythonize = True
184
185 if need_cythonize:
156 c_include_dirs = args.include
186 c_include_dirs = args.include
157 if 'numpy' in code:
187 if 'numpy' in code:
158 import numpy
188 import numpy
@@ -166,21 +196,22 b' class CythonMagics(Magics):'
166 sources = [pyx_file],
196 sources = [pyx_file],
167 include_dirs = c_include_dirs,
197 include_dirs = c_include_dirs,
168 extra_compile_args = args.compile_args,
198 extra_compile_args = args.compile_args,
199 extra_link_args = args.link_args,
169 libraries = args.lib,
200 libraries = args.lib,
201 language = 'c++' if args.cplus else 'c',
170 )
202 )
171 dist = Distribution()
203 build_extension = self._get_build_extension()
172 config_files = dist.find_config_files()
173 try:
174 config_files.remove('setup.cfg')
175 except ValueError:
176 pass
177 dist.parse_config_files(config_files)
178 build_extension = build_ext(dist)
179 build_extension.finalize_options()
180 try:
204 try:
181 build_extension.extensions = cythonize([extension], ctx=ctx, quiet=quiet)
205 opts = dict(
206 quiet=quiet,
207 annotate = args.annotate,
208 force = True,
209 )
210 build_extension.extensions = cythonize([extension], **opts)
182 except CompileError:
211 except CompileError:
183 return
212 return
213
214 if not have_module:
184 build_extension.build_temp = os.path.dirname(pyx_file)
215 build_extension.build_temp = os.path.dirname(pyx_file)
185 build_extension.build_lib = lib_dir
216 build_extension.build_lib = lib_dir
186 build_extension.run()
217 build_extension.run()
@@ -189,6 +220,52 b' class CythonMagics(Magics):'
189 module = imp.load_dynamic(module_name, module_path)
220 module = imp.load_dynamic(module_name, module_path)
190 self._import_all(module)
221 self._import_all(module)
191
222
223 if args.annotate:
224 try:
225 with io.open(html_file, encoding='utf-8') as f:
226 annotated_html = f.read()
227 except IOError as e:
228 # File could not be opened. Most likely the user has a version
229 # of Cython before 0.15.1 (when `cythonize` learned the
230 # `force` keyword argument) and has already compiled this
231 # exact source without annotation.
232 print('Cython completed successfully but the annotated '
233 'source could not be read.', file=sys.stderr)
234 print(e, file=sys.stderr)
235 else:
236 return display.HTML(self.clean_annotated_html(annotated_html))
237
238 @property
239 def so_ext(self):
240 """The extension suffix for compiled modules."""
241 try:
242 return self._so_ext
243 except AttributeError:
244 self._so_ext = self._get_build_extension().get_ext_filename('')
245 return self._so_ext
246
247 def _get_build_extension(self):
248 dist = Distribution()
249 config_files = dist.find_config_files()
250 try:
251 config_files.remove('setup.cfg')
252 except ValueError:
253 pass
254 dist.parse_config_files(config_files)
255 build_extension = build_ext(dist)
256 build_extension.finalize_options()
257 return build_extension
258
259 @staticmethod
260 def clean_annotated_html(html):
261 """Clean up the annotated HTML source.
262
263 Strips the link to the generated C or C++ file, which we do not
264 present to the user.
265 """
266 r = re.compile('<p>Raw output: <a href="(.*)">(.*)</a>')
267 html = '\n'.join(l for l in html.splitlines() if not r.match(l))
268 return html
192
269
193 _loaded = False
270 _loaded = False
194
271
General Comments 0
You need to be logged in to leave comments. Login now