From c0e31a3d44e048a8a758764345ef5b136ecb5433 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 17 Mar 2024 17:45:53 +0000 Subject: [PATCH 1/6] Automatically load IPython extension --- pysrc/juliacall/__init__.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index d9e7a594..ac386cfa 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -248,8 +248,41 @@ def jlstr(x): "PYTHON_JULIACALL_HANDLE_SIGNALS=no." ) -init() + # Next, automatically load the juliacall extension if we are in a Jupyter notebook + CONFIG['autoload_extensions'] = choice('autoload_extensions', ['yes', 'no'])[0] + if CONFIG['autoload_extensions'] in {'yes', None}: + try: + # Get IPython `InteractiveShell` instance + get_ipython = sys.modules['IPython'].get_ipython + + # This line is a check for the IPython kernel being loaded to the + # interactive shell, so that we don't activate for other types of + # interactive shells). + if 'IPKernelApp' not in get_ipython().config: + raise ImportError('console') + + if CONFIG['autoload_extensions'] is None: + # Only let the user know if it was not explicitly set + print( + "Detected Jupyter notebook. Loading juliacall extension. To disable, you can either " + "set the environment variable PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS=no or pass the " + "command line argument '-X juliacall-autoload-extensions=no'. Inside Jupyter, you can " + "do this with `import os; os.environ['PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS'] = 'no'`. " + "To suppress this message, set PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS=yes." + ) + + load_ipython_extension(get_ipython()) + except Exception as e: + if CONFIG['autoload_extensions'] == 'yes': + # Only warn if the user explicitly requested the extension to be loaded + warnings.warn( + "Could not load juliacall extension in Jupyter notebook: " + str(e) + ) + pass + def load_ipython_extension(ip): import juliacall.ipython juliacall.ipython.load_ipython_extension(ip) + +init() From d842b8abda95a88ea1f38a9bb8ba608baed38cf9 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 17 Mar 2024 17:57:37 +0000 Subject: [PATCH 2/6] Test functions in notebook test --- pytest/test_nb.ipynb | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 pytest/test_nb.ipynb diff --git a/pytest/test_nb.ipynb b/pytest/test_nb.ipynb new file mode 100644 index 00000000..f7e2b98f --- /dev/null +++ b/pytest/test_nb.ipynb @@ -0,0 +1,139 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "import numpy as np\n", + "from juliacall import Main as jl" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + } + ], + "source": [ + "%%julia\n", + "\n", + "# Automatically activates Julia magic\n", + "\n", + "x = 1\n", + "println(x + 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n" + ] + } + ], + "source": [ + "%julia println(x + 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jl.cos(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "f (generic function with 1 method)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%julia\n", + "\n", + "function f(x::AbstractArray)\n", + " sum(x)\n", + "end" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jl.f(np.array([1, 2, 3]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 88dcb5fc982656d3171db8f04cfd9e77409f16a1 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 17 Mar 2024 17:57:56 +0000 Subject: [PATCH 3/6] Add unittest for Jupyter extension --- .github/workflows/tests.yml | 2 +- pytest/test_all.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58535f98..09707c93 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,7 +70,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov + pip install flake8 pytest pytest-cov nbval numpy cp pysrc/juliacall/juliapkg-dev.json pysrc/juliacall/juliapkg.json pip install -e . - name: Lint with flake8 diff --git a/pytest/test_all.py b/pytest/test_all.py index c6cff009..211e8b67 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -75,3 +75,21 @@ def test_issue_433(): """ ) assert out == 25 + +def test_notebook(): + import os + import subprocess + import sys + from pathlib import Path + + result = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "--nbval", + str(Path(__file__).parent / "test_nb.ipynb"), + ], + env=os.environ, + ) + assert result.returncode == 0 From bd123352e22112b456efa8ebac30bfb9eafb1f46 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 17 Mar 2024 20:40:14 +0000 Subject: [PATCH 4/6] Change parameter name to `autoload_ipython_extension` --- pysrc/juliacall/__init__.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index ac386cfa..0689d3d4 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -248,32 +248,25 @@ def jlstr(x): "PYTHON_JULIACALL_HANDLE_SIGNALS=no." ) - # Next, automatically load the juliacall extension if we are in a Jupyter notebook - CONFIG['autoload_extensions'] = choice('autoload_extensions', ['yes', 'no'])[0] - if CONFIG['autoload_extensions'] in {'yes', None}: + # Next, automatically load the juliacall extension if we are in IPython or Jupyter + CONFIG['autoload_ipython_extension'] = choice('autoload_ipython_extension', ['yes', 'no'])[0] + if CONFIG['autoload_ipython_extension'] in {'yes', None}: try: - # Get IPython `InteractiveShell` instance get_ipython = sys.modules['IPython'].get_ipython - # This line is a check for the IPython kernel being loaded to the - # interactive shell, so that we don't activate for other types of - # interactive shells). - if 'IPKernelApp' not in get_ipython().config: - raise ImportError('console') - - if CONFIG['autoload_extensions'] is None: + if CONFIG['autoload_ipython_extension'] is None: # Only let the user know if it was not explicitly set print( - "Detected Jupyter notebook. Loading juliacall extension. To disable, you can either " - "set the environment variable PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS=no or pass the " - "command line argument '-X juliacall-autoload-extensions=no'. Inside Jupyter, you can " - "do this with `import os; os.environ['PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS'] = 'no'`. " - "To suppress this message, set PYTHON_JULIACALL_AUTOLOAD_EXTENSIONS=yes." + "Detected IPython. Loading juliacall extension. To disable, you can either " + "set the environment variable PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=no or pass the " + "command line argument '-X juliacall-autoload-ipython-extension=no'. Inside Jupyter, you can " + "do this with `import os; os.environ['PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION'] = 'no'`. " + "To suppress this message, set PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=yes." ) load_ipython_extension(get_ipython()) except Exception as e: - if CONFIG['autoload_extensions'] == 'yes': + if CONFIG['autoload_ipython_extension'] == 'yes': # Only warn if the user explicitly requested the extension to be loaded warnings.warn( "Could not load juliacall extension in Jupyter notebook: " + str(e) From dfe5b84efc88637e3f774a92dda3ac5dffed14e5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 17 Mar 2024 21:00:30 +0000 Subject: [PATCH 5/6] Combine pytest into single command --- .github/workflows/tests.yml | 2 +- pytest/test_all.py | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09707c93..690c1327 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,7 +81,7 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Run tests run: | - pytest -s --cov=pysrc + pytest -s --nbval --cov=pysrc ./pytest/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 env: diff --git a/pytest/test_all.py b/pytest/test_all.py index 211e8b67..c6cff009 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -75,21 +75,3 @@ def test_issue_433(): """ ) assert out == 25 - -def test_notebook(): - import os - import subprocess - import sys - from pathlib import Path - - result = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - "--nbval", - str(Path(__file__).parent / "test_nb.ipynb"), - ], - env=os.environ, - ) - assert result.returncode == 0 From 1672826daac0202ce8a603d6b458c3aa8927665f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Wed, 27 Mar 2024 01:13:05 +0000 Subject: [PATCH 6/6] Refactor documentation of extension autoloading --- docs/src/compat.md | 8 ++++++-- docs/src/juliacall.md | 1 + pysrc/juliacall/__init__.py | 6 +----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/src/compat.md b/docs/src/compat.md index 7df0e235..751a2053 100644 --- a/docs/src/compat.md +++ b/docs/src/compat.md @@ -67,8 +67,9 @@ The `juliacall` IPython extension adds these features to your IPython session: The extension is experimental and unstable - the API can change at any time. -Enable the extension with `%load_ext juliacall`. -See [the IPython docs](https://ipython.readthedocs.io/en/stable/config/extensions/). +You can explicitly enable the extension with `%load_ext juliacall`, but +it will automatically be loaded if `juliacall` is imported and IPython is detected. +You can disable this behavior with an [environment variable](@ref julia-config). The `%%julia` cell magic can synchronise variables between Julia and Python by listing them on the first line: @@ -88,6 +89,9 @@ In [5]: z Out[5]: '2^8 = 256' ``` +Also see [the IPython docs](https://ipython.readthedocs.io/en/stable/config/extensions/) +for more information on extensions. + ## Asynchronous Julia code (including Makie) Asynchronous Julia code will not normally run while Python is executing, unless it is in a diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md index 98d74784..a16b71f2 100644 --- a/docs/src/juliacall.md +++ b/docs/src/juliacall.md @@ -123,3 +123,4 @@ be configured in two ways: | `-X juliacall-sysimage=` | `PYTHON_JULIACALL_SYSIMAGE=` | Use the given system image. | | `-X juliacall-threads=` | `PYTHON_JULIACALL_THREADS=` | Launch N threads. | | `-X juliacall-warn-overwrite=` | `PYTHON_JULIACALL_WARN_OVERWRITE=` | Enable or disable method overwrite warnings. | +| `-X juliacall-autoload-ipython-extension=` | `PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=` | Enable or disable IPython extension autoloading. | diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index 0689d3d4..54b1100f 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -257,11 +257,7 @@ def jlstr(x): if CONFIG['autoload_ipython_extension'] is None: # Only let the user know if it was not explicitly set print( - "Detected IPython. Loading juliacall extension. To disable, you can either " - "set the environment variable PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=no or pass the " - "command line argument '-X juliacall-autoload-ipython-extension=no'. Inside Jupyter, you can " - "do this with `import os; os.environ['PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION'] = 'no'`. " - "To suppress this message, set PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=yes." + "Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython" ) load_ipython_extension(get_ipython())