diff --git a/.github/workflows/ci-tests-colab.yml b/.github/workflows/ci-tests-colab.yml index fd4cbcee..1536e823 100644 --- a/.github/workflows/ci-tests-colab.yml +++ b/.github/workflows/ci-tests-colab.yml @@ -89,7 +89,6 @@ jobs: # install geckodriver driver_path=$(python -c ' - import shutil from pathlib import Path import geckodriver_autoinstaller diff --git a/.github/workflows/ci-tests-jupyter.yml b/.github/workflows/ci-tests-jupyter.yml index 10f5f0c6..0371b937 100644 --- a/.github/workflows/ci-tests-jupyter.yml +++ b/.github/workflows/ci-tests-jupyter.yml @@ -122,7 +122,6 @@ jobs: # install geckodriver driver_path=$(python -c ' - import shutil from pathlib import Path import geckodriver_autoinstaller diff --git a/davos/core/config.py b/davos/core/config.py index 00c76ac0..9fe07dbb 100644 --- a/davos/core/config.py +++ b/davos/core/config.py @@ -20,6 +20,7 @@ import os import pprint +import shlex import shutil import site import sys @@ -29,6 +30,8 @@ from io import StringIO from os.path import expandvars from pathlib import Path +from locale import getpreferredencoding +from subprocess import CalledProcessError, check_output from davos.core.exceptions import ( DavosConfigError, @@ -199,6 +202,7 @@ def __init__(self): self._conda_envs_dirs = None self._default_pip_executable = self._find_default_pip_executable() self._ipy_showsyntaxerror_orig = None + self._jupyter_interface = _get_jupyter_interface() self._repr_formatter = pprint.PrettyPrinter() if sys.version_info.minor >= 8: # sort_dicts constructor param added in Python 3.8, defaults @@ -558,6 +562,75 @@ def _block_greedy_ipython_completer(): raise Exception +def _get_jupyter_interface(): + """ + Determines whether the notebook is being run through the "classic" + Jupyter notebook interface or JupyterLab. Used to set the value of + `davos.config._jupyter_interface`. + + Returns + ------- + interface : str + "notebook" for classic Jupyter notebooks (or unknown); "lab" for + JupyterLab. + + Notes + ----- + 1. This distinction is needed because recent versions of the + `jupyterlab` package no longer depend on `notebook`, so if the + user is running JupyterLab, some `jupyter notebook ...` shell + commands davos runs internally may not be available and will need + to be run as `jupyter lab ...` commands instead. + 2. "notebook" is treated as a strong default assumption and returned + if the interface cannot be determined, for a few reasons: + - Only more recent JupyterLab versions have dropped `notebook` as + a dependency, so it's less likely the user will have JupyterLab + without `notebook` than vice versa. + - IDEs tend to run simple notebook servers rather than JupyterLab + for custom interfaces, but the command to launch the server may + not be the immediate parent process in that case and trying to + check all processes introduces a cascade of other issues. + - Colab notebooks also use the "classic" notebook server and make + up a fairly large percentage of Davos uses, but the Colab VM + environment is changed frequently and without notice, so it's + more likely to break or otherwise mess with users/packages' + ability to query running processes and/or the notebook server, + causing this function to return whatever is chosen as the + fallback/default value. + 3. `subprocess.check_output` is called directly rather than using + `davos.core.core.run_shell_command` like most other davos + functions that run shell commands. In IPython environments, + `run_shell_command` internally calls + `IPython.utils.process.system`, which for some strange reason + truncates the stdout from this particular command at 80 columns + rather than wrapping it like it does with seemingly every other + command. The shell commands we need to get from the output + of `ps` can be quite long because they include multiple absolute + paths, and the info we care about may not be in the first 80 + characters. + """ + cmd = f'ps -o command= -p {os.getppid()}' + try: + parent_proc_cmd = check_output(shlex.split(cmd), + encoding=getpreferredencoding()) + except (FileNotFoundError, CalledProcessError): + # FileNotFoundError: `ps` command not available + # CalledProcessError: command failed for any other reason + interface = 'notebook' + else: + # when launched normally from the command line, the 2nd item in + # the list should be the notebook/lab executable, but safer to + # check more generally in case the user has something unusual + # like a custom script they called to launch the server + for item in parent_proc_cmd.split(): + if item.endswith(('notebook', 'lab')): + interface = item.split('-')[-1] + break + else: + interface = 'notebook' + return interface + + def _get_stdlib_modules(): """ Get names of standard library modules. diff --git a/davos/core/config.pyi b/davos/core/config.pyi index 10321c5a..e2c5a0fc 100644 --- a/davos/core/config.pyi +++ b/davos/core/config.pyi @@ -36,6 +36,7 @@ class DavosConfig(metaclass=SingletonConfig): _environment: _Environment _ipy_showsyntaxerror_orig: _IpyShowSyntaxErrorPre7 | _IpyShowSyntaxErrorPost7 | None _ipython_shell: IpythonShell | None + _jupyter_interface: Literal['notebook', 'lab'] _noninteractive: bool _pip_executable: str _project: AbstractProject | ConcreteProject | None @@ -102,4 +103,5 @@ class DavosConfig(metaclass=SingletonConfig): def _find_default_pip_executable(self) -> str: ... def _block_greedy_ipython_completer() -> None: ... +def _get_jupyter_interface() -> Literal['notebook', 'lab']: ... def _get_stdlib_modules() -> frozenset[str]: ... diff --git a/davos/core/core.py b/davos/core/core.py index 527c18ba..e4c001da 100644 --- a/davos/core/core.py +++ b/davos/core/core.py @@ -30,6 +30,7 @@ import importlib import itertools import sys +import warnings from contextlib import contextmanager, redirect_stdout from io import StringIO from pathlib import Path @@ -50,7 +51,7 @@ OnionParserError, ParserNotImplementedError, SmugglerError, - TheNightIsDarkAndFullOfTErrors + TheNightIsDarkAndFullOfErrors ) from davos.core.parsers import pip_parser from davos.core.regexps import ( @@ -967,8 +968,8 @@ def smuggle_wrapper(*args, **kwargs): # invalidate sys.meta_path finder caches so the global # working set is regenerated based on the updated sys.path. # Note: after pretty extensive spot checking, I haven't - # managed found a case where this is actually since - # migrating to importlib.metadata instead of pkg_resources, + # managed to find a case where this is actually necessary + # since migrating from pkg_resources to importlib.metadata, # but the docs recommend it and the overhead is extremely # minor, so probably worth including in case the user or # notebook environment has implemented some unusual custom @@ -1041,7 +1042,7 @@ def smuggle( pkg_name = name.split('.')[0] if pkg_name == 'davos': - raise TheNightIsDarkAndFullOfTErrors("Don't do that.") + raise TheNightIsDarkAndFullOfErrors("Don't do that.") onion = Onion(pkg_name, installer=installer, args_str=args_str, **installer_kwargs) @@ -1102,6 +1103,7 @@ def smuggle( failed_reloads = [] for dep_name in prev_imported_pkgs: dep_modules_old = {} + top_level_names_old = [] for mod_name in tuple(sys.modules.keys()): # remove submodules of previously imported packages so # new versions get imported when main package is @@ -1113,6 +1115,23 @@ def smuggle( # run, which crashes it... (-_-* ) if mod_name.startswith(f'{dep_name}.'): dep_modules_old[mod_name] = sys.modules.pop(mod_name) + # when reloading package below, importlib.reload + # doesn't seem to automatically follow and + # recursively reload submodules/subpackages loaded + # into the top-level module via relative import + # (e.g., `from . import submodule`) based on their + # *new* locations, if different from their old + # locations. So if a previously smuggled package + # came from the user's main Python environment, and + # the just-smuggled version is now in a project + # directory, the old subpackage/submodule object + # will be re-used in the new top-level module's + # namespace unless we explicitly remove them here + # and force their loaders' paths to be recomputed + submod_name = mod_name[len(dep_name) + 1:] + if submod_name in sys.modules[dep_name].__dict__: + top_level_names_old.append(submod_name) + del sys.modules[dep_name].__dict__[submod_name] # get (but don't pop) top-level package to that it can be # reloaded (must exist in sys.modules) @@ -1122,7 +1141,9 @@ def smuggle( except (ImportError, RuntimeError): # if we aren't able to reload the module, put the old # version's submodules we removed back in sys.modules - # for now and prepare to show a warning post-execution. + # for now, add their names back to the top-level + # module's __dict__, and prepare to show a warning + # post-execution. # This way: # 1. the user still has a working module until they # restart the runtime @@ -1130,6 +1151,10 @@ def smuggle( # we try to reload/import other modules that # import it sys.modules.update(dep_modules_old) + for submod_name in top_level_names_old: + sys.modules[dep_name].__dict__[submod_name] = ( + dep_modules_old[f'{dep_name}.{submod_name}'] + ) failed_reloads.append(dep_name) if any(failed_reloads): @@ -1158,6 +1183,30 @@ def smuggle( raise SmugglerError(msg) else: prompt_restart_rerun_buttons(failed_reloads) + # if the function above returns, the user has chosen to + # continue running the notebook rather than restarting + # to properly reload the package. Issue a warning to let + # them know to proceed with caution + if len(failed_reloads) == 1: + failed_reloads_str = failed_reloads[0] + verb = 'was' + failed_ver_string = f'{failed_reloads_str}.__version__' + else: + verb = 'were' + failed_ver_string = "These packages' '__version__' attributes" + if len(failed_reloads) == 2: + failed_reloads_str = " and ".join(failed_reloads) + else: + failed_reloads_str = ( + f"{', '.join(failed_reloads[:-1])}, and " + f"{failed_reloads[-1]}" + ) + + msg = ( + f"{failed_reloads_str} {verb} partially reloaded. " + f"{failed_ver_string} may be misleading." + ) + warnings.warn(msg, RuntimeWarning, stacklevel=3) if ( config._project is None and diff --git a/davos/core/exceptions.py b/davos/core/exceptions.py index 46b35e09..131260c4 100644 --- a/davos/core/exceptions.py +++ b/davos/core/exceptions.py @@ -201,7 +201,7 @@ class SmugglerError(DavosError): """Base class for errors raised during the smuggle phase.""" -class TheNightIsDarkAndFullOfTErrors(SmugglerError): +class TheNightIsDarkAndFullOfErrors(SmugglerError): """A little Easter egg for anyone who tries to `smuggle davos`.""" diff --git a/davos/core/exceptions.pyi b/davos/core/exceptions.pyi index 1882211b..5879625a 100644 --- a/davos/core/exceptions.pyi +++ b/davos/core/exceptions.pyi @@ -29,7 +29,7 @@ class ProjectNotebookNotFoundError(DavosProjectError, FileNotFoundError): ... class SmugglerError(DavosError): ... -class TheNightIsDarkAndFullOfTErrors(SmugglerError): ... +class TheNightIsDarkAndFullOfErrors(SmugglerError): ... class InstallerError(SmugglerError, CalledProcessError): show_output: bool diff --git a/davos/core/project.py b/davos/core/project.py index f86cc65a..ee1a3e26 100644 --- a/davos/core/project.py +++ b/davos/core/project.py @@ -52,6 +52,7 @@ import warnings from os.path import expandvars from pathlib import Path +from subprocess import CalledProcessError from urllib.request import urlopen from urllib.parse import parse_qs, unquote, urlencode, urljoin, urlparse @@ -256,12 +257,12 @@ def remove(self, yes=False): if not yes: if config.noninteractive: raise DavosProjectError( - "To remove a project when noninteractive mode is " - "enabled, you must explicitly pass 'yes=True'." + "To remove a project when noninteractive mode is enabled, " + "you must explicitly pass 'yes=True'." ) prompt = f"Remove project {self.name!r} and all installed packages?" confirmed = prompt_input(prompt, default='n') - if not confirmed: + if not confirmed and not config.suppress_stdout: print(f"{self.name} not removed") return shutil.rmtree(self.project_dir) @@ -645,8 +646,18 @@ def get_notebook_path(): kernel_filepath = ipykernel.connect.get_connection_file() kernel_id = kernel_filepath.split('/kernel-')[-1].split('.json')[0] - running_nbservers_stdout = run_shell_command('jupyter notebook list', - live_stdout=False) + nbserver_list_cmd = f'jupyter {config._jupyter_interface} list' + try: + running_nbservers_stdout = run_shell_command(nbserver_list_cmd, + live_stdout=False) + except CalledProcessError as e: + # raise RuntimeError so it's caught by `use_default_project` and + # the fallback project is used + raise RuntimeError( + "Shell command to get running Jupyter servers " + f"({nbserver_list_cmd}) failed" + ) from e + for line in running_nbservers_stdout.splitlines(): # should only need to exclude first line ("Currently running # servers:"), but handle safely in case output format changes in @@ -773,6 +784,12 @@ def prune_projects(yes=False): the interpreter is shut down -- they're only checked for and dealt with here as a fallback in case one somehow sneaks through. """ + if config.noninteractive and not yes: + raise DavosProjectError( + "To remove projects when noninteractive mode is enabled, you must " + "explicitly pass 'yes=True'." + ) + # dict of projects to remove -- keys: "safe"-formatted project # directory names; values: corresponding notebook filepaths to_remove = {} @@ -848,7 +865,7 @@ def prune_projects(yes=False): clear_output(wait=False) # print final status for all projects processed print(template.format(*statuses)) - else: + elif not config.suppress_stdout: print("No unused projects found.") diff --git a/davos/implementations/js_functions.py b/davos/implementations/js_functions.py index eb496045..65a09d53 100644 --- a/davos/implementations/js_functions.py +++ b/davos/implementations/js_functions.py @@ -135,7 +135,7 @@ def __setattr__(self, name, value): * Value sent to the notebook kernel's stdin socket if the * given button is clicked and sendResult is true. Used to * forward user input information to Python. JS types are - * converted ty Python types, within reason (Boolean -> bool, + * converted to Python types, within reason (Boolean -> bool, * Object -> dict, Array -> list, null -> None, * undefined -> '', etc.). If omitted, the return value of * onClick will be used instead. diff --git a/paper/figs/example1.pdf b/paper/figs/example1.pdf index 5b8a02f6..a130bf94 100644 Binary files a/paper/figs/example1.pdf and b/paper/figs/example1.pdf differ diff --git a/paper/figs/example2.pdf b/paper/figs/example2.pdf index 425cd1ed..843adab6 100644 Binary files a/paper/figs/example2.pdf and b/paper/figs/example2.pdf differ diff --git a/paper/figs/example3.pdf b/paper/figs/example3.pdf index 308c7663..0f7f3464 100644 Binary files a/paper/figs/example3.pdf and b/paper/figs/example3.pdf differ diff --git a/paper/figs/example4.pdf b/paper/figs/example4.pdf index 506e3e93..715ecc85 100644 Binary files a/paper/figs/example4.pdf and b/paper/figs/example4.pdf differ diff --git a/paper/figs/example5.pdf b/paper/figs/example5.pdf index aa4f9357..b8a86719 100644 Binary files a/paper/figs/example5.pdf and b/paper/figs/example5.pdf differ diff --git a/paper/figs/example6.pdf b/paper/figs/example6.pdf index 66d36e6a..87022a88 100644 Binary files a/paper/figs/example6.pdf and b/paper/figs/example6.pdf differ diff --git a/paper/figs/example7.pdf b/paper/figs/example7.pdf index 4160c931..c2829cd2 100644 Binary files a/paper/figs/example7.pdf and b/paper/figs/example7.pdf differ diff --git a/paper/figs/example8.pdf b/paper/figs/example8.pdf index 399f076e..218a3152 100644 Binary files a/paper/figs/example8.pdf and b/paper/figs/example8.pdf differ diff --git a/paper/figs/illustrative_example.pdf b/paper/figs/illustrative_example.pdf index d150192a..10fe043b 100644 Binary files a/paper/figs/illustrative_example.pdf and b/paper/figs/illustrative_example.pdf differ diff --git a/paper/figs/shareable_code_2d.pdf b/paper/figs/shareable_code_2d.pdf new file mode 100644 index 00000000..17e56f70 Binary files /dev/null and b/paper/figs/shareable_code_2d.pdf differ diff --git a/paper/figs/snippet1.pdf b/paper/figs/snippet1.pdf index cb84eb81..fa630408 100644 Binary files a/paper/figs/snippet1.pdf and b/paper/figs/snippet1.pdf differ diff --git a/paper/figs/snippet5.pdf b/paper/figs/snippet5.pdf index 77f8e28a..706a1680 100644 Binary files a/paper/figs/snippet5.pdf and b/paper/figs/snippet5.pdf differ diff --git a/paper/figs/source/make_shareable_code_base.ipynb b/paper/figs/source/make_shareable_code_base.ipynb new file mode 100644 index 00000000..f9c30dbd --- /dev/null +++ b/paper/figs/source/make_shareable_code_base.ipynb @@ -0,0 +1,100 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "M0Qv4nlz2p7L", + "outputId": "f47a5803-36ff-4cae-9897-6669c02d306f" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Collecting davos\n", + " Downloading davos-0.2.2-py3-none-any.whl (99 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m99.7/99.7 kB\u001b[0m \u001b[31m1.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: packaging in /usr/local/lib/python3.10/dist-packages (from davos) (23.1)\n", + "Installing collected packages: davos\n", + "Successfully installed davos-0.2.2\n" + ] + } + ], + "source": [ + "%pip install davos\n", + "import davos" + ] + }, + { + "cell_type": "code", + "source": [ + "from matplotlib smuggle pyplot as plt\n", + "smuggle seaborn as sns" + ], + "metadata": { + "id": "re5FDpAE2sOi" + }, + "execution_count": 9, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "plt.figure(figsize=(7, 7))\n", + "plt.plot([0, 7], [0, 7], '--', linewidth=2, color='lightgray')\n", + "plt.plot([1, 2, 2, 3, 4, 5, 6], [1, 2, 3, 3, 4, 5, 6], 'ko', markersize=10)\n", + "\n", + "plt.xticks([])\n", + "plt.yticks([])\n", + "plt.xlim([0, 7.1])\n", + "plt.ylim([0, 7.1])\n", + "\n", + "sns.despine(top=True, right=True)\n", + "\n", + "plt.xlabel('Setup cost', fontsize=18)\n", + "plt.ylabel('Reproducibility', fontsize=18);\n", + "\n", + "plt.savefig('shareable_code_base.pdf', bbox_inches='tight')" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 605 + }, + "id": "FtPs8GRd2xaY", + "outputId": "928cf58a-1b52-4489-a9df-817808eadee7" + }, + "execution_count": 38, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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LCq7lgDYvn273n77bf3jP/6jrrrqKhUUFOjYsWP6wQ9+oAsvvFBf+tKXbKkLwHQEKCBFbd26NfT122+/Hfq6rKxM0uIPzaWlnVlF5ezu11ThzFvq6OgI67WSkpKwa1vIe+B2u+V0OiUptITBTM5+baaazjnnHN19993at2+fenp69PLLL+uaa66RJO3YsUPPPvts2DUBWDwEKCBF5eTkhL5etmxZ6Ot169ZJko4cObKgCc0Ox5mPFTPPYzYLCwslSW1tbTO+HgwGdfjw4Xmv99JLL837msPhCK1XFY6LL75YknTgwIE5A97ZMjIydO6550qSXnzxxVn3+9WvfhWqae3atXOe0+Fw6MMf/rCefvppVVZWhmqauo80//sNILYIUECSaWpq0jvvvDPvfrt37w59ffYP8k9+8pMqKCjQ2NiY7rzzzjl/MAeDwWkLSebl5UnStO1TTTyH7+c///mM19i9e/ecnZwJv/nNb2Z8aPHw8LAefPBBSdKGDRtCd7mF4/rrr5fT6VRPT4/uvffesI/767/+a0nS008/rT/+8Y/TXvd6vfr2t78tSbr66quVn58fem1kZGTW8zqdztANABOBaUK47zeAGLNj7QQAi+e5554zDofDXH311Wb37t2mqakp9Nro6Kh59dVXzfXXXx9aQ+mDH/xgaLHHCT/5yU9Cr1911VXm97//fWifQCBg3nzzTfOd73zHvPvd7zY//elPJx174MABI8k4nU7z29/+dtY6f/WrX4Wu8fnPf950d3cbY4zp7+833/3ud01GRoZxu91hLaTpdrvNU089FVpI86233got9ul0Os3/+T//J+L38Wtf+1qovhtvvNG88847odf6+/vNE088Ya655ppJx5y9kGZFRYXZt29f6H17/fXXzcUXXzzrQprnnXeeuf32281LL71kvF5vaHtHR4e57bbbQrVMXZvrM5/5jJFkLr74YtPb2xvx3xPAwhCggCTzy1/+MvTDduLPRBiZWKF84s/atWtNR0fHjOd5+OGHTUZGRmjfZcuWmaKiIpOenj7pHP/8z/886bixsTGzevXq0OuFhYWmqqrKVFVVmaeeemrSvv/pP/2nSecqKCgILWB5++23h7WQ5ne/+93Q9ZYtW2by8/MnLSL6ox/9aEHv4/j4uPnbv/3bSfXl5OSYwsLC0PuYn58/7bg33njDlJeXh47JzMw0eXl5k97Hqe/D2X+fiboLCgpMdnb2pOt/+ctfnnbcoUOHQvU4nU6zYsWK0PsNYPEQoIAk1NDQYHbs2GE++clPmjVr1pjc3FzjcDhMdna2qa+vN5/61KfME088Ma3zNFVTU5P5u7/7O3PeeeeZvLw843Q6TWFhobnwwgvN7bffbg4cODDjOdrb283nP/95U1NTMymETV0tOxAImB07dpjzzz/fuFwuk5eXZy655BLzs5/9zBgT3krku3btMv39/eZrX/uaqa+vN5mZmcbtdpu/+Iu/ML/73e8W/B5O+M1vfmM+85nPmMrKSrNs2TJTUFBg3vve95obb7xxxhXQjTnzqJdvfOMb5vzzzzc5OTlm2bJlpq6uztxyyy3m2LFjMx7z8ssvm/vuu89cdtllpra21mRlZZmMjAxTVVVlrrvuOvPiiy/OWuO+ffvM5ZdfboqKikIBlAEGYHFZxjDzEEDiqa6uVktLi3bt2qXrr7/e7nIApBgmkQMAAESIAAUAABAhAhQAAECECFAAAAARYhI5AABAhOhAAQAARIgABQAAECECFAAAQIQIUAAAABEiQAEAAESIAAUAABAhAhQAAECECFAAAAAR+n/NJC7a8md46wAAAABJRU5ErkJggg==\n" + }, + "metadata": {} + } + ] + } + ] +} \ No newline at end of file diff --git a/paper/figs/source/shareable_code_base.pdf b/paper/figs/source/shareable_code_base.pdf new file mode 100644 index 00000000..b3a25312 Binary files /dev/null and b/paper/figs/source/shareable_code_base.pdf differ diff --git a/paper/main.bib b/paper/main.bib index e81c1b20..552ae4dd 100644 --- a/paper/main.bib +++ b/paper/main.bib @@ -1,3 +1,35 @@ +@misc{skle22, + author = {scikit-learn developers}, + howpublished = {\url{https://scikit-learn.org/1.1/model_persistence.html}}, + month = {May}, + title = {{scikit-learn User Guide: 9. Model persistence}}, + year = {2022}} + +@techreport{HeimCann19, + author = {Christian Heimes and Brett Cannon}, + institution = {Python Software Foundation}, + month = {May}, + number = {594}, + title = {Removing dead batteries from the standard library}, + type = {PEP}, + year = {2019}} + +@techreport{Smit17, + author = {Eric V. Smith}, + institution = {Python Software Foundation}, + month = {June}, + number = {557}, + title = {Data Classes}, + type = {PEP}, + year = {2017}} + +@misc{MIND23, + author = {{MIND Team}}, + howpublished = {\url{https://mindsummerschool.org}}, + month = {August}, + title = {{Methods in Neuroscience at Dartmouth (MIND) Computational Summer School}}, + year = {2023}} + @misc{BickEtal07, author = {I Bicking and B G{\'{a}}bor and {Python Packaging Authority}}, howpublished = {\url{https://github.com/pypa/virtualenv}}, @@ -83,7 +115,7 @@ @misc{cond15 @techreport{CoghStuf13, author = {Nick Coghlan and Donald Stufft}, - institution = {{Python} Software Foundation}, + institution = {Python Software Foundation}, month = {March}, number = {440}, title = {Version {I}dentification and {D}ependency {S}pecification}, diff --git a/paper/main.pdf b/paper/main.pdf index a52b6cc5..51af8efd 100644 Binary files a/paper/main.pdf and b/paper/main.pdf differ diff --git a/paper/main.tex b/paper/main.tex index 61896d69..587aab10 100644 --- a/paper/main.tex +++ b/paper/main.tex @@ -10,6 +10,9 @@ \geometry{left=1in, right=1in, top=1in, bottom=1in, headsep=0pt} +\newcommand{\todo}[1]{\textcolor{red}{\textbf{TODO}: #1}} +\newcommand{\stoppedhere}{\bigskip\bigskip\textcolor{red}{\textbf{========== TODO: finish editing from here to end ==========}}\bigskip\bigskip} + \journal{SoftwareX} \begin{document} @@ -66,9 +69,9 @@ \section*{Current code version} \hline \textbf{Nr.} & \textbf{Code metadata description} & \textbf{Metadata value} \\ \hline -C1 & Current code version & v0.2.0 \\ +C1 & Current code version & v0.2.4 \\ \hline -C2 & Permanent link to code/repository used for this code version & \url{https://github.com/ContextLab/davos/tree/v0.2.0} \\ +C2 & Permanent link to code/repository used for this code version & \url{https://github.com/ContextLab/davos/tree/v0.2.4} \\ \hline C3 & Code Ocean compute capsule & \\ \hline @@ -79,7 +82,7 @@ \section*{Current code version} C6 & Software code languages, tools, and services used & Python, JavaScript, PyPI/pip, IPython, Jupyter, ipykernel, PyZMQ.\newline Additional tools used for tests: pytest, Selenium, Requests, mypy, GitHub Actions \\ \hline C7 & Compilation requirements, operating environments, and - dependencies & Dependencies:~Python $\geq 3.6$, packaging, setuptools.\newline Supported OSes: MacOS, Linux, Unix-like.\newline Supported IPython environments: Jupyter Notebooks, JupyterLab, Google Colaboratory, Binder, IDE-based notebook editors. \\ + dependencies & Dependencies:~Python $\geq 3.6$, packaging, setuptools.\newline Supported OSes: MacOS, Linux, Unix-like.\newline Supported IPython environments: Jupyter Notebooks, JupyterLab, Google Colaboratory, Binder, IDE-based notebook editors, IPython shell. \\ \hline C8 & Link to developer documentation/manual & \url{https://github.com/ContextLab/davos\#readme} \\ \hline @@ -221,14 +224,14 @@ \section{Motivation and significance} \section{Software description} The Davos package is named after Davos Seaworth, a smuggler referred -to as ``the Onion Knight" from the series \textit{A Song of Ice and Fire} by +to as ``the Onion Knight'' from the series \textit{A Song of Ice and Fire} by George R. R. Martin~\cite{Mart98}. The \texttt{smuggle} keyword provided by Davos is a play on Python's \texttt{import} keyword: whereas importing can load a package into the Python workspace within the existing rules and frameworks provided by the Python language, ``smuggling'' provides an alternative that expands the scope and reach of ``importing.'' Like the character Davos Seaworth (who became famous for smuggling onions through a -blockade on his homeland), we use ``onion'' comments to precisely control how +blockade on his homeland), the Davos package uses ``onion comments'' to precisely control how packages are smuggled into the Python workspace. \begin{figure}[tp] @@ -237,8 +240,8 @@ \section{Software description} \caption{\small \textbf{Package structure.} The Davos package comprises two interdependent subpackages. The \texttt{davos.core} subpackage includes modules for parsing \texttt{smuggle} statements - and onion comments, installing and validating packages, isolating and managing and - configuring Davos's behavior. The + and onion comments, installing and validating packages, isolating and managing + installed packages, and configuring Davos's behavior. The \texttt{davos.implementations} subpackage includes environment-specific modifications and features that are needed to support the core functionality across different notebook-based @@ -250,7 +253,7 @@ \section{Software description} \end{figure} -\subsection{Software architecture} +\subsection{Software architecture}\label{sec:architecture} The Davos package consists of two interdependent subpackages (see Fig.~\ref{fig:package-structure}). The first, @@ -336,7 +339,7 @@ \subsubsection{The onion comment}\label{subsec:onion} \end{center} Occasionally, a package's distribution name (i.e., the name used when installing it) may differ from its top-level module name (i.e., the name -used when importing it). In such cases, an onion comment may be used to ensure +used when importing it). In such cases, an onion comment can be used to ensure that Davos installs the proper package if it cannot be found locally: \begin{center} \includegraphics[width=0.9\textwidth]{figs/snippet2} @@ -346,7 +349,7 @@ \subsubsection{The onion comment}\label{subsec:onion} how, where, and when smuggled packages are installed. Critically, if an onion comment includes a version specifier~\cite{CoghStuf13}, Davos will ensure that the version of the package loaded into the notebook matches the specific -version requested, or satisfies the given version constraints. If the smuggled +version requested (or satisfies the given version constraints). If the smuggled package exists locally, Davos will extract its version information from its metadata and compare it to the specifier provided. If the two are incompatible (or no local installation is found), Davos will download, install, and load a @@ -361,22 +364,22 @@ \subsubsection{The onion comment}\label{subsec:onion} \end{center} Davos processes onion comments internally before forwarding arguments to the installer program. In addition to preventing shared notebooks from executing -arbitrary code in a user's shell, this enables Davos to adapt its behavior +arbitrary code in a user's shell, this enables Davos to adjust its behavior based on how particular flags will affect the behavior of the installer program. For example, including \texttt{pip}'s \texttt{--no-input} flag will also temporarily enable Davos's non-interactive mode (see Sec.~\ref{subsec:config}). -Similarly, if an onion comment contains either the \texttt{-I}/\texttt{--ignore-installed}, -\texttt{-U}/\texttt{--upgrade}, or \texttt{--force-reinstall} flag, Davos will -skip checking for a local copy of the smuggled package before installing a -new one: +Similarly, if an onion comment contains either \texttt{-I}/\texttt{--ignore-installed}, +\texttt{-U}/\texttt{--upgrade}, or \texttt{--force-reinstall}, Davos will +install and load a new copy of the smuggled package without first checking +for it locally: \begin{center} \includegraphics[width=0.9\textwidth]{figs/snippet5} \end{center} Since the purpose of an onion comment is to describe how a smuggled package should be installed (if necessary) so that it can be loaded and used -immediately, options that would cause the package not to be installed (such as -\texttt{-h}/\texttt{--help} or \texttt{--dry-run}) are disallowed. Additionally, -when using a Davos project to isolate smuggled packages (the default behavior; +immediately, options that would normally cause the package not to be installed +(such as \texttt{-h}/\texttt{--help} or \texttt{--dry-run}) are disallowed. Additionally, +when using a Davos ``project'' to isolate smuggled packages (the default behavior; see Sec.~\ref{subsec:projects}), onion comments may not contain options that would change the package's installation location (such as \texttt{-t}/\texttt{--target}, \texttt{--root}, or \texttt{--prefix}). However, if @@ -387,132 +390,78 @@ \subsubsection{The onion comment}\label{subsec:onion} \subsubsection{Projects}\label{subsec:projects} -Standard approaches to installing packages from within a notebook can alter the local Python environment in potentially unexpected and undesired ways. For example, running a notebook that installs its dependencies via system shell commands (prefixed with ``\texttt{!}'') or IPython magic commands (prefixed with ``\texttt{\%}'') may cause other existing packages in the user's environment to be uninstalled and replaced with alternate versions. This can lead to incompatibilities between installed packages, affect the behavior of the user's other scripts or notebooks, or even interfere with system applications. - -To prevent Davos-enhanced notebooks from having unwanted side-effects on the user's environment, Davos automatically isolates packages installed via \texttt{smuggle} statements using a custom scheme called ``projects.'' Functionally, a Davos project is similar to a standard Python virtual environment (e.g., created with the standard library's \texttt{venv} module or a third-party tool like \texttt{virtualenv}~\cite{BickEtal07}): it consists of a directory (within a hidden \texttt{.davos} folder in the user's home directory) that houses third-party packages needed for a particular project or task. However, unlike standard virtual environments, Davos projects do not need to be manually activated and deactivated, do not contain separate Python or \texttt{pip} executables, and \textit{extend} the user's main Python environment rather than replace it. - -When Davos is imported into a notebook, a notebook-specific project directory is automatically created (if it does not exist already). -%When Davos is imported into a notebook, a notebook-specific project directory is automatically created (if it does not exist already), named for the absolute path to the notebook file. - - -Notebook-specific projects are named for the absolute path to the notebook file. - - - %Davos projects function similarly to simplified versions of standard Python virtual environments (e.g., created with the standard library's \texttt{venv} module or a third-party tool like \texttt{virtualenv}~\cite{BickEtal07}) with a few differences: they do not need to be manually activated and deactivated, they do not contain separate Python or \texttt{pip} executables, and they \textit{extend} the main Python environment rather than replace it. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% ADD THIS EITHER TO START OF PROJECTS SUBSUBSECTION OR IN IMPACT SECTION % -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%A common way of avoiding this is to create a virtual environment in which to run the notebook, and instruct anyone with whom the notebook is shared to do the same. While effective, this added requirement introduces additional -% -% -%(or group of related notebooks) that require different -% -% -%\begin{itemize} -% \item{common solution is to use a virtual environment} -% \item{this introduces complexity} -% \item{davos's solution is projects} -%\end{itemize} - -\bigskip\bigskip\textcolor{red}{\textbf{========== TODO: finish editing from here to end ==========}}\bigskip\bigskip - -%Because Davos can install new packages, running the code in a -%Davos-enhanced notebook might (in principle) affect the behavior of -%\textit{other} Python-based software (e.g., other notebooks, scripts, etc.) by -%altering which packages are installed in the runtime environment. This could -%lead to undesired consequences. For example, suppose Person A develops a -%notebook (Notebook A) for their research project. We will assume that Notebook -%A does not use Davos to manage project dependencies. If Person A -%runs a Davos-enhanced Notebook B, e.g., sent by another developer, -%might this unexpectedly affect the behavior of Notebook A? - -%To prevent unwanted -%changes to the user's Python environment, Davos incorporates its -%own virtual environment-like scheme for isolating packages it installs. When Davos -%is imported, a new virtual environment (folder) is created automatically. The -%folder's name may be customized to support multi-notebook projects. Any -%\texttt{smuggle}d packages that were not available in the notebook's runtime -%environment are installed to the current project folder. The runtime -%environment remains unaffected by Davos's behavior (see -%Sec.~\ref{subsec:projects}). -%By default, projects are notebook-specific, but can also be shared by multiple notebooks, and can be managed interactively from within a Davos-enhanced notebook. - -%%%%%%%% JEREMY VERSION %%%%%%%%% -%Installing new packages in a notebook using standard approaches (e.g., system commands) affect the -%runtime environment. This could lead to undesired behaviors. For example, running a notebook that -%installs new packages in the user’s primary system environment might alter their main system installation -%and/or containerized environment in unexpected ways (e.g., changing package versions, causing conflicts -%with other packages, etc.). To protect against undesired changes to the runtime environment, Davos -%incorporates its own virtual environment for managing packages it installs. When Davos is imported, -%a new virtual environment (folder) is created automatically. The folder’s name may be customized to -%support multi-notebook projects. Any smuggled packages that were not available in the notebook’s -%runtime environment are installed to the current project folder. The runtime environment remains unaffected -%by Davos’s behavior (see Sec. 2.2.3). - -We implemented a ``project'' system in Davos to protect against the -above scenario. By default, importing Davos creates a new project -folder in the user's home directory (contained within a hidden \texttt{.davos} -folder). The default project name is computed to uniquely identify each -notebook according to its filename and path. Any packages that were not -originally available in the notebook's runtime environment are installed to the -notebook's project directory. When external libraries are \texttt{smuggle}d, -Davos temporally appends the current project directory to the search -path. Because the user's system path remains unchanged, and because none of the -runtime environment's packages are altered, the user's system and runtime -environment remain unaffected (aside from installing the Davos package -itself to the runtime environment). - -Each notebook's project may be customized by setting \texttt{davos.project} to -any string that can be used as a valid folder name in the user's operating -system. By customizing the project name, users can build multi-notebook -projects that share the same core set of dependencies without needing to -duplicate each package for each notebook in the project. - -Finally, if the user \textit{does} wish to modify their runtime environment, -this may be done by setting \texttt{davos.project} to \texttt{None}. Doing so -will cause any packages installed by Davos to affect the user's -runtime environment. This is generally not recommended, as it can lead to -unintended consequences for other code that shares the runtime environment. +Standard approaches to installing packages from within a notebook can alter the local Python environment in potentially unexpected and undesired ways. +For example, running a notebook that installs its dependencies via system shell commands (prefixed with ``\texttt{!}'') or IPython magic commands (prefixed with ``\texttt{\%}'') may cause other existing packages in the user's environment to be uninstalled and replaced with alternate versions. +This can lead to incompatibilities between installed packages, affect the behavior of the user's other scripts or notebooks, or even interfere with system applications. + +To prevent Davos-enhanced notebooks from having unwanted side effects on the user's environment, any packages installed via \texttt{smuggle} statements are automatically isolated using a custom, virtual environment-like system called ``projects.'' +Davos projects are similar to standard Python virtual environments (e.g., created with the standard library's \texttt{venv} module or a third-party tool like \texttt{virtualenv}~\cite{BickEtal07}) but with a few noteworthy differences that make them generally lighter-weight and simpler to use. +Like a standard virtual environment, a Davos project consists of a directory (within a hidden \texttt{.davos} folder in the user's home directory) that houses third-party packages needed for a particular Python project, workflow, or task. +However, unlike standard virtual environments, Davos projects do not need to be manually created, activated, or deactivated, and function to \textit{extend} the user's existing Python environment rather than replace it. + +When Davos is imported into a notebook, a project directory for that notebook is automatically created (if it does not exist already). +When \texttt{smuggle} statements within that notebook are then executed, any packages (or specific versions of packages) that are not already available in the user's Python environment are installed into the notebook's project directory (along with any missing dependencies of those packages). +During each \texttt{smuggle} statement's execution, Davos also temporarily prepends the notebook's project directory to the module search path so that these project-installed packages are visible when searching for smuggled packages locally, and prioritized over those in the user's main environment. + +Thus, rather than constructing fully separate Python environments from scratch, Davos projects work by supplementing the user's existing environment with any additional packages (or specific package versions) needed to satisfy the dependencies of their corresponding notebooks. +In some cases, this might include every package smuggled into a notebook (e.g., if the notebook is run inside a freshly created, empty virtual environment). +In other cases, the user's environment may already provide all required packages, and the notebook's project directory will go unused (in which case it will be deleted automatically when the notebook kernel is shut down). +But regardless of the extent to which the existing environment is augmented, Davos's project system ensures that all smuggled packages are installed locally and loaded successfully at runtime, while the contents of the user's Python environment are never altered. + +Additionally, because \texttt{smuggle} statements in a given notebook are evaluated every time it is run, this system also ensures that the notebook's requirements will remain satisfied even if the user's Python environment changes. +For example, suppose a user has \texttt{NumPy}~\cite{HarrEtal20} v1.24.3 installed in their current Python environment and runs a Davos-enhanced notebook that smuggles \texttt{NumPy} with ``\texttt{numpy==1.24.3}'' specified in an onion comment (see Sec.~\ref{subsec:onion}). +Since the user's existing version of the package satisfies this requirement, Davos will happily load it into the notebook. +But if the user later upgrades their environment's \texttt{NumPy} version to v1.25.0 (perhaps as a result of installing a different package that depends on it) and subsequently re-runs this notebook, the local version will longer satisfy this requirement, so Davos will install \texttt{NumPy} v1.24.3 into the notebook's project directory and load that version instead. +From then on, any further changes to the user's \texttt{Numpy} installation would have no effect on Davos's behavior in this particular notebook, as a satisfactory version now exists in its project directory. +(If the version specified in the onion comment were changed, Davos would update the version installed in the project directory accordingly.) +For efficiency, Davos projects will generally not duplicate dependencies already satisfied by the user's Python environment. +However, if desired, adding \texttt{pip}'s \texttt{--ignore-installed} flag to an onion comment in the notebook will cause Davos to install the smuggled package into the project directory whether or not it already exists locally. + +By default, each Davos-enhanced notebook will create and use its own notebook-specific project named for the absolute path to the notebook file. +However, before smuggling its required packages, a notebook may be set to instead use an arbitrarily named, notebook-agnostic project by assigning any (non-empty) string to \texttt{davos.project} (see Sec.~\ref{subsec:config}). +This provides a convenient way for multiple related notebooks that share a common set of requirements to use the same Davos project, by setting \texttt{davos.project} to the same string in each one. +It is also possible (though typically not recommended) to disable Davos's project system entirely and install smuggled packages directly into the user's Python environment by setting \texttt{davos.project} to \texttt{None}. + +When accessed (unless its value has been set to \texttt{None}), \texttt{davos.project} will return a \texttt{Project} object that represents the project used by the current notebook (strings assigned to \texttt{davos.project} are converted to \texttt{Project}s internally). This object supports methods for interacting with the current project, including locating its directory on the file system, listing all installed packages' names and versions, changing the project's name, and deleting its contents altogether. +\texttt{Project} instances can also be created and managed programmatically, and Davos provides additional utilities for viewing and working with all existing projects (see Secs.~\ref{subsec:config} and \ref{subsec:toplevel}). \subsubsection{Configuring and querying Davos}\label{subsec:config} -Davos's behavior may be customized by modifying a set of attributes attached to -the \texttt{davos} module object that is added to the workspace when Davos is -imported. These attributes may be modified, displayed, or checked -programmatically at runtime (see Sec.~\ref{sec:illustrative-example} for an -illustrative example or Sec.~\ref{subsec:implementation} for implementation -details and additional information). These include: +After importing Davos into a notebook, the top-level \texttt{davos} module exposes a set of attributes whose values determine various aspects of Davos's behavior. +The majority of these are writeable options that can be modified to customize how, where, and when Davos installs smuggled packages (see Sec.~\ref{sec:illustrative-example} for an illustrative example). +These include: \begin{itemize} \item \texttt{.active}: This attribute controls whether support for \texttt{smuggle} statements and onion comments is enabled (\texttt{True}) or disabled (\texttt{False}). When Davos is first imported, - the \texttt{.active} attribute is set to \texttt{True}. + \texttt{davos.active} is set to \texttt{True} (see Sec.~\ref{subsec:implementation} for implementation details and additional information). \item \texttt{.auto\_rerun}: This attribute controls how Davos behaves when attempting to \texttt{smuggle} a new - version of a package that was previously imported and cannot be + version of a package that was previously loaded (via an \texttt{import} or \texttt{smuggle} statement) and cannot be reloaded. This can happen if the package includes extension modules that dynamically link C or C++ objects to the Python interpreter, and the code that generates those objects was changed between the - previously imported and to-be-smuggled versions. If this attribute + previously loaded and to-be-smuggled versions. If this attribute is set to \texttt{True}, Davos will automatically restart - the notebook kernel and rerun all code up to (and including) the + the notebook kernel and re-run all code up to (and including) the current \texttt{smuggle} statement. If set to \texttt{False} (the default), Davos will instead issue a warning, pause execution, and - prompt the user to either restart and rerun the notebook, or - continue running with the previously imported package version until + prompt the user to either restart and re-run the notebook, or + continue running with the previously loaded package version until the next time the kernel is restarted manually. Note that, as of - this writing, the \texttt{.auto\_rerun} attribute is not supported - in Google Colaboratory notebooks. + this writing, setting \texttt{davos.auto\_rerun} to \texttt{True} is not + supported in Google Colaboratory notebooks. \item \texttt{.confirm\_install}: If set to \texttt{True} (default: \texttt{False}), Davos will require user confirmation - before installing a smuggled package that does not yet exist in the - user's environment. + before installing a smuggled package that is not already + available locally. This is primarily useful if the user has disabled + Davos's ``project'' system for isolating smuggled packages (see + Sec.~\ref{subsec:projects}) but still wants to carefully control what + packages are installed into their main Python environment. \item \texttt{.noninteractive}: Setting this attribute to \texttt{True} (default: \texttt{False}) enables non-in\-ter\-act\-ive @@ -525,47 +474,90 @@ \subsubsection{Configuring and querying Davos}\label{subsec:config} \item \texttt{.pip\_executable}: This attribute's value specifies the path to the \texttt{pip} executable used to install smuggled - packages. The default is programmatically determined from the Python - environment and falls back to \texttt{sys.executable -m pip} if no + packages. The default is programmatically determined from the user's Python + environment and falls back to \texttt{ -m pip} if no executable can be found. +\item \texttt{.project}: This attribute's value is a \texttt{Project} instance representing the Davos project associated with the current notebook. + As described in Section~\ref{subsec:projects}, Davos projects serve to isolate packages installed by \texttt{smuggle} statements from the user's main Python environment, and the \texttt{Project} class provides an interface for inspecting and managing projects at runtime. + This attribute's default value is a notebook-specific project named for the absolute path to the notebook file. + To change the project used in the current notebook (e.g., in order to use the same project in multiple related notebooks), this attribute may be assigned a different \texttt{Project} instance or, for simplicity, the name of the desired project as a string or \texttt{pathlib.Path} (either of which will be converted to a \texttt{Project} on assignment). + Alternatively, setting \texttt{davos.project} to \texttt{None} will disable project-based isolation for the current notebook and cause Davos to install any missing packages directly into the main Python environment. + This attribute can be reset to its default value using the top-level \texttt{use\_default\_project()} function (see Sec.~\ref{subsec:toplevel}). + For more information about Davos projects, see Section \ref{subsec:projects}. + \item \texttt{.suppress\_stdout}: If this attribute is set to \texttt{True} (default: \texttt{False}), Davos suppresses printed (console) outputs from both itself and the installer program. - This can be useful when smuggling packages that need to install many - dependencies and/or generate extensive output. However, if the installer - program throws an error, both its stdout and stderr streams will be + This can be useful when smuggling packages that require installing many + dependencies and/or generate extensive output when built from source + distributions. Note that if this option is enabled and the installer + program throws an error, both its stdout and stderr streams will still be displayed alongside the Python traceback to allow for debugging. -\item \texttt{.project}: \textcolor{red}{\textbf{TODO: fix this}} This attribute is a string that specifies the name of -the ``project'' associated with the current notebook. As described in -Section~\ref{subsec:projects}, a notebook's project determines where and how -any \texttt{smuggle}d dependencies are installed if they are not available in -the current runtime environment. By default, this attribute is named according -to the current notebook's absolute file path. However, the project name may be -customized to enable shared dependency installations across notebooks (see -Sec.~\ref{subsec:projects}). - \end{itemize} -\noindent Davos namespace also defines the \texttt{davos.configure()} function, -which allows setting multiple configuration options simultaneously. In addition -to the above configurable attributes, the \texttt{davos} object also includes -several read-only attributes that contain potentially useful information about -the current environment or Davos's behavior: +\noindent The attributes above can be modified directly or via the \texttt{davos.configure()} function, which allows setting multiple options simultaneously (see Sec.~\ref{subsec:toplevel} for more information or Sec.~\ref{sec:illustrative-example} for example usage). +In addition to these writeable options, the top-level \texttt{davos} module also provides several read-only attributes that can be displayed in the notebook or checked programmatically at runtime, and contain potentially useful information about the notebook environment or Davos's internal state: \begin{itemize} -\item \texttt{.environment}: This attribute's value is a string describing the notebook -environment Davos was imported into. As of the current version (0.2.0), this -attribute will be set to either \texttt{“IPython<7.0"}, \texttt{“IPython>=7.0”}, or \texttt{“Colaboratory”}. +\item \texttt{.all\_projects}: This attribute contains a list of all Davos projects that exist on the user's local system (see Sec.~\ref{subsec:projects} for more information about Davos projects). + Each item in this list is either a \texttt{Project} or \texttt{AbstractProject} instance. + \texttt{AbstractProject}s represent notebook-specific projects whose associated notebooks no longer exist. + They support all the same functionality as \texttt{Project} objects (including methods for inspecting, renaming, and deleting them) and serve primarily to help users identify and clean up extraneous projects left behind after deleting Davos-enhanced notebooks (e.g., see Sec.~\ref{subsec:toplevel}). + +\item \texttt{.environment}: This attribute's value is a string denoting the set of environment-dependent ``helper functions'' used by Davos in the current notebook. + As described in Section \ref{sec:architecture}, Davos internally chooses between interchangeable implementations of certain core features based on various properties of the notebook's frontend and IPython kernel. + As of this writing, three unique combinations of helper functions are required to support existing notebook environments, ergo this attribute has three possible values: \texttt{"IPython<7.0"}, \texttt{"IPython>=7.0"}, or \texttt{"Colaboratory"}. + However, this attribute could take on additional values in the future, as new notebook interfaces are created and IPython's internals are updated, and additional versions of helper functions are added to Davos to support them. + +\item \texttt{.ipython\_shell}: This attribute contains the global IPython \texttt{InteractiveShell} instance underlying the notebook kernel session. - \item \texttt{.ipython\_shell}: This attribute contains the global IPython \texttt{InteractiveShell} instance underlying the notebook kernel session. +\item \texttt{.smuggled}: This attribute's value is a Python dictionary that functions as a cache of \texttt{smuggle} statements executed during the current notebook kernel session. + The dictionary's keys are names of smuggled packages, and its values are arguments passed to the installer program via onion comments. + Entries appear in order of the \texttt{smuggle} statements' execution. + +\end{itemize} + +\noindent The current values of all \texttt{davos} attributes may be viewed at once within a notebook by displaying the \texttt{davos.config} object. - \item \texttt{.smuggled}: This attribute is set to a Python dictionary that functions as a cache of any \texttt{smuggle} commands run during the current -session. The dictionary's keys are package names and the values are arguments passed via the corresponding smuggle statement's onion comment. - \item \texttt{.all\_projects}: This attribute contains a list of all local projects (i.e., projects with virtual environment directories located in \texttt{\$HOME/.davos/projects}). See Section~\ref{subsec:projects} for additional information about Davos projects. +\subsubsection{Other top-level Davos functions}\label{subsec:toplevel} + +The Davos package also provides a handful of functions available in the top-level \texttt{davos} namespace. +Some of these functions serve primarily as conveniences, while others provide additional functionality: + +\begin{itemize} + +\item \texttt{configure(**kwargs)}: This function provides an alternate way of assigning values to the writeable attributes listed in Section \ref{subsec:config} and can be used to configure multiple options at once (see Sec.~\ref{sec:illustrative-example} for example usage). + The function accepts attribute names as keyword-only arguments to which their desired values are passed. + If any of the options passed are incompatible (e.g., both \texttt{confirm\_install=True} and \texttt{noninteractive=True} are passed) or assignment to any of the specified attributes fails for any reason, none of the given options will be modified. + +\item \texttt{get\_project(project\_name, create=False)}: This function can be passed the name of a Davos project (\texttt{project\_name}) to get the \texttt{Project} or \texttt{AbstractProject} instance representing it. + The optional \texttt{create} argument determines the function's behavior when no project with the given name exists: if \texttt{create=False} (the default), the function will return \texttt{None}; if \texttt{create=True}, a project with the given name will be created and returned. + +\item \texttt{prune\_projects(yes=False)}: This function allows users to quickly ``clean up'' their local Davos projects by deleting notebook-specific projects whose corresponding notebooks no longer exist (i.e., \texttt{AbstractProject}s). + As with standard virtual environments, periodically removing unused project directories can be useful for reclaiming disk space from dependencies of code that is no longer in use. + By default, this function will interactively display a list of all unused projects and allow the user to choose whether or not to delete each one. + Alternatively, passing \texttt{yes=True} will immediately remove all unused projects without prompting for confirmation. + Note that if Davos's non-interactive mode is enabled (see Sec.~\ref{subsec:config}), \texttt{yes=True} must be explicitly passed, otherwise the function will raise an exception. + This serves as a safeguard against accidentally deleting projects since non-interactive mode disables all user input and confirmation. + Also note that this function will not delete notebook-agnostic projects (i.e., manually created projects whose names are not notebook filepaths), as they are not linked to specific notebooks whose existence determines whether or not they are still needed. + These (and any) projects may be deleted individually by calling their \texttt{Project} objects' \texttt{.remove()} method. + +\item \texttt{require\_python(version\_spec, warn=False, extra\_msg=None, prereleases=None)}: Through \texttt{smuggle} statements and onion comments, Davos can automatically ensure that all Python packages needed to run a notebook are installed, and that the same versions of those packages are used no matter when or by whom the notebook is run. + However, because Davos operates at runtime, one thing it cannot do automatically is install and switch to a specific version of Python itself. + Distributing shared code along with a precise Python version for running it requires a heavier-weight solution, such as a Conda environment or Docker container (see Fig.~\ref{fig:code-sharing}). + Yet a Davos-enhanced notebook may still \texttt{smuggle} certain packages that depend on users having a particular Python version or range of versions (e.g., even just within the standard library, the \texttt{dataclass} module was first added in Python 3.7 \cite{Smit17} and at least 19 modules are slated for removal in Python 3.13 \cite{HeimCann19}). + The \texttt{davos.require\_python()} function can be added to the top of a Davos-enhanced notebook to communicate to users that the notebook's code should be run with a specific or constrained Python version (see Sec.~\ref{sec:illustrative-example} for example usage). + The function may be passed a version identifier (e.g., \texttt{"3.10.5"}) or any valid version specifier \cite{CoghStuf13} (e.g., \texttt{"\raisebox{0.5ex}{\texttildelow}=3.11"}, \texttt{">=3.9;<3.12"}, etc.) and will raise an exception if the user's Python version is incompatible. + Alternatively, a ``soft'' or suggested constraint can be imposed by passing \texttt{warn=True} to issue a warning rather than raise an error. + Additional information can be added to the default error/warning message (e.g., the specific reason for this requirement) via the \texttt{extra\_msg} argument, and the optional \texttt{prereleases} argument can be used to explicitly allow (\texttt{True}) or disallow (\texttt{False}) pre-release versions (by default, the policy is determined by the value of \texttt{version\_spec}). + +\item \texttt{use\_default\_project()}: By default, each Davos-enhanced notebook will create and use a notebook-specific project named based on its absolute path. + If a user manually changes the project used by the current notebook (i.e., by setting the value of the \texttt{davos.project} attribute; see Sec.~\ref{subsec:config}), this function can be called to switch back to using the notebook's default project and reset \texttt{davos.project} to its default value. + See Section \ref{subsec:projects} for more information about Davos projects and Section \ref{sec:illustrative-example} for an illustrative example. \end{itemize} @@ -584,8 +576,8 @@ \subsection{Implementation details}\label{subsec:implementation} IPython preprocesses all executed code as plain text before it is sent to the Python compiler, in order to handle special constructs like -\texttt{\%magic} and \texttt{!shell} commands. Davos uses -this process to transform \texttt{smuggle} statements into +\texttt{!}-prefixed shell commands and \texttt{\%}-prefixed ``magic'' commands. Davos uses +this same process to invisibly transform \texttt{smuggle} statements into syntactically valid Python code. The Davos parser uses a regular expression to match lines of code containing \texttt{smuggle} statements (and, optionally, onion comments), extract relevant @@ -611,23 +603,21 @@ \subsection{Implementation details}\label{subsec:implementation} overwritten and no longer refers to the \texttt{smuggle()} function. It will also deregister the Davos parser from the set of input transformers run when each notebook cell is -executed. While the overhead added by the Davos parser is -minimal, this may be useful, for example, when optimizing or precisely -profiling code. +executed. \begin{figure}[tp] \centering \includegraphics[width=\textwidth]{figs/flow_chart} \caption{\small \textbf{\texttt{smuggle()} function algorithm.} At a high level, the \texttt{smuggle()} function may be conceptualized as -following two basic steps. First (left), Davos ensures that the -correct version of the desired package has been installed, carrying -out the installation automatically if needed. Second (right), -Davos imports the package and updates the current runtime environment.} + following two basic steps. First (left), Davos ensures that the + correct version of the desired package is available locally, installing + it automatically (into the notebook's project directory) if needed. Second (right), + Davos loads the package into the notebook and updates the current + runtime environment.} \label{fig:flow-chart} \end{figure} - \section{Illustrative Example}\label{sec:illustrative-example} \begin{figure}[tp] @@ -639,164 +629,202 @@ \section{Illustrative Example}\label{sec:illustrative-example} \label{fig:illustrative-example} \end{figure} -Across different versions of a given package, particular modules, functions, +%The example code throughout Section \ref{subsec:onion} illustrates how Davos is most typically used: +%By including a series of \texttt{smuggle} statements and onion comments with version specifiers or other options in an IPython notebook, researchers can share their code and its dependencies in a single file that can be easily run without any additional tools or setup, creates and manages its own isolated environment, automatically installs its required packages at runtime, and ensures that the package versions with which it is run do not change unexpectedly. +The example code throughout Section \ref{subsec:onion} illustrates how Davos is most typically used: a series of smuggle statements and onion comments with version specifiers or other options collectively describes and automatically constructs a reproducible environment for running the code that follows it. +When added to the top of a Jupyter notebook, this allows researchers to bundle their code and its dependencies into a single file that can be easily shared and run without any additional tools or setup, automatically installs its required packages at runtime, isolates them from the user's main Python environment, and ensures their versions do not change unexpectedly over time. +In this section, we have contrived a more complex scenario to highlight some of Davos's more advanced features, and illustrate how they may be used to handle certain challenges that can arise when writing, running, and sharing reproducible scientific code. + +Across different versions of a given package, various modules, functions, and other objects may be updated, removed, renamed, or otherwise altered. In addition to changing the behaviors of active computations, these changes can render saved objects created using one version of a package incompatible with other versions of the same package. For example, the popular -\texttt{pandas}~\cite{McKi10} library used to include the \texttt{Panel} data -structure for storing 3-dimensional arrays. Since version 0.20.0, however, the -\texttt{Panel} class has been deprecated, and in version 0.25.0, it was removed +\texttt{pandas}~\cite{McKi10} library originally included the \texttt{Panel} data +structure for storing 3-dimensional arrays. In version 0.20.0, however, the +\texttt{Panel} class was deprecated, and in version 0.25.0, it was removed entirely. Suppose a user had a dataset stored in a \texttt{Panel} object (created using an older version of \texttt{pandas}) and had saved it to their disk (e.g., for later reuse or to share with other users) by serializing the \texttt{Panel} with Python's \texttt{pickle} protocol. The \texttt{pickle} -protocol is a popular built-in method of persisting data in Python, allowing +protocol is a popular built-in method of persisting data in Python that allows users to save, share, and load arbitrary objects. However, in order to -successfully ``unpickle'' (i.e., load and restore) a ``pickled'' (i.e., saved) -object, the object's class must be defined in and importable from the same -module as when it was saved. Thus, because of the \texttt{Panel} class's +successfully ``unpickle'' (i.e., load and restore) a ``pickled'' (i.e., previously saved) +object, that object's class must be defined in and importable from the same +module as it was when the object was originally saved. Thus, because of the \texttt{Panel} class's removal, the user's dataset could not be read by any version of \texttt{pandas} -from 0.25.0 or beyond. These incompatibilities are also not limited solely to +from 0.25.0 onward. These incompatibilities are also not limited solely to traditional forms of data. For example, saved model states and other objects may reference modules, functions, attributes, classes, or other objects that may not be identical (or even present) across all versions of their associated -package. +packages. The example provided in Figure~\ref{fig:illustrative-example} demonstrates how -the Davos package can be used to circumvent these incompatibilities by -carefully controlling which versions of each package are used in different -parts of the notebook. The example shows how a dataset and model that require +Davos can be used to circumvent these incompatibilities by +temporarily switching between different versions of the same package within a single runtime. +%carefully controlling which versions of each package are used in different parts of the notebook. +The example shows how a dataset and model that require now-incompatible components of the \texttt{pandas} and -\texttt{scikit-learn}~\cite{PedrEtal11} packages may be loaded in (using older +\texttt{scikit-learn}~\cite{PedrEtal11} libraries can be loaded in (using older versions of each package) and used alongside more recent versions of each package that provide new and improved functionality. When included at the top of a Jupyter notebook, the code in Figure~\ref{fig:illustrative-example} ensures that these objects will be loaded successfully and analyzed using the -same set of package versions, no matter when or by whom the notebook is run. +same set of package versions no matter when or by whom the notebook is run. -After installing and importing Davos (lines 1--2), we first \texttt{smuggle} two +After installing and importing Davos (lines 1--2), we first use the \texttt{davos.require\_python()} function to constrain the Python version used to run the notebook (see Sec.~\ref{subsec:toplevel}). +As described above, the example code in Figure \ref{fig:illustrative-example} loads two different versions of the \texttt{pandas} library: first, an older version needed to access a dataset saved in an outmoded format, then a newer one to use throughout the remainder of the notebook. +We therefore want to make sure upfront (in line 6) that the notebook's Python version falls within the range of versions that both of these two versions of \texttt{pandas} support. +%Line 6 therefore ensures upfront that the notebook's Python version falls within the overlap between the ranges of Python versions that these two versions of \texttt{pandas} support. +If it does not, the function in line 6 will raise an error that includes a message to this effect (lines 4--5). +\begin{center} +\includegraphics[width=0.9\textwidth]{figs/example1} +\end{center} + +Next, in lines 8--9, we \texttt{smuggle} two utilities for interacting with local files in the code below. The -\texttt{smuggle} statement in line 4 loads the \texttt{is\_file()} +\texttt{smuggle} statement in line 8 loads the \texttt{is\_file()} function from the Python standard library's \texttt{os.path} module. Standard library modules are included with all Python distributions, so this line is functionally equivalent to an \texttt{import} statement and does not need or benefit from an onion -comment. Line 5 loads the \texttt{joblib} package~\cite{Varo10}, -installing it first, if necessary. Since \texttt{joblib}'s I/O +comment (since there is no chance the module will need to be installed). +Line 9 then loads the \texttt{joblib} package~\cite{Varo10}, +installing it into the notebook's project directory if necessary. Since \texttt{joblib}'s I/O interface has historically remained stable and backwards-compatible -across releases, requiring that users have a particular exact version -installed would likely be unnecessarily restrictive. However, a -\textit{future} release might introduce some breaking change. The -onion comment in line 5 helps ensure the analysis notebook continues +across releases, requiring a particular exact version +would likely be unnecessarily restrictive. However, it is possible a +\textit{future} release could introduce some breaking change. The +onion comment in line 9 helps ensure that the analysis notebook will continue to run properly in the future by limiting allowable versions to those already released when the code was written: \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example1} +\includegraphics[width=0.9\textwidth]{figs/example2} \end{center} -Line 7 then uses the \texttt{davos.config} object to enable -Davos's \texttt{auto\_rerun} option before smuggling the next -two packages: \texttt{NumPy}~\cite{HarrEtal20} and +It is worth noting, however, that beyond illustrative purposes, the benefit of specifying only a maximum version for \texttt{joblib} rather than an exact version is relatively minor. +The main advantage to relaxing a version constraint in an onion comment (when a package's behavior does not differ meaningfully between versions) is that doing so increases the likelihood that a satisfactory version will already be available in the user's Python environment, and therefore Davos will not need to install a new copy in the notebook's project directory. +For large packages, this can be a worthwhile consideration; however \texttt{joblib} is very lightweight---less than 0.5 MB pre-built, with no required dependencies. +Thus a more conservative approach that guarantees an exact version is used would also be reasonable in this case. + +Line 11 then enables +Davos's \texttt{auto\_rerun} option (see Sec.~\ref{subsec:config}) before smuggling the next +two packages: \texttt{NumPy} and \texttt{pandas}. Because these packages rely heavily on custom C data -types, loading the particular versions from the onion comments may -require restarting the notebook kernel if different versions had been previously -imported during the same interpreter session (see -Sec.~\ref{subsec:config}). +types, loading the particular versions specified in their onion comments may +require restarting the notebook kernel if different versions were previously +imported during the same interpreter session---including internally by other packages. +Enabling \texttt{auto\_rerun} allows Davos to handle kernel restarts automatically and continue running the code seamlessly without user intervention. \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example2} +\includegraphics[width=0.9\textwidth]{figs/example3} \end{center} -Setting the \texttt{auto\_rerun} attribute to \texttt{True} is particularly useful -for managing the installation of \texttt{pandas} in the next -lines: +In the case of \texttt{NumPy}, whether or not a kernel restart is necessary will depend on the user's existing Python environment. +The \texttt{joblib} package has an optional dependency on \texttt{NumPy} for memoizing and parallelizing array operations, and will \texttt{import numpy} internally to enable these features if the package is available. +If the user already has \texttt{NumPy} installed in their Python environment when \texttt{joblib} is smuggled in line 9, their installed version is different from the one specified in the onion comment on line 12, and there were changes made to \texttt{NumPy}'s C extensions between those two versions, then Davos will automatically restart the kernel and re-run the lines above. +The newly smuggled version would then be used both in the notebook itself and by \texttt{joblib} internally. +% (Note that outside the context of an illustrative example, one could avoid a kernel restart here altogether simply by smuggling \texttt{NumPy} before \texttt{joblib}.) + +The primary reason for enabling the \texttt{auto\_rerun} option, however, is to manage the installation of \texttt{pandas} in the next set of lines: \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example3} +\includegraphics[width=0.9\textwidth]{figs/example4} \end{center} -If we suppose that the data contained in \texttt{data-old.pkl} is +If we suppose that the ``\texttt{data-old.pkl}'' file contains a dataset stored in a pickled \texttt{Panel} object, then we must use a version of -\texttt{pandas} prior to 0.25.0 (i.e., the version in which the \texttt{Panel} -class was removed) to be able to load it in. Line 11 ensures -that an older version of \texttt{pandas} will be imported, enabling -the data to be read in (and, in line 13, written to a CSV -file, which is compatible with newer \texttt{pandas} versions). +\texttt{pandas} prior to v0.25.0 (i.e., the version in which the \texttt{Panel} +class was removed) to be able to read it. Line 15 ensures +that a sufficiently old version of \texttt{pandas} will be imported, enabling +the data to be successfully loaded in line 16 and (in line 17) written to a CSV +file, which can be read by any \texttt{pandas} version. Newer versions of \texttt{pandas} have brought substantial improvements -including better performance, bug fixes, and additional functionality. Although +including performance enhancements, bug fixes, and additional functionality. Although the original dataset had to be read in using an older version of the package, we can take advantage of these more recent updates by smuggling \texttt{pandas} -a second time on line 15 (whose onion comment specifies that version 1.3.5 -should be installed and loaded). Since a different version of \texttt{pandas} -had already been loaded by the Python interpreter (on line 11), the notebook -kernel must be restarted in order to replace the old version's custom C -extensions with those from the new version. The \texttt{auto\_rerun} flag set -on line 7 enables Davos to trigger this process automatically so that -the code can continue running without user intervention, and converting the -dataset to a CSV file in lines 10--13 ensures that the older version of -\texttt{pandas} does not need to be reinstalled. - -Next, line 17 uses the \texttt{davos.configure()} function to disable +a second time in line 19 (whose onion comment specifies that version 1.3.5 +should be installed and loaded). Since a different \texttt{pandas} version +has already been loaded by the Python interpreter (line 15) and there have been +substantial changes to the library (including its extension modules) +between that version and v1.3.5, the notebook +kernel must be restarted in order to fully unload the old version in favor of +the new one. +When Davos automatically does so and re-runs the code above, having now converted the dataset to a CSV file means the old version does not need to be reinstalled (line 14). + +Next, line 21 uses the \texttt{davos.configure()} function to disable the \texttt{auto\_rerun} option and simultaneously enable two other options: \texttt{suppress\_stdout} and \texttt{noninteractive}. With -these options enabled, lines 18--19 \texttt{smuggle} +these options enabled, lines 22--23 \texttt{smuggle} \texttt{TensorFlow}~\cite{AbadEtal15}, a powerful end-to-end platform for building and working with machine learning models, and \texttt{UMAP}~\cite{McInEtal18}, a package that implements a family -of related manifold learning techniques. The onion comment in line 19 +of related manifold learning techniques. The onion comment in line 23 also specifies that \texttt{UMAP} should be installed with the optional requirements needed for its ``plot'' and ``parametric\_umap'' features. Together, these two packages depend on 36 other unique -packages, most of which have dependencies of their own. And if many of +packages, most of which have dependencies of their own. If many of these are not already installed in the user's environment, lines -18--19 could take several minutes to run. Enabling the +22--23 could take several minutes to run. Enabling the \texttt{noninteractive} option ensures that the installation will continue automatically without user input during that time. Enabling \texttt{suppress\_stdout} also suppresses console outputs while installing these packages and their many dependencies to prevent other potentially important outputs from being buried. \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example4} +\includegraphics[width=0.9\textwidth]{figs/example5} \end{center} -After re-enabling these two options (line 20), we next \texttt{smuggle} +After re-enabling these two options (line 24), we next \texttt{smuggle} specific versions of three plotting packages: \texttt{Matplotlib}~\cite{Hunt07}, \texttt{seaborn}~\cite{Wask21}, and -\texttt{Quail}~\cite{HeusEtal17} (lines 22--24). Because the first two +\texttt{Quail}~\cite{HeusEtal17} (lines 26--28). Because the first two are requirements of \texttt{UMAP}'s optional ``plot'' feature, they -will have already been installed by line 19, though possibly as +will have already been installed (if necessary) by line 23, though possibly as different versions than those specified in the onion comments on lines -22 and 23. If the installed and specified versions are the same, these +26 and 28. If the installed and specified versions are the same, these \texttt{smuggle} statements will function like standard \texttt{import} -statements to load the packages into the notebook namespace. If they +statements to load the packages into the notebook's namespace. If they differ, Davos will download the requested versions in place -of the installed versions before doing so. +of the installed versions, ensuring that they are used both in the notebook itself and by \texttt{UMAP} internally. \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example5} +\includegraphics[width=0.9\textwidth]{figs/example6} \end{center} -Line 24 uses an onion comment to specify that \texttt{Quail} should be -installed directly from a specific GitHub commit (\texttt{6c847a4}). -This ability to load packages directly from GitHub repositories can -enable developers to more easily use forked or modified versions of other -packages in their notebooks, even if those versions have not been -officially released. - -In lines 26--29, we demonstrate another aspect of Davos's -functionality that supports more advanced installation scenarios. The -\texttt{ipywidgets}~\cite{FredEtal15} package provides an API for -creating various JavaScript widgets with Python code, and the \texttt{widgetsnbextension} package provides -the machinery needed by the notebook frontend to display them. +The onion comment in line 28 specifies that \texttt{Quail} should be +installed from a fork of its GitHub repository (\texttt{myfork}), in its state as of a specific commit (\texttt{6c847a4}). +This ability to load packages directly from remote (or local) Git repositories can +enable developers to more easily use forked or customized versions of other +packages in their code, even if those versions have not been +officially released. Targeting specific VCS references (e.g., commits, tags, etc.) can also provide even finer-grained control over smuggled package versions than is possible with traditional version specifiers. + +In lines 30--37, we demonstrate another aspect of Davos's functionality that supports more advanced installation scenarios. +%The \texttt{ipywidgets}~\cite{FredEtal15} package (also known as Jupyter Widgets) provides a Python API for creating various JavaScript widgets within a notebook, and the \texttt{widgetsnbextension} package provides the JavaScript machinery needed by the the notebook frontend to display them. +The \texttt{ipywidgets}~\cite{FredEtal15} package (also known as Jupyter Widgets) provides a Python API for creating interactive JavaScript widgets within a notebook. +It depends on the \texttt{widgetsnbextension} package, which provides the JavaScript machinery needed by the notebook frontend to display these widgets. +%A complication is that, while \texttt{ipywidgets} must be installed in a location that is accessible from the IPython kernel (i.e., the Python runtime of the notebook itself), \texttt{widgetsnbextension} must be made accessible to the Jupyter notebook server, which is a separate Python runtime. +%A complication is that, while \texttt{ipywidgets} must be installed in an environment accessible from the IPython kernel (i.e., importable into the notebook itself), \texttt{widgetsnbextension} must be installed in the environment that houses the Jupyter notebook server. +A complication is that \texttt{ipywidgets} must be installed in a location that is accessible from the IPython kernel (i.e., the Python runtime within the notebook itself), while \texttt{widgetsnbextension} must be installed in the environment that houses the Jupyter notebook server (a separate Python runtime that serves and manages the notebook frontend client). +In many basic setups, the IPython kernel and notebook server exist in the same environment. +However, a common ``advanced'' approach entails running the notebook server from a base environment, with additional environments each providing their own separate, interchangeable IPython kernels. + +Lines 30--37 account for both of these possibilities programmatically: \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example6} +\includegraphics[width=0.9\textwidth]{figs/example7} \end{center} -A complication is that \texttt{ipywidgets} must be installed in the -same environment as the IPython kernel, whereas -\texttt{widgetsnbextension} must be installed in the environment that -houses the Jupyter notebook server. In many basic setups, these two -environments are the same. However, a common ``advanced'' approach -entails running the notebook server from a base environment, with -additional environments each providing their own separate, -interchangeable IPython kernels. To accommodate this multi-environment -scenario, on lines 26 and 28, we use the \texttt{pip\_executable} option to control which environments each -package should be installed to. Once these two packages are installed -and imported, line 31 smuggles \texttt{tqdm}~\cite{daCoEtal22}, which -display progress bars to provide status updates for running code. In +First, in line 30, we set the \texttt{davos.project} attribute to \texttt{None} to temporarily +%disable Davos's project isolation system and +allow installing smuggled packages outside of the notebook's project directory. +As noted in Section \ref{subsec:projects}, this is typically discouraged, as doing so can risk interfering with the user's Python environment if existing package versions are overwritten. +In this particular case, however, a combination of factors make this relatively safe and inconsequential. +First, the package we need to install directly into the notebook server environment (\texttt{widgetsnbextension}) is smuggled without an accompanying onion comment (line 34), meaning that Davos will not replace any version the user may already have installed. +Second, the package has no dependencies of its own, so if Davos does install it, no other packages could potentially be installed or updated as a side effect. +Third, the package itself provides no functionality outside of rendering Jupyter widgets, so its presence would not alter any other code's expected behavior. + +Next, in lines 31--33, we change the \texttt{pip} executable Davos uses to install smuggled packages (see Sec.~\ref{subsec:config}), storing the default executable's path in a variable before doing so. +When Davos's project system is disabled, using a \texttt{pip} executable from a particular Python environment will cause smuggled packages to be installed into (and subsequently loaded from) that environment. +The default \texttt{pip\_executable} will install packages into the environment used to run the IPython kernel. +Here, the new value assigned to \texttt{davos.pip\_executable} in line 33 is the output of running ``\texttt{command -v pip}'' as a \texttt{!}-prefixed IPython system shell command in line 32 (``\texttt{command -v}'' outputs the path to an executable, similar to ``\texttt{which}'' but more portable). +Since IPython system shell command are always executed in the notebook server environment, this command's output will be the path to that environment's \texttt{pip} executable---which may or may not be different from the kernel environment's. + +After smuggling the \texttt{widgetsnbextension} package in line 34, we use the \texttt{davos.use\_default\_project()} function in line 35 to revert to installing package into the notebook's project directory, restore the default value of \texttt{davos.pip\_executable} in line 36, and \texttt{smuggle} the specified version of \texttt{ipywidgets} in line 37. +With these two packages now installed +and imported, line 39 smuggles \texttt{tqdm}~\cite{daCoEtal22}, which +displays progress bars to provide status updates for running code. In Jupyter notebooks, the \texttt{tqdm.notebook} module can be imported to enable more aesthetically pleasing progress bars that are displayed via \texttt{ipywidgets}, if that package is installed and @@ -804,6 +832,8 @@ \section{Illustrative Example}\label{sec:illustrative-example} important to \texttt{smuggle} \texttt{tqdm} after ensuring the \texttt{ipywidgets} package was available. +\stoppedhere + Next, we load in the reformatted dataset (line 33) and pre-trained model (line 35) that we wish to use in our analysis. In our hypothetical example, we can suppose that the model was provided as a @@ -813,11 +843,11 @@ \section{Illustrative Example}\label{sec:illustrative-example} word counts are passed to a topic model~\cite{BleiEtal03} using a pretrained \texttt{LatentDirichletAllocation} instance. \begin{center} -\includegraphics[width=0.9\textwidth]{figs/example7} +\includegraphics[width=0.9\textwidth]{figs/example8} \end{center} Let us suppose that the \texttt{Pipeline} object had been saved by its original creator using the \texttt{joblib} package, as -\texttt{scikit-learn}'s documentation recommends. Because +\texttt{scikit-learn}'s documentation recommends \cite{skle22}. Because \texttt{joblib} uses the \texttt{pickle} protocol internally, the ability to save and load pre-trained models is not guaranteed across different \texttt{scikit-learn} versions. For example, suppose that @@ -841,6 +871,8 @@ \section{Illustrative Example}\label{sec:illustrative-example} \textit{analyze} and manipulate the data and model output using the latest approaches and implementations. +\todo{mention notebook reproducibility, cell order, multiple package versions re: reviewer's comment; note importance of running lines 14--19 \& 38--40 in single cell} + \section{Impact} @@ -891,10 +923,12 @@ \section{Impact} research studies~\citep{MannEtal23a, OwenMann23, ZimaEtal23}, Davos is being used by both students and instructors in programming and methods courses such as Storytelling with Data~\cite{Mann21a} (an open course on data science, -visualization, and communication) and Laboratory in Psychological +visualization, and communication), Laboratory in Psychological Science~\cite{Mann22} (an open course on experimental and statistical methods -for psychology research) to simplify distributing lessons and submitting -assignments, as well as in online demos such as +for psychology research), and the Methods in Neuroscience at Dartmouth (MIND) +Computational Summer School~\cite{MIND23} (a week-long intensive course on +computational neuroscience methods) to simplify distributing lessons +and submitting assignments, as well as in online demos such as \texttt{abstract2paper}~\cite{Mann21b} (an example application of GPT-Neo~\cite{GaoEtal20, BlacEtal21}) to share ready-to-run code that installs dependencies automatically. The 2023 offering of Neuromatch @@ -974,6 +1008,8 @@ \subsection{Pitfalls and limitations} software would therefore need to use existing non-Davos approaches to managing those requirements. +\textcolor{red}{\textbf{TODO: add note about default/fallback project for non-traditional notebook interfaces}} + \section{Conclusions} The Davos package supports reproducible research by providing @@ -1014,7 +1050,11 @@ \section*{Acknowledgements} We acknowledge useful feedback and discussion from the students of JRM's \textit{Storytelling with Data} course (Winter, 2022 offering) -who used preliminary versions of our package in several assignments. +who used preliminary versions of our package in several assignments, +and the students of the Methods in Neuroscience at Dartmouth (MIND) +Computational Summer School (2023 offering) who used our package +during several workshops and tutorials. + \bibliographystyle{elsarticle-num} \bibliography{main} diff --git a/tests/test_core.ipynb b/tests/test_core.ipynb index baad2ff6..4bcc0c9c 100644 --- a/tests/test_core.ipynb +++ b/tests/test_core.ipynb @@ -2680,7 +2680,7 @@ "source": [ "def test_smuggle_davos_raises():\n", " \"\"\"trying to smuggle davos itself should raise an error\"\"\"\n", - " with raises(davos.core.exceptions.TheNightIsDarkAndFullOfTErrors):\n", + " with raises(davos.core.exceptions.TheNightIsDarkAndFullOfErrors):\n", " smuggle davos" ] },