diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58535f98..690c1327 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 @@ -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/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 d9e7a594..54b1100f 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -248,8 +248,30 @@ def jlstr(x): "PYTHON_JULIACALL_HANDLE_SIGNALS=no." ) -init() + # 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 = sys.modules['IPython'].get_ipython + + if CONFIG['autoload_ipython_extension'] is None: + # Only let the user know if it was not explicitly set + print( + "Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython" + ) + + load_ipython_extension(get_ipython()) + except Exception as e: + 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) + ) + pass + def load_ipython_extension(ip): import juliacall.ipython juliacall.ipython.load_ipython_extension(ip) + +init() 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 +}