cythonmagic.py
279 lines
| 9.8 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r7031 | # -*- coding: utf-8 -*- | ||
""" | ||||
Cython related magics. | ||||
Author: | ||||
* Brian Granger | ||||
Parts of this code were taken from Cython.inline. | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
# Copyright (C) 2010-2011, IPython Development Team. | ||||
# | ||||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
Bradley M. Froehle
|
r8095 | from __future__ import print_function | ||
Brian Granger
|
r7031 | import imp | ||
Bradley M. Froehle
|
r8127 | import io | ||
import os | ||||
Bradley M. Froehle
|
r8170 | import re | ||
Bradley M. Froehle
|
r8127 | import sys | ||
Bradley M. Froehle
|
r8146 | import time | ||
Brian Granger
|
r7031 | |||
try: | ||||
import hashlib | ||||
except ImportError: | ||||
import md5 as hashlib | ||||
from distutils.core import Distribution, Extension | ||||
from distutils.command.build_ext import build_ext | ||||
Bradley M. Froehle
|
r8093 | from IPython.core import display | ||
Bradley M. Froehle
|
r8090 | from IPython.core import magic_arguments | ||
Brian Granger
|
r7031 | from IPython.core.magic import Magics, magics_class, cell_magic | ||
from IPython.testing.skipdoctest import skip_doctest | ||||
Brian Granger
|
r7102 | from IPython.utils import py3compat | ||
Brian Granger
|
r7031 | |||
import Cython | ||||
from Cython.Compiler.Errors import CompileError | ||||
from Cython.Build.Dependencies import cythonize | ||||
@magics_class | ||||
class CythonMagics(Magics): | ||||
def __init__(self, shell): | ||||
super(CythonMagics,self).__init__(shell) | ||||
self._reloads = {} | ||||
self._code_cache = {} | ||||
def _import_all(self, module): | ||||
for k,v in module.__dict__.items(): | ||||
if not k.startswith('__'): | ||||
self.shell.push({k:v}) | ||||
@cell_magic | ||||
def cython_inline(self, line, cell): | ||||
Brian Granger
|
r7037 | """Compile and run a Cython code cell using Cython.inline. | ||
This magic simply passes the body of the cell to Cython.inline | ||||
and returns the result. If the variables `a` and `b` are defined | ||||
in the user's namespace, here is a simple example that returns | ||||
their sum:: | ||||
Bradley M. Froehle
|
r8126 | |||
Brian Granger
|
r7037 | %%cython_inline | ||
return a+b | ||||
For most purposes, we recommend the usage of the `%%cython` magic. | ||||
""" | ||||
Brian Granger
|
r7031 | locs = self.shell.user_global_ns | ||
globs = self.shell.user_ns | ||||
return Cython.inline(cell, locals=locs, globals=globs) | ||||
@cell_magic | ||||
def cython_pyximport(self, line, cell): | ||||
Brian Granger
|
r7037 | """Compile and import a Cython code cell using pyximport. | ||
The contents of the cell are written to a `.pyx` file in the current | ||||
working directory, which is then imported using `pyximport`. This | ||||
magic requires a module name to be passed:: | ||||
Bradley M. Froehle
|
r8126 | |||
Brian Granger
|
r7037 | %%cython_pyximport modulename | ||
def f(x): | ||||
return 2.0*x | ||||
Bradley M. Froehle
|
r8126 | The compiled module is then imported and all of its symbols are | ||
injected into the user's namespace. For most purposes, we recommend | ||||
the usage of the `%%cython` magic. | ||||
Brian Granger
|
r7037 | """ | ||
Brian Granger
|
r7031 | module_name = line.strip() | ||
if not module_name: | ||||
raise ValueError('module name must be given') | ||||
fname = module_name + '.pyx' | ||||
Brian Granger
|
r7102 | with io.open(fname, 'w', encoding='utf-8') as f: | ||
Brian Granger
|
r7031 | f.write(cell) | ||
if 'pyximport' not in sys.modules: | ||||
import pyximport | ||||
pyximport.install(reload_support=True) | ||||
if module_name in self._reloads: | ||||
module = self._reloads[module_name] | ||||
reload(module) | ||||
else: | ||||
Bradley M. Froehle
|
r7342 | __import__(module_name) | ||
module = sys.modules[module_name] | ||||
Brian Granger
|
r7031 | self._reloads[module_name] = module | ||
self._import_all(module) | ||||
Bradley M. Froehle
|
r8090 | @magic_arguments.magic_arguments() | ||
@magic_arguments.argument( | ||||
Fernando Perez
|
r7419 | '-c', '--compile-args', action='append', default=[], | ||
Bradley M. Froehle
|
r8126 | help="Extra flags to pass to compiler via the `extra_compile_args` " | ||
"Extension flag (can be specified multiple times)." | ||||
Fernando Perez
|
r7419 | ) | ||
Bradley M. Froehle
|
r8090 | @magic_arguments.argument( | ||
David Hirschfeld
|
r8098 | '-la', '--link-args', action='append', default=[], | ||
Bradley M. Froehle
|
r8126 | help="Extra flags to pass to linker via the `extra_link_args` " | ||
"Extension flag (can be specified multiple times)." | ||||
David Hirschfeld
|
r8098 | ) | ||
@magic_arguments.argument( | ||||
Fernando Perez
|
r7419 | '-l', '--lib', action='append', default=[], | ||
Bradley M. Froehle
|
r8126 | help="Add a library to link the extension against (can be specified " | ||
"multiple times)." | ||||
Fernando Perez
|
r7419 | ) | ||
Bradley M. Froehle
|
r8090 | @magic_arguments.argument( | ||
Chris Laumann
|
r8395 | '-L', dest='library_dirs', metavar='dir', action='append', default=[], | ||
Chris Laumann
|
r8391 | help="Add a path to the list of libary directories (can be specified " | ||
"multiple times)." | ||||
) | ||||
@magic_arguments.argument( | ||||
Fernando Perez
|
r7422 | '-I', '--include', action='append', default=[], | ||
Bradley M. Froehle
|
r8126 | help="Add a path to the list of include directories (can be specified " | ||
"multiple times)." | ||||
Fernando Perez
|
r7419 | ) | ||
Bradley M. Froehle
|
r8090 | @magic_arguments.argument( | ||
Bradley M. Froehle
|
r8128 | '-+', '--cplus', action='store_true', default=False, | ||
help="Output a C++ rather than C file." | ||||
) | ||||
@magic_arguments.argument( | ||||
Brian Granger
|
r7031 | '-f', '--force', action='store_true', default=False, | ||
Bradley M. Froehle
|
r8146 | help="Force the compilation of a new module, even if the source has been " | ||
"previously compiled." | ||||
Brian Granger
|
r7031 | ) | ||
Bradley M. Froehle
|
r8093 | @magic_arguments.argument( | ||
'-a', '--annotate', action='store_true', default=False, | ||||
Bradley M. Froehle
|
r8146 | help="Produce a colorized HTML version of the source." | ||
Bradley M. Froehle
|
r8093 | ) | ||
Brian Granger
|
r7031 | @cell_magic | ||
def cython(self, line, cell): | ||||
Brian Granger
|
r7037 | """Compile and import everything from a Cython code cell. | ||
The contents of the cell are written to a `.pyx` file in the | ||||
Bradley M. Froehle
|
r8126 | directory `IPYTHONDIR/cython` using a filename with the hash of the | ||
code. This file is then cythonized and compiled. The resulting module | ||||
Brian Granger
|
r7037 | is imported and all of its symbols are injected into the user's | ||
namespace. The usage is similar to that of `%%cython_pyximport` but | ||||
you don't have to pass a module name:: | ||||
%%cython | ||||
def f(x): | ||||
return 2.0*x | ||||
""" | ||||
Bradley M. Froehle
|
r8090 | args = magic_arguments.parse_argstring(self.cython, line) | ||
Brian Granger
|
r7031 | code = cell if cell.endswith('\n') else cell+'\n' | ||
Fernando Perez
|
r7419 | lib_dir = os.path.join(self.shell.ipython_dir, 'cython') | ||
quiet = True | ||||
Brian Granger
|
r7031 | key = code, sys.version_info, sys.executable, Cython.__version__ | ||
Bradley M. Froehle
|
r8093 | |||
Brian Granger
|
r7031 | if not os.path.exists(lib_dir): | ||
os.makedirs(lib_dir) | ||||
Bradley M. Froehle
|
r8146 | if args.force: | ||
# Force a new module name by adding the current time to the | ||||
# key which is hashed to determine the module name. | ||||
key += time.time(), | ||||
module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest() | ||||
module_path = os.path.join(lib_dir, module_name + self.so_ext) | ||||
have_module = os.path.isfile(module_path) | ||||
need_cythonize = not have_module | ||||
if args.annotate: | ||||
html_file = os.path.join(lib_dir, module_name + '.html') | ||||
if not os.path.isfile(html_file): | ||||
need_cythonize = True | ||||
if need_cythonize: | ||||
Fernando Perez
|
r7419 | c_include_dirs = args.include | ||
Brian Granger
|
r7031 | if 'numpy' in code: | ||
import numpy | ||||
c_include_dirs.append(numpy.get_include()) | ||||
pyx_file = os.path.join(lib_dir, module_name + '.pyx') | ||||
Thomas Kluyver
|
r7113 | pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding()) | ||
Brian Granger
|
r7102 | with io.open(pyx_file, 'w', encoding='utf-8') as f: | ||
Brian Granger
|
r7031 | f.write(code) | ||
extension = Extension( | ||||
name = module_name, | ||||
sources = [pyx_file], | ||||
include_dirs = c_include_dirs, | ||||
Chris Laumann
|
r8395 | library_dirs = args.library_dirs, | ||
Fernando Perez
|
r7419 | extra_compile_args = args.compile_args, | ||
David Hirschfeld
|
r8098 | extra_link_args = args.link_args, | ||
Fernando Perez
|
r7419 | libraries = args.lib, | ||
Bradley M. Froehle
|
r8128 | language = 'c++' if args.cplus else 'c', | ||
Brian Granger
|
r7031 | ) | ||
Bradley M. Froehle
|
r8127 | build_extension = self._get_build_extension() | ||
Brian Granger
|
r7031 | try: | ||
Bradley M. Froehle
|
r8128 | opts = dict( | ||
quiet=quiet, | ||||
annotate = args.annotate, | ||||
Bradley M. Froehle
|
r8146 | force = True, | ||
Bradley M. Froehle
|
r8128 | ) | ||
build_extension.extensions = cythonize([extension], **opts) | ||||
Brian Granger
|
r7031 | except CompileError: | ||
return | ||||
Bradley M. Froehle
|
r8146 | |||
if not have_module: | ||||
Brian Granger
|
r7031 | build_extension.build_temp = os.path.dirname(pyx_file) | ||
build_extension.build_lib = lib_dir | ||||
build_extension.run() | ||||
self._code_cache[key] = module_name | ||||
module = imp.load_dynamic(module_name, module_path) | ||||
self._import_all(module) | ||||
Bradley M. Froehle
|
r8093 | if args.annotate: | ||
Bradley M. Froehle
|
r8095 | try: | ||
with io.open(html_file, encoding='utf-8') as f: | ||||
annotated_html = f.read() | ||||
except IOError as e: | ||||
# File could not be opened. Most likely the user has a version | ||||
# of Cython before 0.15.1 (when `cythonize` learned the | ||||
# `force` keyword argument) and has already compiled this | ||||
# exact source without annotation. | ||||
print('Cython completed successfully but the annotated ' | ||||
'source could not be read.', file=sys.stderr) | ||||
print(e, file=sys.stderr) | ||||
else: | ||||
Bradley M. Froehle
|
r8170 | return display.HTML(self.clean_annotated_html(annotated_html)) | ||
Brian Granger
|
r7031 | |||
Bradley M. Froehle
|
r8127 | @property | ||
def so_ext(self): | ||||
"""The extension suffix for compiled modules.""" | ||||
try: | ||||
return self._so_ext | ||||
except AttributeError: | ||||
self._so_ext = self._get_build_extension().get_ext_filename('') | ||||
return self._so_ext | ||||
def _get_build_extension(self): | ||||
dist = Distribution() | ||||
config_files = dist.find_config_files() | ||||
try: | ||||
config_files.remove('setup.cfg') | ||||
except ValueError: | ||||
pass | ||||
dist.parse_config_files(config_files) | ||||
build_extension = build_ext(dist) | ||||
build_extension.finalize_options() | ||||
return build_extension | ||||
Bradley M. Froehle
|
r8170 | @staticmethod | ||
def clean_annotated_html(html): | ||||
"""Clean up the annotated HTML source. | ||||
Strips the link to the generated C or C++ file, which we do not | ||||
present to the user. | ||||
""" | ||||
r = re.compile('<p>Raw output: <a href="(.*)">(.*)</a>') | ||||
html = '\n'.join(l for l in html.splitlines() if not r.match(l)) | ||||
return html | ||||
Brian Granger
|
r7031 | |||
def load_ipython_extension(ip): | ||||
"""Load the extension in IPython.""" | ||||
Thomas Kluyver
|
r8552 | ip.register_magics(CythonMagics) | ||