├── .github └── workflows │ ├── CI.yaml │ └── pypi.yaml ├── .gitignore ├── .static ├── jupyter-screenshot-2.png └── jupyter-screenshot.png ├── CHANGELOG.md ├── CONTRIBUTING.md ├── FAQ.md ├── LICENSE.md ├── Makefile ├── README.md ├── setup.py ├── src └── localvenv_kernel │ ├── __init__.py │ ├── __main__.py │ └── kernelspec.py └── test ├── requirements-local.txt ├── requirements-site.txt └── test_kernel.py /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - dev 7 | - 'release-*' 8 | tags: 9 | - '*' 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | env: 15 | PIP_DISABLE_PIP_VERSION_CHECK: 1 16 | KERNEL_VENV: .local-venv 17 | 18 | jobs: 19 | test: 20 | name: Test (Linux) 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@master 24 | - name: Install Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: '3.9' 28 | - name: Install prerequisites 29 | run: | 30 | python -m pip install -r test/requirements-site.txt 31 | - name: Build PyPI distributions 32 | run: make build 33 | - name: Check dist files 34 | run: twine check dist/* 35 | - name: Install wheel 36 | run: | 37 | python -m pip install dist/*.whl 38 | - name: Set up project environment for test 39 | run: | 40 | python -m venv .local-venv 41 | .local-venv/bin/python -m pip install -r test/requirements-local.txt 42 | - name: Run test 43 | run: | 44 | python test/test_kernel.py 45 | test-windows: 46 | name: Test (Windows) 47 | runs-on: windows-latest 48 | steps: 49 | - uses: actions/checkout@master 50 | - name: Install Python 51 | uses: actions/setup-python@v4 52 | with: 53 | python-version: '3.11' 54 | - name: Install prerequisites 55 | run: | 56 | python -m pip install -r test\requirements-site.txt 57 | - name: Install package 58 | run: | 59 | python -m pip install . 60 | - name: Set up project environment for test 61 | run: | 62 | python -m venv .local-venv 63 | .\.local-venv\Scripts\activate 64 | python -m pip install -r test\requirements-local.txt 65 | - name: Run test 66 | run: | 67 | python test\test_kernel.py 68 | codestyle: 69 | name: Codestyle 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: psf/black@stable 74 | with: 75 | options: "--check --diff --line-length 79" 76 | version: "~= 23.0" 77 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | env: 8 | PIP_DISABLE_PIP_VERSION_CHECK: 1 9 | 10 | jobs: 11 | publish-pypi: 12 | name: Publish to PyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | - name: Install Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.10' 20 | - run: | 21 | python -m pip install build wheel --user 22 | - name: Build PyPI distributions 23 | run: | 24 | python -m build --no-isolation 25 | - name: Publish a Python distribution to PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | with: 28 | user: __token__ 29 | password: ${{ secrets.PYPI_API_TOKEN_PYTHON_LOCALVENV_KERNEL }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | .site-venv 111 | .local-venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | -------------------------------------------------------------------------------- /.static/jupyter-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goerz/python-localvenv-kernel/5ca734a4624009698e29c53de6caceae1dc1110b/.static/jupyter-screenshot-2.png -------------------------------------------------------------------------------- /.static/jupyter-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goerz/python-localvenv-kernel/5ca734a4624009698e29c53de6caceae1dc1110b/.static/jupyter-screenshot.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 4 | 5 | 6 | ## Unreleased 7 | 8 | This project is in alpha status and does not yet have a 1.0 release. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Development of this package is driven via `make`. If you do not have `make` installed, see the [Makefile][] for the equivalent Python commands. 4 | 5 | Run `make help` (or just `make`) to see a list of targets. 6 | 7 | 8 | ## Development installation 9 | 10 | Run `PYTHON= make develop` to dev-install the package into the environment for the given `python` executable. Note that that Python environment should have Jupyter installed. 11 | 12 | The `make develop` command is mostly equivalent to `$PYTHON -m pip install -e .`, except that an editable install will not copy the kernel file into the environment. When not using the `Makefile`, you must manually copy the `build/kernels/python-localvenv` folder to `{sys.prefix}/share/jupyter/kernels/python-localvenv`. 13 | 14 | 15 | ## Running tests locally 16 | 17 | Run `make test`. See the [Makefile][] and [`test/test_kernel.py`](test/test_kernel.py) for details. 18 | 19 | 20 | ## Code style 21 | 22 | Code should be formatted with `black -l 79` using the [Black formatter](https://black.readthedocs.io/en/stable/). 23 | 24 | Apply the formatting with `make codestyle`. 25 | 26 | 27 | ## Making a release 28 | 29 | Releases happen by simply tagging a commit as, e.g., `v1.0.0`. The version number in [`setup.py`](setup.py) must correspond to the tagged version. In between releases, the version number should have a `+dev` or `-dev` suffix, see [Inter-Release Versioning Recommendations](https://michaelgoerz.net/notes/inter-release-versioning-recommendations.html). 30 | 31 | There is a Github workflow that will automatically upload the release to [PyPI](https://pypi.org/project/python-localvenv-kernel/). 32 | 33 | [There also is a bot](https://conda-forge.org/docs/maintainer/updating_pkgs.html) that monitors PyPI and automatically updates the [Conda feedstock](https://github.com/conda-forge/python-localvenv-kernel-feedstock). It may take a day or so before the new release is detected, so please be patient. 34 | 35 | 36 | [Makefile]: Makefile 37 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Why do I have to install `ipykernel` manually? 4 | 5 | Virtual environments should be reproducible. We will not modify the project environment in any way. 6 | 7 | ## The "Python (local .venv)" kernel does not show up in Jupyter 8 | 9 | The `python-localvenv-kernl` must be installed in the same environment as `jupyter`. Run `jupyter kernelspec list`. This should show something like 10 | 11 | ``` 12 | … 13 | python-localvenv {sys.prefix}/share/jupyter/kernels/python-localvenv 14 | python3 {sys.prefix}/share/jupyter/kernels/python3 15 | ``` 16 | 17 | where `{sys.prefix}` is the path to the environment where `jupyter` is installed. The `python3` kernel on the last line is the default kernel for that Jupyter installation. If `python-localvenv` is not listed with the same `{sys.prefix}`, the package is not installed correctly. Run 18 | 19 | ``` 20 | ./bin/python -m pip install python-localvenv-kernel 21 | ``` 22 | 23 | inside the `{sys.prefix}` folder to install the package into the environment (or, if the `{sys.prefix}` folder is managed by `conda`, use `conda install python-localvenv-kernel` as appropriate). 24 | 25 | 26 | ## How can I change the directory for the virtual environment? 27 | 28 | Setting the environment variable `KERNEL_VENV` allows to override the folder name for the project virtual environment. The `python-localvenv-kernel` will search for the folder name in the directory where the notebook file is located and all its parent directories. 29 | 30 | Setting `KERNEL_VENV` to an absolute path will use that path directly. In all cases, the `KERNEL_VENV` must point to a Python environment and have the `ipykernel` package installed. 31 | 32 | A valid Python environment must have a Python executable at `{KERNEL_VENV}/bin/python` (`{KERNEL_VENV}\Scripts\python` on Windows). For exotic environments, the location of the `python` executable can set with via the `KERNEL_VENV_PYTHON` environment variable, relative to `KERNEL_VENV`. 33 | 34 | 35 | ## How does this kernel differ from poetry-kernel? 36 | 37 | The `python-localvenv-kernel` is derived from the [`poetry-kernel`](https://github.com/pathbird/poetry-kernel). However, instead of delegating to whatever virtual environment Poetry has set up for a project, `python-localvenv-kernel` always delegates to a virtual environment in the `.venv` subdirectory of the project folder (respectively, the directory pointed to by the `KERNEL_VENV` environment variable). 38 | 39 | Thus, `python-localvenv-kernel` does not depend on Poetry. The `.venv` directory could be set up with a simple `python -m venv .venv` and initialized with `pip` based on a `requirements.txt`. 40 | 41 | If Poetry's [`virtualenvs.in-project`](https://python-poetry.org/docs/configuration/#virtualenvsin-project) option is set to `true`, Poetry will use a local `.venv` folder for its virtual environment. In that case, the `python-localvenv-kernel` is a *replacement* for `poetry-kernel`. If instead, Poetry is set up to create virtual environments in its [cache directory](https://python-poetry.org/docs/configuration/#cache-directory), using the `poetry-kernel` might be more appropriate. However, even in that case, `python-locavenv-kernel` could still be used, by setting the environment variable 42 | 43 | ~~~ 44 | KERNEL_VENV=`poetry env info -p` 45 | ~~~ 46 | 47 | immediately before launching `jupyter` from the project directory. 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Michael H. Goerz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean install develop uninstall build test 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | define PRINT_HELP_PYSCRIPT 6 | import re, sys 7 | 8 | for line in sys.stdin: 9 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 10 | if match: 11 | target, help = match.groups() 12 | print("%-20s %s" % (target, help)) 13 | endef 14 | export PRINT_HELP_PYSCRIPT 15 | 16 | 17 | define DEVELOP_KERNEL_INSTALL_PYSCRIPT 18 | import sys 19 | import shutil 20 | from pathlib import Path 21 | src = Path("build") / "kernels" 22 | dst = Path(sys.prefix) / "share" / "jupyter" / "kernels" 23 | shutil.copytree(src, dst, dirs_exist_ok=True) 24 | endef 25 | export DEVELOP_KERNEL_INSTALL_PYSCRIPT 26 | 27 | 28 | define DEVELOP_KERNEL_UNINSTALL_PYSCRIPT 29 | import sys 30 | import shutil 31 | from pathlib import Path 32 | kernel = Path(sys.prefix) / "share" / "jupyter" / "kernels" / "python-localvenv" 33 | if kernel.is_dir(): 34 | print(f'Removing "{kernel}"') 35 | shutil.rmtree(kernel) 36 | endef 37 | export DEVELOP_KERNEL_UNINSTALL_PYSCRIPT 38 | 39 | 40 | PYTHON ?= python3 41 | 42 | help: ## Show this help 43 | @$(PYTHON) -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 44 | 45 | install: ## Install the package into the current PYTHON installation with pip 46 | @$(PYTHON) -m pip install . 47 | 48 | develop: ## Dev-install the package into the current PYTHON installation with pip 49 | @$(PYTHON) -m pip install -e . 50 | @$(PYTHON) -c "$$DEVELOP_KERNEL_INSTALL_PYSCRIPT" 51 | 52 | uninstall: ## Uninstall package installed with `make install` or `make develop` 53 | @$(PYTHON) -m pip uninstall python-localvenv-kernel 54 | @$(PYTHON) -c "$$DEVELOP_KERNEL_UNINSTALL_PYSCRIPT" 55 | 56 | build: ## Locally build package (source distribution and wheel) 57 | $(PYTHON) -m build --no-isolation 58 | 59 | .site-venv/bin/python: 60 | $(PYTHON) -m venv .site-venv 61 | PIP_DISABLE_PIP_VERSION_CHECK=1 .site-venv/bin/python -m pip install -r test/requirements-site.txt 62 | PIP_DISABLE_PIP_VERSION_CHECK=1 .site-venv/bin/python -m pip install -e . 63 | .site-venv/bin/python -c "$$DEVELOP_KERNEL_INSTALL_PYSCRIPT" 64 | 65 | .local-venv/bin/python: 66 | $(PYTHON) -m venv .local-venv 67 | PIP_DISABLE_PIP_VERSION_CHECK=1 .local-venv/bin/python -m pip install -r test/requirements-local.txt 68 | 69 | test: .site-venv/bin/python .local-venv/bin/python ## Test the kernel 70 | KERNEL_VENV=.local-venv .site-venv/bin/python test/test_kernel.py 71 | 72 | codestyle: ## Auto-format the code 73 | black --line-length 79 . 74 | 75 | clean: ## Remove all build and compilation artifacts 76 | rm -rf build 77 | rm -rf dist 78 | rm -rf python_localvenv_kernel.egg-info 79 | rm -rf src/localvenv_kernel/__pycache__ 80 | rm -rf test/__pycache__ 81 | rm -rf .site-venv 82 | rm -rf .local-venv 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Local `.venv` Kernel 2 | 3 | [![Github](https://img.shields.io/badge/goerz-python--localvenv--kernel-blue.svg?logo=github)](https://github.com/goerz/python-localvenv-kernel) 4 | [![Build Status](https://github.com/goerz/python-localvenv-kernel/workflows/CI/badge.svg)](https://github.com/goerz/python-localvenv-kernel/actions) 5 | [![PyPI](https://img.shields.io/pypi/v/python-localvenv-kernel.svg)](https://pypi.org/project/python-localvenv-kernel/) 6 | [![Conda Version](https://img.shields.io/conda/vn/conda-forge/python-localvenv-kernel.svg)](https://anaconda.org/conda-forge/python-localvenv-kernel) 7 | [![Conda Recipe](https://img.shields.io/badge/recipe-conda--forge-green.svg)](https://github.com/conda-forge/python-localvenv-kernel-feedstock) 8 | [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 9 | 10 | A Jupyter kernel that delegates to `ipykernel` in the `.venv` environment of a project folder. 11 | 12 | Derived from [`poetry-kernel`](https://github.com/pathbird/poetry-kernel), see the [FAQ](https://github.com/goerz/python-localvenv-kernel/blob/master/FAQ.md#how-does-this-kernel-differ-from-poetry-kernel). 13 | 14 | See the demo at the December 2023 Jupyter Community Call: 15 | 16 | [![Jupyter Community Call](https://img.youtube.com/vi/hUU77BfU-Kk/0.jpg)](https://www.youtube.com/watch?v=hUU77BfU-Kk&t=360) 17 | 18 | 19 | ## Installation 20 | 21 | The `python-localvenv-kernel` package can be installed via `pip` (`pip install python-localvenv-kernel`) or as a [Conda package](https://github.com/conda-forge/python-localvenv-kernel-feedstock#about-python-localvenv-kernel-feedstock) (`conda install python-localvenv-kernel`). 22 | 23 | It must be installed into the same environment as the Jupyter server, see [Usage](#usage) below. 24 | 25 | 26 | ## Usage 27 | 28 | * Jupyter and the `python-localvenv-kernel` package should be installed in the same environment 29 | * The project folder must have a virtual (project) environment instantiated in a subfolder `.venv`. The name of folder can be overridden by setting the environment variable `KERNEL_VENV` (see [FAQ](https://github.com/goerz/python-localvenv-kernel/blob/master/FAQ.md#how-can-i-change-the-directory-for-the-virtual-environment)) 30 | * The project environment must include the `ipykernel` package (but not `jupyter`) 31 | * Start Jupyter from the project folder 32 | * Select the "Python (local .venv)" kernel 33 | 34 | ![Jupyter launcher screenshot (kernel selector)](https://github.com/goerz/python-localvenv-kernel/blob/master/.static/jupyter-screenshot.png?raw=true) 35 | 36 | ![Jupyter launcher screenshot (notebook)](https://github.com/goerz/python-localvenv-kernel/blob/master/.static/jupyter-screenshot-2.png?raw=true) 37 | 38 | 39 | ## FAQ 40 | 41 | [See `FAQ.md`.][FAQ] 42 | 43 | [FAQ]: https://github.com/goerz/python-localvenv-kernel/blob/master/FAQ.md 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | from glob import glob 5 | from pathlib import Path 6 | 7 | from setuptools import find_packages, setup 8 | 9 | current_dir = os.path.abspath(os.path.dirname(__file__)) 10 | setup_args = dict() 11 | 12 | # Package the kernel.json file 13 | # Note: this was adapted from the ipython/ipykernel setup.py script 14 | # https://github.com/ipython/ipykernel/blob/abefee4c935ee79d3821dfda02f1511f55d4c996/setup.py#L95 15 | # (Modified BSD License) 16 | if any(a.startswith(("bdist", "install", "develop")) for a in sys.argv): 17 | sys.path.insert(0, os.path.join(current_dir, "src")) 18 | 19 | spec_dir = Path(current_dir) / "build" / "kernels" / "python-localvenv" 20 | if spec_dir.is_dir(): 21 | shutil.rmtree(spec_dir) 22 | spec_dir.mkdir(parents=True) 23 | from localvenv_kernel.kernelspec import _write_kernelspec 24 | 25 | _write_kernelspec(spec_dir) 26 | 27 | setup_args["data_files"] = [ 28 | # Extract the kernel.json file relative to the installation root 29 | # (i.e., the virtual environment or system Python installation). 30 | ( 31 | os.path.join("share", "jupyter", "kernels", "python-localvenv"), 32 | [str(f) for f in spec_dir.glob("*")], 33 | ), 34 | ] 35 | # NOTE: Editable installs won't copy the kernel file. See `Makefile` 36 | 37 | with open(os.path.join(current_dir, "README.md")) as fp: 38 | README = fp.read() 39 | 40 | setup( 41 | name="python-localvenv-kernel", 42 | version="0.1.7", 43 | author="Michael H. Goerz", 44 | author_email="mail@michaelgoerz.net", 45 | url="https://github.com/goerz/python-localvenv-kernel", 46 | license="MIT", 47 | description=( 48 | "Python Jupyter kernel delegating to a local virtual environment" 49 | ), 50 | long_description=README, 51 | long_description_content_type="text/markdown", 52 | keywords=["Interactive", "Interpreter", "Shell", "Web"], 53 | classifiers=[ 54 | "Framework :: Jupyter", 55 | "Intended Audience :: Developers", 56 | "Intended Audience :: System Administrators", 57 | "Intended Audience :: Science/Research", 58 | "License :: OSI Approved :: MIT License", 59 | "Operating System :: OS Independent", 60 | "Programming Language :: Python", 61 | "Programming Language :: Python :: 3", 62 | ], 63 | packages=find_packages(where="src"), 64 | package_dir={"": "src"}, 65 | python_requires=">=3.9", 66 | install_requires=[ 67 | "colorama ~= 0.4.4", 68 | ], 69 | **setup_args, 70 | ) 71 | -------------------------------------------------------------------------------- /src/localvenv_kernel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goerz/python-localvenv-kernel/5ca734a4624009698e29c53de6caceae1dc1110b/src/localvenv_kernel/__init__.py -------------------------------------------------------------------------------- /src/localvenv_kernel/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import signal 4 | import subprocess 5 | import sys 6 | from textwrap import dedent 7 | from pathlib import Path 8 | 9 | import colorama 10 | 11 | 12 | def main(): 13 | colorama.init() 14 | 15 | # The working directory (cwd) is always the folder that contains the 16 | # notebook. This may be a subfolder of the directory in which the Jupyter 17 | # server was started. 18 | cwd = Path().resolve() 19 | name = os.environ.get("KERNEL_VENV", ".venv").strip() 20 | venv_folder = find_venv(cwd, name) 21 | if venv_folder is None: 22 | if os.path.isabs(name): 23 | error( 24 | f""" 25 | Expected folder KERNEL_VENV="{name}" to exist 26 | """ 27 | ) 28 | else: 29 | error( 30 | f""" 31 | Expected folder "{name}" in notebook directory {cwd} 32 | (or any parent directory) 33 | """ 34 | ) 35 | python = find_python(venv_folder) 36 | cmd_check_kernel = [ 37 | python, 38 | "-m", 39 | "ipykernel_launcher", 40 | "--version", 41 | ] 42 | try: 43 | kernel_version = subprocess.check_output( 44 | cmd_check_kernel, stderr=subprocess.STDOUT, text=True 45 | ) 46 | except subprocess.CalledProcessError as exc_info: 47 | error( 48 | f""" 49 | {' '.join([str(a) for a in cmd_check_kernel])} 50 | returned exit status {exc_info.returncode} 51 | 52 | ERROR: {exc_info.output.strip()} 53 | 54 | Make sure that the 'ipykernel' package is installed in the virtual 55 | environment {venv_folder} 56 | """ 57 | ) 58 | except OSError as exc_info: 59 | error( 60 | f""" 61 | {' '.join([str(a) for a in cmd_check_kernel])} 62 | failed: 63 | 64 | ERROR: {exc_info} 65 | """ 66 | ) 67 | cmd = [ 68 | python, 69 | "-m", 70 | "ipykernel_launcher", 71 | *sys.argv[1:], 72 | ] 73 | print( 74 | colorama.Fore.GREEN 75 | + "PYTHON-LOCALVENV KERNEL" 76 | + colorama.Style.RESET_ALL 77 | + " delegate to " 78 | + " ".join([str(part) for part in cmd]) 79 | + f" (version {kernel_version.strip()})", 80 | file=sys.stderr, 81 | ) 82 | proc = subprocess.Popen(cmd) 83 | 84 | if platform.system() == "Windows": 85 | forward_signals = set(signal.Signals) - { 86 | signal.CTRL_BREAK_EVENT, 87 | signal.CTRL_C_EVENT, 88 | signal.SIGTERM, 89 | } 90 | else: 91 | forward_signals = set(signal.Signals) - { 92 | signal.SIGKILL, 93 | signal.SIGSTOP, 94 | } 95 | 96 | def handle_signal(sig, _frame): 97 | proc.send_signal(sig) 98 | 99 | for sig in forward_signals: 100 | signal.signal(sig, handle_signal) 101 | 102 | exit_code = proc.wait() 103 | if exit_code == 0: 104 | print( 105 | "PYTHON-LOCALVENV KERNEL: ipykernel_launcher exited", 106 | file=sys.stderr, 107 | ) 108 | else: 109 | print( 110 | "PYTHON-LOCALVENV KERNEL: ipykernel_launcher exited with code:", 111 | exit_code, 112 | file=sys.stderr, 113 | ) 114 | 115 | 116 | def error(msg): 117 | print( 118 | colorama.Fore.RED 119 | + colorama.Style.BRIGHT 120 | + "\n" 121 | + "!" * 80 122 | + "\nCannot start python-localvenv kernel:\n" 123 | + dedent(msg) 124 | + "!" * 80 125 | + "\n" 126 | + colorama.Style.RESET_ALL, 127 | file=sys.stderr, 128 | ) 129 | sys.exit(1) 130 | 131 | 132 | def find_venv(root, name): 133 | candidate_dirs = [root, *root.parents] 134 | if os.path.isabs(name) and os.path.isdir(name): 135 | return Path(name) 136 | else: 137 | for dirs in candidate_dirs: 138 | venv_folder = dirs / name 139 | if venv_folder.is_dir(): 140 | return venv_folder 141 | return None 142 | 143 | 144 | def find_python(venv): 145 | if "KERNEL_VENV_PYTHON" in os.environ: 146 | python = venv / os.path.normpath(os.environ["KERNEL_VENV_PYTHON"]) 147 | else: 148 | python = venv / "bin" / "python" 149 | if platform.system() == "Windows": 150 | python = venv / "Scripts" / "python.exe" # python -m venv 151 | if not python.is_file(): # some versions of conda (?) 152 | python = venv / "python.exe" 153 | if not python.is_file(): 154 | print(f"WARNING: file '{python}' does not exist") 155 | return python 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /src/localvenv_kernel/kernelspec.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | import tempfile 4 | 5 | 6 | def _write_kernelspec(dir): 7 | spec = { 8 | "argv": [ 9 | "python", 10 | "-m", 11 | "localvenv_kernel", 12 | "-f", 13 | "{connection_file}", 14 | ], 15 | "display_name": "Python (local .venv)", 16 | "language": "python", 17 | "metadata": { 18 | "debugger": True, 19 | }, 20 | } 21 | with open(os.path.join(dir, "kernel.json"), "w") as fp: 22 | json.dump(spec, fp) 23 | 24 | 25 | def install(): 26 | # Make this import inside the install function because this file is used 27 | # during package install and we don't necessarily have jupyter_client 28 | # installed yet 29 | from jupyter_client.kernelspec import KernelSpecManager 30 | 31 | manager = KernelSpecManager() 32 | with tempfile.TemporaryDirectory() as tmpdir: 33 | _write_kernelspec(tmpdir) 34 | manager.install_kernel_spec(tmpdir, kernel_name="python-localvenv") 35 | -------------------------------------------------------------------------------- /test/requirements-local.txt: -------------------------------------------------------------------------------- 1 | sympy 2 | ipykernel 3 | -------------------------------------------------------------------------------- /test/requirements-site.txt: -------------------------------------------------------------------------------- 1 | black 2 | build 3 | jupyter 4 | jupyter_kernel_test 5 | twine 6 | wheel 7 | -------------------------------------------------------------------------------- /test/test_kernel.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import jupyter_kernel_test as jkt 3 | from os.path import sep 4 | 5 | # Using test suite from https://github.com/jupyter/jupyter_kernel_test 6 | 7 | # This test file must be run from a "site" Python environment that has the 8 | # packages listed in `requirements-site.txt` as well as the 9 | # `python-localvenv-kernel` package. The environment variable 10 | # `KERNEL_VENV=.local-venv` must be set. 11 | # 12 | # Furthermore, the project root must contain a folder `.local-venv` that has 13 | # the packages listed in `requirements-local.txt` (most importantly, 14 | # `ipykernel`) 15 | # 16 | # On a Unix system, running `make test` from the project root should do the 17 | # right thing. See the `Makefile` for details, or the CI Github workflow. 18 | # 19 | # This only tests the "happy path". Feel free to play around with what happens 20 | # if the `.local-venv` environment doesn't exist, or if `ipykernel` is removed 21 | # from the `.local-venv` environment. 22 | 23 | 24 | class LocalVenvKernelTests(jkt.KernelTests): 25 | kernel_name = "python-localvenv" 26 | language_name = "python" 27 | 28 | # Code in the kernel's language to write "hello, world" to stdout 29 | code_hello_world = "print('hello, world')" 30 | 31 | def test_localvenv_kernel_module_location(self): 32 | self.flush_channels() 33 | # We check that the `sympy` package (which should not be present in 34 | # the "site" environment) is loaded from the project virtual 35 | # environment (`.local-venv` in the project root) 36 | reply, output_msgs = self.execute_helper( 37 | code="import sympy; sympy.__file__" 38 | ) 39 | output = output_msgs[0]["content"]["data"]["text/plain"] 40 | self.assertTrue(f"{sep}.local-venv{sep}" in output) 41 | self.assertFalse(f"{sep}.site-venv{sep}" in output) 42 | 43 | 44 | if __name__ == "__main__": 45 | unittest.main() 46 | --------------------------------------------------------------------------------