Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Poetry buildpack #972

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
- julia
- nix
- pipfile
- poetry
- r
- unit
- venv
Expand Down
2 changes: 2 additions & 0 deletions repo2docker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
LegacyBinderDockerBuildPack,
NixBuildPack,
PipfileBuildPack,
PoetryBuildPack,
PythonBuildPack,
RBuildPack,
)
Expand Down Expand Up @@ -97,6 +98,7 @@ def _default_log_level(self):
RBuildPack,
CondaBuildPack,
PipfileBuildPack,
PoetryBuildPack,
PythonBuildPack,
],
config=True,
Expand Down
1 change: 1 addition & 0 deletions repo2docker/buildpacks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .base import BuildPack, BaseImage
from .python import PythonBuildPack
from .pipfile import PipfileBuildPack
from .poetry import PoetryBuildPack
from .conda import CondaBuildPack
from .julia import JuliaProjectTomlBuildPack
from .julia import JuliaRequireBuildPack
Expand Down
186 changes: 186 additions & 0 deletions repo2docker/buildpacks/poetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Buildpack for git repos with poetry.lock or pyproject.toml

`poetry` will be used to install the dependencies conda will provide the base
Python environment, same as the Python or Conda build packs.
"""

import os

from poetry.core.semver import parse_constraint
import toml

from ..conda import CondaBuildPack

# Minimum compatible version of python2 for use with Poetry
COMPATIBLE_PYTHON2_VERSIONS = parse_constraint(">=2.7")

# Minimum compatible version of python3 for use with Poetry
COMPATIBLE_PYTHON3_VERSIONS = parse_constraint(">=3.5")


class PoetryBuildPack(CondaBuildPack):
"""Setup Python with poetry for use with a repository."""

@property
def python_version(self):
"""
Detect the Python version declared in a `poetry.lock`, `pyproject.toml'.
Will return 'x.y' if version is found (e.g '3.6'), or a Falsy empty
string `''` if not found.
"""

if hasattr(self, "_python_version"):
return self._python_version

requested_version = "*"

pyproject = self.binder_path("pyproject.toml")
if os.path.exists(pyproject):
with open(pyproject) as f:
pyproject_info = toml.load(f)
specified_version = (
pyproject_info.get("tool", {})
.get("poetry", {})
.get("dependencies", {})
.get("python", None)
)

if not specified_version is None:
requested_version = specified_version

lockfile = self.binder_path("poetry.lock")
if os.path.exists(lockfile):
with open(lockfile) as f:
lock_info = toml.load(f)
specified_version = lock_info.get("metadata", {}).get(
"python-versions", None
)

if not specified_version is None:
requested_version = specified_version

requested_constraint = parse_constraint(requested_version)

version_range = parse_constraint("*")

if requested_constraint.allows(parse_constraint("2")):
version_range = version_range.intersect(COMPATIBLE_PYTHON2_VERSIONS)

if requested_constraint.allows(parse_constraint("3")):
# If the given constraint allows for python 3, then this will
# overwrite the range provided by python 2
version_range = version_range.intersect(COMPATIBLE_PYTHON3_VERSIONS)

if requested_constraint.allows_any(version_range):
# If the requested constraint is in the version range, then the
# intersection is non-zero and should be valid, so we narrow the
# constraint here
requested_constraint = version_range.intersect(requested_constraint)
else:
# If the requested constraint not in the version range then most
# likely the requested constraint is outside of the the
# COMPATIBLE_PYTHON3_VERSIONS, this should only happen if a newer
# versions of python is explicitly required, we trust this request
requested_constraint = requested_constraint.min

self._python_version = str(requested_constraint.min)

return self._python_version

def get_preassemble_script_files(self):
"""Return files needed for preassembly"""
files = super().get_preassemble_script_files()
for name in ("requirements3.txt", "pyproject.toml", "poetry.lock"):
path = self.binder_path(name)
if os.path.exists(path):
files[path] = path
return files

def get_preassemble_scripts(self):
"""scripts to run prior to staging the repo contents"""
scripts = super().get_preassemble_scripts()
# install poetry to install dependencies within poetry.lock or
# pyproject.toml
scripts.append(
("${NB_USER}", "${KERNEL_PYTHON_PREFIX}/bin/pip install poetry==1.1.3")
)
return scripts

def get_assemble_scripts(self):
"""Return series of build-steps specific to this repository."""
# If we have either poetry.lock, pyproject.toml, or runtime.txt declare
# the use of Python 2, Python 2.7 will be made available in the *kernel*
# environment. The notebook servers environment on the other hand
# requires Python 3 but may require something additional installed in it
# still such as `nbgitpuller`. For this purpose, a "requirements3.txt"
# file will be used to install dependencies for the notebook servers
# environment, if Python 2 had been specified for the kernel
# environment.
assemble_scripts = super().get_assemble_scripts()

if self.py2:
# using Python 2 as a kernel, but Python 3 for the notebook server

# requirements3.txt allows for packages to be installed to the
# notebook servers Python environment
nb_requirements_file = self.binder_path("requirements3.txt")
if os.path.exists(nb_requirements_file):
assemble_scripts.append(
(
"${NB_USER}",
'${{NB_PYTHON_PREFIX}}/bin/pip install --no-cache-dir -r "{}"'.format(
nb_requirements_file
),
)
)

# pyproject.toml and poetry.lock files can have relative path references
# so we should be careful about the working directory during the install
# [tool.poetry.dependencies]
# python = "^3.8"
# extra-data = {path = "sampleproject"}
working_directory = self.binder_dir or "."

# NOTES:
# - poetry either uses a configuration file or environment variables for
# configuration settings, here we use the inline
# `POETRY_VIRTUALENVS_CREATE=false` to tell poetry to not create
# another virtual environment during the install and to just install
# into the system python environment
assemble_scripts.append(
(
"${NB_USER}",
"""(cd {working_directory} && \\
PATH="${{KERNEL_PYTHON_PREFIX}}/bin:$PATH" \\
POETRY_VIRTUALENVS_CREATE=false poetry install \\
)""".format(
working_directory=working_directory,
),
)
)

return assemble_scripts

def detect(self):
"""Check if current repo should be built with the Poetry buildpack."""
# first make sure python is not explicitly unwanted
runtime_txt = self.binder_path("runtime.txt")
if os.path.exists(runtime_txt):
with open(runtime_txt) as f:
runtime = f.read().strip()
if not runtime.startswith("python-"):
return False

pyproject = self.binder_path("pyproject.toml")
poetry_lock = self.binder_path("poetry.lock")

is_poetry = False
if os.path.exists(pyproject):
with open(pyproject) as f:
pyproject_info = toml.load(f)
backend = pyproject_info.get("build-system", {}).get(
"build-backend", ""
)
is_poetry = backend == "poetry.masonry.api"

return is_poetry or os.path.exists(poetry_lock)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def get_identifier(json):
"escapism",
"iso8601",
"jinja2",
"poetry-core",
"python-json-logger",
"requests",
"ruamel.yaml>=0.15",
Expand Down
4 changes: 4 additions & 0 deletions tests/poetry/binder-folder-lock/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Python - binder/poetry.lock + poetry.lock
-----------------------------------------

We should make ``binder/poetry.lock`` take precedence over ``poetry.lock``.
18 changes: 18 additions & 0 deletions tests/poetry/binder-folder-lock/binder/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/poetry/binder-folder-lock/binder/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "binder-folder"
version = "0.1.0"
description = "Test project for poetry buildpack"
authors = ["Robert Rosca <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.8"
cowsay = "*"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
40 changes: 40 additions & 0 deletions tests/poetry/binder-folder-lock/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/poetry/binder-folder-lock/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "binder-folder"
version = "0.1.0"
description = "Test project for poetry buildpack"
authors = ["Robert Rosca <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.8"
sampleproject = "*"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
5 changes: 5 additions & 0 deletions tests/poetry/binder-folder-lock/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python
import cowsay

# pypi_pkg_test is installed from the binder folder's Pipfile, but not from the
# root folder's Pipfile!
4 changes: 4 additions & 0 deletions tests/poetry/binder-folder/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Python - binder/pyproject.toml + pyproject.toml
-----------------------------------------------

We should make ``binder/pyproject.toml`` take precedence over ``pyproject.toml``.
15 changes: 15 additions & 0 deletions tests/poetry/binder-folder/binder/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "binder-folder"
version = "0.1.0"
description = "Test project for poetry buildpack"
authors = ["Robert Rosca <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.8"
cowsay = "*"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
15 changes: 15 additions & 0 deletions tests/poetry/binder-folder/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "binder-folder"
version = "0.1.0"
description = "Test project for poetry buildpack"
authors = ["Robert Rosca <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.8"
sampleproject = "*"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
5 changes: 5 additions & 0 deletions tests/poetry/binder-folder/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python
import cowsay

# pypi_pkg_test is installed from the binder folder's Pipfile, but not from the
# root folder's Pipfile!
6 changes: 6 additions & 0 deletions tests/poetry/environment-yml/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Python - pyproject.toml (poetry.lock) + environment.yml
-------------------------------------------------------

We should ignore the ``pyproject.toml`` or ``poetry.lock`` if there is an
``environment.yml`` alongside it. Conda can install more things than ``pip`` or
can so we would limit ourselves if we prioritized the ``Pipfile``s.
3 changes: 3 additions & 0 deletions tests/poetry/environment-yml/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies:
- pip:
- sampleproject
Loading