diff --git a/src/sage/misc/cython.py b/src/sage/misc/cython.py index adf2d8a10ef..29c013eab19 100644 --- a/src/sage/misc/cython.py +++ b/src/sage/misc/cython.py @@ -24,6 +24,8 @@ import re import sys import shutil +import webbrowser +from pathlib import Path from sage.env import (SAGE_LOCAL, cython_aliases, sage_include_directories) @@ -78,9 +80,16 @@ def _standard_libs_libdirs_incdirs_aliases(): sequence_number = {} +def _webbrowser_open_file(path): + """ + Open a html file in a web browser. + """ + webbrowser.open(Path(path).as_uri()) + + def cython(filename, verbose=0, compile_message=False, - use_cache=False, create_local_c_file=False, annotate=True, sage_namespace=True, - create_local_so_file=False): + use_cache=False, create_local_c_file=False, annotate=True, view_annotate=False, + view_annotate_callback=_webbrowser_open_file, sage_namespace=True, create_local_so_file=False): r""" Compile a Cython file. This converts a Cython file to a C (or C++ file), and then compiles that. The .c file and the .so file are @@ -110,6 +119,14 @@ def cython(filename, verbose=0, compile_message=False, in the temporary directory, but if ``create_local_c_file`` is also True, then save a copy of the .html file in the current directory. + - ``view_annotate`` -- boolean (default: ``False``); if ``True``, open the + annotated html file in a web browser + + - ``view_annotate_callback`` -- function; a function that takes a string + being the path to the html file. This can be overridden to change + what to do with the annotated html file. Have no effect unless + ``view_annotate`` is ``True``. + - ``sage_namespace`` -- boolean (default: ``True``); if ``True``, import ``sage.all`` @@ -226,6 +243,34 @@ def cython(filename, verbose=0, compile_message=False, ....: from sage.misc.cachefunc cimport cache_key ....: ''') + Test ``view_annotate``:: + + sage: cython(''' + ....: def f(int n): + ....: return n*n + ....: ''', view_annotate=True) # optional -- webbrowser + + :: + + sage: cython(''' + ....: def f(int n): + ....: return n*n + ....: ''', view_annotate=True, annotate=False) + Traceback (most recent call last): + ... + ValueError: cannot view annotated file without creating it + + :: + + sage: collected_paths = [] + sage: cython(''' + ....: def f(int n): + ....: return n*n + ....: ''', view_annotate=True, view_annotate_callback=collected_paths.append) + sage: collected_paths + ['...'] + sage: len(collected_paths) + 1 """ if not filename.endswith('pyx'): print("Warning: file (={}) should have extension .pyx".format(filename), file=sys.stderr) @@ -381,6 +426,11 @@ def cython(filename, verbose=0, compile_message=False, shutil.copy(os.path.join(target_dir, name + ".html"), os.curdir) + if view_annotate: + if not annotate: + raise ValueError("cannot view annotated file without creating it") + view_annotate_callback(os.path.join(target_dir, name + ".html")) + # This emulates running "setup.py build" with the correct options # # setuptools plugins considered harmful: diff --git a/src/sage/repl/ipython_extension.py b/src/sage/repl/ipython_extension.py index 329a7b9b95a..0818b0033b2 100644 --- a/src/sage/repl/ipython_extension.py +++ b/src/sage/repl/ipython_extension.py @@ -65,6 +65,8 @@ """ from IPython.core.magic import Magics, magics_class, line_magic, cell_magic +from IPython.core.display import HTML +from IPython.core.getipython import get_ipython from sage.repl.load import load_wrap from sage.env import SAGE_IMPORTALL, SAGE_STARTUP_FILE @@ -72,6 +74,14 @@ from sage.misc.misc import run_once +def _running_in_notebook(): + try: + from ipykernel.zmqshell import ZMQInteractiveShell + except ImportError: + return False + return isinstance(get_ipython(), ZMQInteractiveShell) + + @magics_class class SageMagics(Magics): @@ -348,6 +358,12 @@ def cython(self, line, cell): This is syntactic sugar on the :func:`~sage.misc.cython.cython_compile` function. + Note that there is also the ``%%cython`` cell magic provided by Cython, + which can be loaded with ``%load_ext cython``, see + `Cython documentation `_ + for more details. + The semantic is slightly different from the version provided by Sage. + INPUT: - ``line`` -- parsed as keyword arguments. The allowed arguments are: @@ -357,12 +373,20 @@ def cython(self, line, cell): - ``--use-cache`` - ``--create-local-c-file`` - ``--annotate`` + - ``--view-annotate`` - ``--sage-namespace`` - ``--create-local-so-file`` - ``--no-compile-message``, ``--no-use-cache``, etc. See :func:`~sage.misc.cython.cython` for details. + If ``--view-annotate`` is given, the annotation is either displayed + inline in the Sage notebook or opened in a new web browser, depending + on whether the Sage notebook is used. + + You can override the selection by specifying + ``--view-annotate=webbrowser`` or ``--view-annotate=displayhtml``. + - ``cell`` -- string; the Cython source code to process OUTPUT: none; the Cython code is compiled and loaded @@ -403,6 +427,45 @@ def cython(self, line, cell): ....: ''') UsageError: unrecognized arguments: --help + Test ``--view-annotate`` invalid arguments:: + + sage: # needs sage.misc.cython + sage: shell.run_cell(''' + ....: %%cython --view-annotate=xx + ....: print(1) + ....: ''') + UsageError: argument --view-annotate: invalid choice: 'xx' (choose from 'none', 'auto', 'webbrowser', 'displayhtml') + + Test ``--view-annotate=displayhtml`` (note that in a notebook environment + an inline HTML frame will be displayed):: + + sage: # needs sage.misc.cython + sage: shell.run_cell(''' + ....: %%cython --view-annotate=displayhtml + ....: print(1) + ....: ''') + 1 + + + Test ``--view-annotate=webbrowser``:: + + sage: # needs sage.misc.cython webbrowser + sage: shell.run_cell(''' + ....: %%cython --view-annotate + ....: print(1) + ....: ''') + 1 + sage: shell.run_cell(''' + ....: %%cython --view-annotate=auto + ....: print(1) + ....: ''') # --view-annotate=auto is undocumented feature, equivalent to --view-annotate + 1 + sage: shell.run_cell(''' + ....: %%cython --view-annotate=webbrowser + ....: print(1) + ....: ''') + 1 + Test invalid quotes:: sage: # needs sage.misc.cython @@ -434,10 +497,26 @@ def error(self, message): parser.add_argument("--use-cache", action=argparse.BooleanOptionalAction) parser.add_argument("--create-local-c-file", action=argparse.BooleanOptionalAction) parser.add_argument("--annotate", action=argparse.BooleanOptionalAction) + parser.add_argument("--view-annotate", choices=["none", "auto", "webbrowser", "displayhtml"], + nargs="?", const="auto", default="none") parser.add_argument("--sage-namespace", action=argparse.BooleanOptionalAction) parser.add_argument("--create-local-so-file", action=argparse.BooleanOptionalAction) args = parser.parse_args(shlex.split(line)) - return cython_compile(cell, **{k: v for k, v in args.__dict__.items() if v is not None}) + view_annotate = args.view_annotate + del args.view_annotate + if view_annotate == "auto": + if _running_in_notebook(): + view_annotate = "displayhtml" + else: + view_annotate = "webbrowser" + args_dict = {k: v for k, v in args.__dict__.items() if v is not None} + if view_annotate != "none": + args_dict["view_annotate"] = True + if view_annotate == "displayhtml": + path_to_annotate_html_container = [] + cython_compile(cell, **args_dict, view_annotate_callback=path_to_annotate_html_container.append) + return HTML(filename=path_to_annotate_html_container[0]) + return cython_compile(cell, **args_dict) @cell_magic def fortran(self, line, cell):