Skip to content

Commit

Permalink
Allow specifying Python version (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
tusharsadhwani authored May 17, 2024
1 parent b5a9361 commit d2ca06c
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 28 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ packaged ./curve.bin 'pip install -r requirements.txt' 'python bubble_sort_curve

This produces a `./curve.bin` binary with:

- Python 3.11
- Python 3.12
- `matplotlib`
- `numba`
- `llvmlite`
Expand All @@ -56,7 +56,7 @@ create your package. For example, try the `minesweeper` project:
packaged ./example/minesweeper
```

[This configuration](tests/end_to_end/test_packages/minesweeper/packaged.toml)
[This configuration](https://github.com/tusharsadhwani/packaged/blob/main/example/minesweeper/packaged.toml)
is used for building the package. The equivalent command to build the project
without `pyproject.toml` would be:

Expand Down
1 change: 1 addition & 0 deletions example/minesweeper/packaged.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
output_path = "minesweeper.bin"
build_command = "pip install ."
startup_command = "python -m minesweeper"
python_version = "3.11"
21 changes: 17 additions & 4 deletions src/packaged/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import subprocess
import tempfile

import yen
import yen.github


MAKESELF_PATH = os.path.join(os.path.dirname(__file__), "makeself.sh")
DEFAULT_PYTHON_VERSION = "3.12"


class SourceDirectoryNotFound(Exception):
Expand All @@ -21,11 +22,20 @@ def __init__(self, directory_path: str) -> None:
self.directory_path = directory_path


class PythonNotAvailable(Exception):
"""Raised when the Python version asked for is not available for download."""

def __init__(self, python_version: str) -> None:
super().__init__(python_version)
self.python_version = python_version


def create_package(
source_directory: str | None,
output_path: str,
build_command: str,
startup_command: str,
python_version: str,
) -> None:
"""Create the makeself executable, with the startup script in it."""
if source_directory is None:
Expand All @@ -43,7 +53,7 @@ def create_package(

try:
# Use `yen` to ensure a portable Python is present on the system
python_version, yen_python_bin_path = ensure_python()
python_version, yen_python_bin_path = ensure_python(python_version)
yen_python_path = os.path.join(yen.PYTHON_INSTALLS_PATH, python_version)
yen_python_bin_relpath = os.path.relpath(yen_python_bin_path, yen_python_path)

Expand Down Expand Up @@ -98,9 +108,12 @@ def create_package(
shutil.rmtree(packaged_python_path)


def ensure_python() -> tuple[str, str]:
def ensure_python(version: str) -> tuple[str, str]:
"""
Checks that the version of Python we want to use is available on the
system, and if not, downloads it.
"""
return yen.ensure_python("3.11")
try:
return yen.ensure_python(version)
except yen.github.NotAvailable:
raise PythonNotAvailable(version)
42 changes: 26 additions & 16 deletions src/packaged/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
from __future__ import annotations

import argparse
import dataclasses
import os.path
import platform
import sys
import typing

from packaged import SourceDirectoryNotFound, create_package
from packaged import (
DEFAULT_PYTHON_VERSION,
PythonNotAvailable,
SourceDirectoryNotFound,
create_package,
)
from packaged.config import (
Config,
ConfigValidationError,
Expand Down Expand Up @@ -40,13 +47,6 @@ def cli(argv: list[str] | None = None) -> int:
error(f"Expected key {exc.key!r} in config")
return 3

source_directory, output_path, build_command, startup_command = (
config.source_directory,
config.output_path,
config.build_command,
config.startup_command,
)

else:
parser = argparse.ArgumentParser()
parser.add_argument("output_path", help="Filename for the generated binary")
Expand All @@ -63,18 +63,28 @@ def cli(argv: list[str] | None = None) -> int:
nargs="?",
default=None,
)
args = parser.parse_args(argv, namespace=Config)

source_directory, output_path, build_command, startup_command = (
args.source_directory,
args.output_path,
args.build_command,
args.startup_command,
parser.add_argument(
"--python-version",
metavar=DEFAULT_PYTHON_VERSION,
help="Version of Python to package your project with.",
default=DEFAULT_PYTHON_VERSION,
)
args = parser.parse_args(argv)
config = Config(**vars(args))

try:
create_package(source_directory, output_path, build_command, startup_command)
create_package(
config.source_directory,
config.output_path,
config.build_command,
config.startup_command,
config.python_version,
)
except SourceDirectoryNotFound as exc:
error(f"Folder {exc.directory_path!r} does not exist.")
return 4
except PythonNotAvailable as exc:
error(f"Python {exc.python_version!r} is not available for download.")
return 5

return 0
2 changes: 2 additions & 0 deletions src/packaged/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Config:
output_path: str
build_command: str
startup_command: str
python_version: str


CONFIG_NAME = "./packaged.toml"
Expand Down Expand Up @@ -52,6 +53,7 @@ def parse_config(source_directory: str) -> Config:
config_data["output_path"],
config_data["build_command"],
config_data["startup_command"],
config_data.get("python_version", "3.12"),
)
except KeyError as exc:
key = exc.args[0]
Expand Down
22 changes: 18 additions & 4 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@

from unittest import mock

from pytest import MonkeyPatch

import packaged
import packaged.cli


def test_cli(monkeypatch: MonkeyPatch) -> None:
def test_cli() -> None:
"""Ensures that CLI passes args to `create_package()` properly."""
with mock.patch.object(packaged.cli, "create_package") as mocked:
packaged.cli.cli(["./foo", "pip install foo", "python -m foo"])

# source_directory is None
mocked.assert_called_with(None, "./foo", "pip install foo", "python -m foo")
mocked.assert_called_with(
None,
"./foo",
"pip install foo",
"python -m foo",
packaged.DEFAULT_PYTHON_VERSION,
)

with mock.patch.object(packaged.cli, "create_package") as mocked:
packaged.cli.cli(
["./baz", "pip install baz", "python -m baz", "--python-version=3.10"]
)

# specified python version
mocked.assert_called_with(None, "./baz", "pip install baz", "python -m baz", "3.10")

with mock.patch.object(packaged.cli, "create_package") as mocked:
packaged.cli.cli(
Expand All @@ -31,6 +44,7 @@ def test_cli(monkeypatch: MonkeyPatch) -> None:
"./bar",
"pip install -rrequirements.txt",
"python src/mypackage/cli.py",
packaged.DEFAULT_PYTHON_VERSION,
)
args = mocked.call_args[0]
assert args[0].endswith("/mypackage")
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ def pytest_sessionstart(session: pytest.Session) -> None:

return

_, python_bin_path = packaged.ensure_python()
_, python_bin_path = packaged.ensure_python(packaged.DEFAULT_PYTHON_VERSION)
assert os.path.isfile(python_bin_path)
6 changes: 5 additions & 1 deletion tests/end_to_end/packaged_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ def build_package(
"""Builds the package, but also delete it afterwards."""
try:
packaged.create_package(
source_directory, output_path, build_command, startup_command
source_directory,
output_path,
build_command,
startup_command,
python_version=packaged.DEFAULT_PYTHON_VERSION,
)
yield
finally:
Expand Down

0 comments on commit d2ca06c

Please sign in to comment.