Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sphinx:
build:
os: ubuntu-22.04
tools:
python: "3.10"
python: "3.11"

python:
install:
Expand Down
12 changes: 12 additions & 0 deletions docs/source/configuration/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,22 @@ environment. Our
[example `requirements.txt` file](https://github.com/binder-examples/requirements/blob/HEAD/requirements.txt)
on GitHub shows a typical requirements file.

(pyproject)=

## `pyproject.toml` - Install Python packages

To install your repository like a Python package, you may include a
`pyproject.toml` file. `repo2docker`installs `pyproject.toml` files by running
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space:

Suggested change
`pyproject.toml` file. `repo2docker`installs `pyproject.toml` files by running
`pyproject.toml` file. `repo2docker` installs `pyproject.toml` files by running

`pip install -e .`.

(setup-py)=

## `setup.py` - Install Python packages

```{note}
We recommend to use `pyproject.toml` as it is the recommended way since 2020 when PEPs [621](https://peps.python.org/pep-0621/) and [631](https://peps.python.org/pep-0631/) were accepted.
```

To install your repository like a Python package, you may include a
`setup.py` file. `repo2docker` installs `setup.py` files by running
`pip install -e .`.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies = [
"semver",
"toml",
"traitlets",
"packaging",
]

[project.urls]
Expand Down
77 changes: 61 additions & 16 deletions repo2docker/buildpacks/python/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
"""Generates Dockerfiles based on an input matrix based on Python."""

import os
import re
from functools import lru_cache

try:
import tomllib
except ImportError:
import tomli as tomllib

from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import Version

from ...utils import is_local_pip_requirement, open_guess_encoding
from ..conda import CondaBuildPack

Expand All @@ -17,23 +26,42 @@ def python_version(self):

name, version, _ = self.runtime

if name != "python" or not version:
# Either not specified, or not a Python runtime (e.g. R, which subclasses this)
if name is not None and name != "python":
# Either not a Python runtime (e.g. R, which subclasses this)
# use the default Python
self._python_version = self.major_pythons["3"]
self.log.warning(
f"Python version unspecified, using current default Python version {self._python_version}. This will change in the future."
)
return self._python_version

py_version_info = version.split(".")
py_version = ""
if len(py_version_info) == 1:
py_version = self.major_pythons[py_version_info[0]]
if name is None or version is None:
self._python_version = self.major_pythons["3"]
runtime_version = Version(self.major_pythons["3"])
self.log.warning(
f"Python version unspecified, using current default Python version {self._python_version}. This will change in the future."
)
else:
# get major.minor
py_version = ".".join(py_version_info[:2])
self._python_version = py_version
self._python_version = version
runtime_version = Version(version)

pyproject_toml = "pyproject.toml"
if not self.binder_dir and os.path.exists(pyproject_toml):
with open(pyproject_toml, "rb") as _pyproject_file:
pyproject = tomllib.load(_pyproject_file)

if "project" in pyproject and "requires-python" in pyproject["project"]:
# This is the minumum version!
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires
pyproject_python_specifier = SpecifierSet(
pyproject["project"]["requires-python"]
)

if runtime_version not in pyproject_python_specifier:
raise RuntimeError(
"runtime.txt version not supported by pyproject.toml."
)

return self._python_version

def _get_pip_scripts(self):
Expand Down Expand Up @@ -78,9 +106,11 @@ def _should_preassemble_pip(self):
If there are any local references, e.g. `-e .`,
stage the whole repo prior to installation.
"""
if not os.path.exists("binder") and os.path.exists("setup.py"):
# can't install from subset if we're using setup.py
return False
# can't install from subset
for _configuration_file in ("pyproject.toml", "setup.py"):
if not os.path.exists("binder") and os.path.exists(_configuration_file):
return False

for name in ("requirements.txt", "requirements3.txt"):
requirements_txt = self.binder_path(name)
if not os.path.exists(requirements_txt):
Expand Down Expand Up @@ -119,26 +149,41 @@ def get_assemble_scripts(self):
# and requirements3.txt (if it exists)
# will be installed in the python 3 notebook server env.
assemble_scripts = super().get_assemble_scripts()
setup_py = "setup.py"
# KERNEL_PYTHON_PREFIX is the env with the kernel,
# whether it's distinct from the notebook or the same.
pip = "${KERNEL_PYTHON_PREFIX}/bin/pip"
if not self._should_preassemble_pip:
assemble_scripts.extend(self._get_pip_scripts())

# setup.py exists *and* binder dir is not used
if not self.binder_dir and os.path.exists(setup_py):
assemble_scripts.append(("${NB_USER}", f"{pip} install --no-cache-dir ."))
for _configuration_file in ("pyproject.toml", "setup.py"):
if not self.binder_dir and os.path.exists(_configuration_file):
assemble_scripts.append(
("${NB_USER}", f"{pip} install --no-cache-dir .")
)
break

return assemble_scripts

def detect(self):
"""Check if current repo should be built with the Python buildpack."""
requirements_txt = self.binder_path("requirements.txt")
pyproject_toml = "pyproject.toml"
setup_py = "setup.py"

name = self.runtime[0]
if name:
return name == "python"
if not self.binder_dir and os.path.exists(pyproject_toml):
with open(pyproject_toml, "rb") as _pyproject_file:
pyproject = tomllib.load(_pyproject_file)

if (
("project" in pyproject)
and ("build-system" in pyproject)
and ("requires" in pyproject["build-system"])
):
return True

if not self.binder_dir and os.path.exists(setup_py):
return True
return os.path.exists(requirements_txt)
4 changes: 4 additions & 0 deletions tests/pyproject/pyproject-toml/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Python - pyproject.toml
-----------------------

``pyproject.toml`` should be used over ``setup.py``.
Empty file.
26 changes: 26 additions & 0 deletions tests/pyproject/pyproject-toml/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mwt"
version = "1.0.0"
dependencies = [
"numpy",
]
requires-python = ">=3.10"
authors = [
{name = "Project Jupyter Contributors", email = "[email protected]"},
]
maintainers = [
{name = "Project Jupyter Contributors", email = "[email protected]"},
]
description = "Test for repo2docker"
readme = "README.rst"
license = "MIT"

[project.urls]
Homepage = "https://repo2docker.readthedocs.io/"
Documentation = "https://repo2docker.readthedocs.io/"
Repository = "https://github.com/jupyterhub/repo2docker.git"
"Bug Tracker" = "https://github.com/jupyterhub/repo2docker/issues"
5 changes: 5 additions & 0 deletions tests/pyproject/pyproject-toml/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python
try:
import mwt
except ImportError:
raise Exception("'mwt' shouldn't have been installed!")
Loading