123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- # -*- coding: utf-8 -*-
- """
- =====================
- Cython related magics
- =====================
- Magic command interface for interactive work with Cython
- .. note::
- The ``Cython`` package needs to be installed separately. It
- can be obtained using ``easy_install`` or ``pip``.
- Usage
- =====
- To enable the magics below, execute ``%load_ext cython``.
- ``%%cython``
- {CYTHON_DOC}
- ``%%cython_inline``
- {CYTHON_INLINE_DOC}
- ``%%cython_pyximport``
- {CYTHON_PYXIMPORT_DOC}
- Author:
- * Brian Granger
- Code moved from IPython and adapted by:
- * Martín Gaitán
- 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 ipython-COPYING.rst, distributed with this software.
- #-----------------------------------------------------------------------------
- from __future__ import absolute_import, print_function
- import io
- import os
- import re
- import sys
- import time
- import copy
- import distutils.log
- import textwrap
- IO_ENCODING = sys.getfilesystemencoding()
- IS_PY2 = sys.version_info[0] < 3
- try:
- reload
- except NameError: # Python 3
- from imp import reload
- try:
- import hashlib
- except ImportError:
- import md5 as hashlib
- from distutils.core import Distribution, Extension
- from distutils.command.build_ext import build_ext
- from IPython.core import display
- from IPython.core import magic_arguments
- from IPython.core.magic import Magics, magics_class, cell_magic
- try:
- from IPython.paths import get_ipython_cache_dir
- except ImportError:
- # older IPython version
- from IPython.utils.path import get_ipython_cache_dir
- from IPython.utils.text import dedent
- from ..Shadow import __version__ as cython_version
- from ..Compiler.Errors import CompileError
- from .Inline import cython_inline, load_dynamic
- from .Dependencies import cythonize
- PGO_CONFIG = {
- 'gcc': {
- 'gen': ['-fprofile-generate', '-fprofile-dir={TEMPDIR}'],
- 'use': ['-fprofile-use', '-fprofile-correction', '-fprofile-dir={TEMPDIR}'],
- },
- # blind copy from 'configure' script in CPython 3.7
- 'icc': {
- 'gen': ['-prof-gen'],
- 'use': ['-prof-use'],
- }
- }
- PGO_CONFIG['mingw32'] = PGO_CONFIG['gcc']
- if IS_PY2:
- def encode_fs(name):
- return name if isinstance(name, bytes) else name.encode(IO_ENCODING)
- else:
- def encode_fs(name):
- return name
- @magics_class
- class CythonMagics(Magics):
- def __init__(self, shell):
- super(CythonMagics, self).__init__(shell)
- self._reloads = {}
- self._code_cache = {}
- self._pyximport_installed = False
- def _import_all(self, module):
- mdict = module.__dict__
- if '__all__' in mdict:
- keys = mdict['__all__']
- else:
- keys = [k for k in mdict if not k.startswith('_')]
- for k in keys:
- try:
- self.shell.push({k: mdict[k]})
- except KeyError:
- msg = "'module' object has no attribute '%s'" % k
- raise AttributeError(msg)
- @cell_magic
- def cython_inline(self, line, cell):
- """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::
- %%cython_inline
- return a+b
- For most purposes, we recommend the usage of the `%%cython` magic.
- """
- 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):
- """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::
- %%cython_pyximport modulename
- def f(x):
- return 2.0*x
- 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.
- """
- module_name = line.strip()
- if not module_name:
- raise ValueError('module name must be given')
- fname = module_name + '.pyx'
- with io.open(fname, 'w', encoding='utf-8') as f:
- f.write(cell)
- if 'pyximport' not in sys.modules or not self._pyximport_installed:
- import pyximport
- pyximport.install()
- self._pyximport_installed = True
- if module_name in self._reloads:
- module = self._reloads[module_name]
- # Note: reloading extension modules is not actually supported
- # (requires PEP-489 reinitialisation support).
- # Don't know why this should ever have worked as it reads here.
- # All we really need to do is to update the globals below.
- #reload(module)
- else:
- __import__(module_name)
- module = sys.modules[module_name]
- self._reloads[module_name] = module
- self._import_all(module)
- @magic_arguments.magic_arguments()
- @magic_arguments.argument(
- '-a', '--annotate', action='store_true', default=False,
- help="Produce a colorized HTML version of the source."
- )
- @magic_arguments.argument(
- '-+', '--cplus', action='store_true', default=False,
- help="Output a C++ rather than C file."
- )
- @magic_arguments.argument(
- '-3', dest='language_level', action='store_const', const=3, default=None,
- help="Select Python 3 syntax."
- )
- @magic_arguments.argument(
- '-2', dest='language_level', action='store_const', const=2, default=None,
- help="Select Python 2 syntax."
- )
- @magic_arguments.argument(
- '-f', '--force', action='store_true', default=False,
- help="Force the compilation of a new module, even if the source has been "
- "previously compiled."
- )
- @magic_arguments.argument(
- '-c', '--compile-args', action='append', default=[],
- help="Extra flags to pass to compiler via the `extra_compile_args` "
- "Extension flag (can be specified multiple times)."
- )
- @magic_arguments.argument(
- '--link-args', action='append', default=[],
- help="Extra flags to pass to linker via the `extra_link_args` "
- "Extension flag (can be specified multiple times)."
- )
- @magic_arguments.argument(
- '-l', '--lib', action='append', default=[],
- help="Add a library to link the extension against (can be specified "
- "multiple times)."
- )
- @magic_arguments.argument(
- '-n', '--name',
- help="Specify a name for the Cython module."
- )
- @magic_arguments.argument(
- '-L', dest='library_dirs', metavar='dir', action='append', default=[],
- help="Add a path to the list of library directories (can be specified "
- "multiple times)."
- )
- @magic_arguments.argument(
- '-I', '--include', action='append', default=[],
- help="Add a path to the list of include directories (can be specified "
- "multiple times)."
- )
- @magic_arguments.argument(
- '-S', '--src', action='append', default=[],
- help="Add a path to the list of src files (can be specified "
- "multiple times)."
- )
- @magic_arguments.argument(
- '--pgo', dest='pgo', action='store_true', default=False,
- help=("Enable profile guided optimisation in the C compiler. "
- "Compiles the cell twice and executes it in between to generate a runtime profile.")
- )
- @magic_arguments.argument(
- '--verbose', dest='quiet', action='store_false', default=True,
- help=("Print debug information like generated .c/.cpp file location "
- "and exact gcc/g++ command invoked.")
- )
- @cell_magic
- def cython(self, line, cell):
- """Compile and import everything from a Cython code cell.
- The contents of the cell are written to a `.pyx` file in the
- directory `IPYTHONDIR/cython` using a filename with the hash of the
- code. This file is then cythonized and compiled. The resulting module
- 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
- To compile OpenMP codes, pass the required `--compile-args`
- and `--link-args`. For example with gcc::
- %%cython --compile-args=-fopenmp --link-args=-fopenmp
- ...
- To enable profile guided optimisation, pass the ``--pgo`` option.
- Note that the cell itself needs to take care of establishing a suitable
- profile when executed. This can be done by implementing the functions to
- optimise, and then calling them directly in the same cell on some realistic
- training data like this::
- %%cython --pgo
- def critical_function(data):
- for item in data:
- ...
- # execute function several times to build profile
- from somewhere import some_typical_data
- for _ in range(100):
- critical_function(some_typical_data)
- In Python 3.5 and later, you can distinguish between the profile and
- non-profile runs as follows::
- if "_pgo_" in __name__:
- ... # execute critical code here
- """
- args = magic_arguments.parse_argstring(self.cython, line)
- code = cell if cell.endswith('\n') else cell + '\n'
- lib_dir = os.path.join(get_ipython_cache_dir(), 'cython')
- key = (code, line, sys.version_info, sys.executable, cython_version)
- if not os.path.exists(lib_dir):
- os.makedirs(lib_dir)
- if args.pgo:
- key += ('pgo',)
- 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(),)
- if args.name:
- module_name = str(args.name) # no-op in Py3
- else:
- module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
- html_file = os.path.join(lib_dir, module_name + '.html')
- module_path = os.path.join(lib_dir, module_name + self.so_ext)
- have_module = os.path.isfile(module_path)
- need_cythonize = args.pgo or not have_module
- if args.annotate:
- if not os.path.isfile(html_file):
- need_cythonize = True
- extension = None
- if need_cythonize:
- extensions = self._cythonize(module_name, code, lib_dir, args, quiet=args.quiet)
- if extensions is None:
- # Compilation failed and printed error message
- return None
- assert len(extensions) == 1
- extension = extensions[0]
- self._code_cache[key] = module_name
- if args.pgo:
- self._profile_pgo_wrapper(extension, lib_dir)
- try:
- self._build_extension(extension, lib_dir, pgo_step_name='use' if args.pgo else None,
- quiet=args.quiet)
- except distutils.errors.CompileError:
- # Build failed and printed error message
- return None
- module = load_dynamic(module_name, module_path)
- self._import_all(module)
- if args.annotate:
- 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:
- return display.HTML(self.clean_annotated_html(annotated_html))
- def _profile_pgo_wrapper(self, extension, lib_dir):
- """
- Generate a .c file for a separate extension module that calls the
- module init function of the original module. This makes sure that the
- PGO profiler sees the correct .o file of the final module, but it still
- allows us to import the module under a different name for profiling,
- before recompiling it into the PGO optimised module. Overwriting and
- reimporting the same shared library is not portable.
- """
- extension = copy.copy(extension) # shallow copy, do not modify sources in place!
- module_name = extension.name
- pgo_module_name = '_pgo_' + module_name
- pgo_wrapper_c_file = os.path.join(lib_dir, pgo_module_name + '.c')
- with io.open(pgo_wrapper_c_file, 'w', encoding='utf-8') as f:
- f.write(textwrap.dedent(u"""
- #include "Python.h"
- #if PY_MAJOR_VERSION < 3
- extern PyMODINIT_FUNC init%(module_name)s(void);
- PyMODINIT_FUNC init%(pgo_module_name)s(void); /*proto*/
- PyMODINIT_FUNC init%(pgo_module_name)s(void) {
- PyObject *sys_modules;
- init%(module_name)s(); if (PyErr_Occurred()) return;
- sys_modules = PyImport_GetModuleDict(); /* borrowed, no exception, "never" fails */
- if (sys_modules) {
- PyObject *module = PyDict_GetItemString(sys_modules, "%(module_name)s"); if (!module) return;
- PyDict_SetItemString(sys_modules, "%(pgo_module_name)s", module);
- Py_DECREF(module);
- }
- }
- #else
- extern PyMODINIT_FUNC PyInit_%(module_name)s(void);
- PyMODINIT_FUNC PyInit_%(pgo_module_name)s(void); /*proto*/
- PyMODINIT_FUNC PyInit_%(pgo_module_name)s(void) {
- return PyInit_%(module_name)s();
- }
- #endif
- """ % {'module_name': module_name, 'pgo_module_name': pgo_module_name}))
- extension.sources = extension.sources + [pgo_wrapper_c_file] # do not modify in place!
- extension.name = pgo_module_name
- self._build_extension(extension, lib_dir, pgo_step_name='gen')
- # import and execute module code to generate profile
- so_module_path = os.path.join(lib_dir, pgo_module_name + self.so_ext)
- load_dynamic(pgo_module_name, so_module_path)
- def _cythonize(self, module_name, code, lib_dir, args, quiet=True):
- pyx_file = os.path.join(lib_dir, module_name + '.pyx')
- pyx_file = encode_fs(pyx_file)
- c_include_dirs = args.include
- c_src_files = list(map(str, args.src))
- if 'numpy' in code:
- import numpy
- c_include_dirs.append(numpy.get_include())
- with io.open(pyx_file, 'w', encoding='utf-8') as f:
- f.write(code)
- extension = Extension(
- name=module_name,
- sources=[pyx_file] + c_src_files,
- include_dirs=c_include_dirs,
- library_dirs=args.library_dirs,
- extra_compile_args=args.compile_args,
- extra_link_args=args.link_args,
- libraries=args.lib,
- language='c++' if args.cplus else 'c',
- )
- try:
- opts = dict(
- quiet=quiet,
- annotate=args.annotate,
- force=True,
- )
- if args.language_level is not None:
- assert args.language_level in (2, 3)
- opts['language_level'] = args.language_level
- elif sys.version_info[0] >= 3:
- opts['language_level'] = 3
- return cythonize([extension], **opts)
- except CompileError:
- return None
- def _build_extension(self, extension, lib_dir, temp_dir=None, pgo_step_name=None, quiet=True):
- build_extension = self._get_build_extension(
- extension, lib_dir=lib_dir, temp_dir=temp_dir, pgo_step_name=pgo_step_name)
- old_threshold = None
- try:
- if not quiet:
- old_threshold = distutils.log.set_threshold(distutils.log.DEBUG)
- build_extension.run()
- finally:
- if not quiet and old_threshold is not None:
- distutils.log.set_threshold(old_threshold)
- def _add_pgo_flags(self, build_extension, step_name, temp_dir):
- compiler_type = build_extension.compiler.compiler_type
- if compiler_type == 'unix':
- compiler_cmd = build_extension.compiler.compiler_so
- # TODO: we could try to call "[cmd] --version" for better insights
- if not compiler_cmd:
- pass
- elif 'clang' in compiler_cmd or 'clang' in compiler_cmd[0]:
- compiler_type = 'clang'
- elif 'icc' in compiler_cmd or 'icc' in compiler_cmd[0]:
- compiler_type = 'icc'
- elif 'gcc' in compiler_cmd or 'gcc' in compiler_cmd[0]:
- compiler_type = 'gcc'
- elif 'g++' in compiler_cmd or 'g++' in compiler_cmd[0]:
- compiler_type = 'gcc'
- config = PGO_CONFIG.get(compiler_type)
- orig_flags = []
- if config and step_name in config:
- flags = [f.format(TEMPDIR=temp_dir) for f in config[step_name]]
- for extension in build_extension.extensions:
- orig_flags.append((extension.extra_compile_args, extension.extra_link_args))
- extension.extra_compile_args = extension.extra_compile_args + flags
- extension.extra_link_args = extension.extra_link_args + flags
- else:
- print("No PGO %s configuration known for C compiler type '%s'" % (step_name, compiler_type),
- file=sys.stderr)
- return orig_flags
- @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 _clear_distutils_mkpath_cache(self):
- """clear distutils mkpath cache
- prevents distutils from skipping re-creation of dirs that have been removed
- """
- try:
- from distutils.dir_util import _path_created
- except ImportError:
- pass
- else:
- _path_created.clear()
- def _get_build_extension(self, extension=None, lib_dir=None, temp_dir=None,
- pgo_step_name=None, _build_ext=build_ext):
- self._clear_distutils_mkpath_cache()
- dist = Distribution()
- config_files = dist.find_config_files()
- try:
- config_files.remove('setup.cfg')
- except ValueError:
- pass
- dist.parse_config_files(config_files)
- if not temp_dir:
- temp_dir = lib_dir
- add_pgo_flags = self._add_pgo_flags
- if pgo_step_name:
- base_build_ext = _build_ext
- class _build_ext(_build_ext):
- def build_extensions(self):
- add_pgo_flags(self, pgo_step_name, temp_dir)
- base_build_ext.build_extensions(self)
- build_extension = _build_ext(dist)
- build_extension.finalize_options()
- if temp_dir:
- temp_dir = encode_fs(temp_dir)
- build_extension.build_temp = temp_dir
- if lib_dir:
- lib_dir = encode_fs(lib_dir)
- build_extension.build_lib = lib_dir
- if extension is not None:
- build_extension.extensions = [extension]
- return build_extension
- @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
- __doc__ = __doc__.format(
- # rST doesn't see the -+ flag as part of an option list, so we
- # hide it from the module-level docstring.
- CYTHON_DOC=dedent(CythonMagics.cython.__doc__\
- .replace('-+, --cplus', '--cplus ')),
- CYTHON_INLINE_DOC=dedent(CythonMagics.cython_inline.__doc__),
- CYTHON_PYXIMPORT_DOC=dedent(CythonMagics.cython_pyximport.__doc__),
- )
|