Skip to content

Commit

Permalink
Optionally select local PyApp source (#19)
Browse files Browse the repository at this point in the history
* allow for local pyapp source

* add test/fix for when local file does not exist
  • Loading branch information
trappitsch authored Feb 28, 2024
1 parent f763f95 commit 849c7d1
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 44 deletions.
10 changes: 5 additions & 5 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ click==8.1.7
# via rich-click
colorama==0.4.6
# via box
coverage==7.4.1
coverage==7.4.3
# via pytest-cov
gitdb==4.0.11
# via gitpython
Expand All @@ -34,7 +34,7 @@ pygments==2.17.2
# via rich
pyproject-hooks==1.0.0
# via build
pytest==8.0.0
pytest==8.0.2
# via pytest-cov
# via pytest-mock
pytest-cov==4.1.0
Expand All @@ -44,10 +44,10 @@ rich==13.7.0
# via rich-click
rich-click==1.7.3
# via box
ruff==0.1.15
ruff==0.2.2
smmap==5.0.1
# via gitdb
tomlkit==0.12.3
tomlkit==0.12.4
# via box
typing-extensions==4.9.0
typing-extensions==4.10.0
# via rich-click
4 changes: 2 additions & 2 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ rich==13.7.0
# via rich-click
rich-click==1.7.3
# via box
tomlkit==0.12.3
tomlkit==0.12.4
# via box
typing-extensions==4.9.0
typing-extensions==4.10.0
# via rich-click
13 changes: 11 additions & 2 deletions src/box/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,16 @@ def init(quiet, builder, optional_deps, entry, python_version):
is_flag=True,
help="Flag to enable verbose mode.",
)
def package(verbose):
@click.option(
"-p",
"--pyapp-source",
default=None,
help=(
"Use local PyApp source code. "
"Provide path to the folder or the .tar.gz archive."
),
)
def package(verbose, pyapp_source):
"""Build the project, then package it with PyApp.
Note that if the pyapp source is already in the `build` directory,
Expand All @@ -74,7 +83,7 @@ def package(verbose):
my_packager = PackageApp(verbose=verbose)
my_packager.check_requirements()
my_packager.build()
my_packager.package()
my_packager.package(local_source=pyapp_source)
binary_file = my_packager.binary_name
fmt.success(
f"Project successfully packaged.\n"
Expand Down
96 changes: 62 additions & 34 deletions src/box/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import shutil
import subprocess
import tarfile
from typing import List
from typing import List, Union
import urllib.request

import rich_click as click
Expand All @@ -31,7 +31,7 @@ def __init__(self, verbose=False):
self.subp_kwargs["stdout"] = subprocess.DEVNULL
self.subp_kwargs["stderr"] = subprocess.DEVNULL

self.binary_name = None # name of the binary file at the end of packaging
self._binary_name = None # name of the binary file at the end of packaging

# self._builder = box_config.builder
self._dist_path = Path.cwd().joinpath("dist")
Expand Down Expand Up @@ -63,6 +63,10 @@ def builders(self) -> List:
"""Return a list of supported builders and their commands."""
return list(self._builders.keys())

@property
def binary_name(self):
return self._binary_name

@property
def config(self) -> PyProjectParser:
"""Return the project configuration."""
Expand All @@ -81,56 +85,80 @@ def build(self):

fmt.success(f"Project built with {builder}.")

def package(self):
"""Package the project with PyApp."""
def package(self, local_source: Union[Path, str] = None):
"""Package the project with PyApp.
:param local_source: Path to the local source. Can be folder or .tar.gz archive.
"""
fmt.info("Hold on, packaging the project with PyApp...")
self._build_dir.mkdir(exist_ok=True)
self._release_dir.mkdir(parents=True, exist_ok=True)
self._get_pyapp()
self._get_pyapp(local_source=local_source)
self._set_env()
self._package_pyapp()

def _get_pyapp(self):
def _get_pyapp(self, local_source: Union[Path, str] = None):
"""Download the PyApp source code and extract to `build/pyapp-latest` folder.
Download and or extraction are skipped if folder already exists.
:param local_source: Path to the local source. Can be folder or .tar.gz archive.
:raises: `click.ClickException` if no pyapp source code is found
"""
tar_name = Path("pyapp-source.tar.gz")

with ut.set_dir(self._build_dir):
if not tar_name.is_file():
urllib.request.urlretrieve(PYAPP_SOURCE, tar_name)
if isinstance(local_source, str):
local_source = Path(local_source)

if not tar_name.is_file():
raise click.ClickException(
"Error: no pyapp source code found. "
"Please check your internet connection and try again."
)
with ut.set_dir(self._build_dir):
if local_source: # copy local source if provided
if local_source.suffix == ".gz" and local_source.is_file():
shutil.copy(local_source, tar_name)
elif local_source.is_dir():
shutil.copytree(
local_source, self._build_dir.joinpath(local_source.name)
)
else:
raise click.ClickException(
"Error: invalid local pyapp source code. "
"Please provide a valid folder or a .tar.gz archive."
)

else: # no local source
if not tar_name.is_file():
urllib.request.urlretrieve(PYAPP_SOURCE, tar_name)

if not tar_name.is_file():
raise click.ClickException(
"Error: no pyapp source code found. "
"Please check your internet connection and try again."
)

# check if pyapp source code is already extracted
all_pyapp_folders = []
for file in Path(".").iterdir():
if file.is_dir() and file.name.startswith("pyapp-"):
all_pyapp_folders.append(file)

with tarfile.open(tar_name, "r:gz") as tar:
tarfile_members = tar.getmembers()

# only extract if the folder in there (first entry) does not already exist
folder_exists = False
new_folder = tarfile_members[0].name
for folder in all_pyapp_folders:
if folder.name == new_folder:
folder_exists = True
break

# extract the source with tarfile package as pyapp-latest
if not folder_exists:
tar.extractall()
if "pyapp-" in new_folder:
all_pyapp_folders.append(Path(new_folder))
# extract the source code if we didn't just copy a local folder
if not local_source or local_source.suffix == ".gz":
with tarfile.open(tar_name, "r:gz") as tar:
tarfile_members = tar.getmembers()

# only extract if the folder in archive does not already exist
folder_exists = False
new_folder = tarfile_members[0].name
for folder in all_pyapp_folders:
if folder.name == new_folder:
folder_exists = True
break

# extract the source with tarfile package
if not folder_exists:
tar.extractall()
if "pyapp-" in new_folder:
all_pyapp_folders.append(Path(new_folder))

# find the name of the pyapp folder and return it
if len(all_pyapp_folders) == 1:
Expand Down Expand Up @@ -168,10 +196,10 @@ def _package_pyapp(self):
if not binary_path.is_file():
binary_path = binary_path.with_suffix(".exe") # we are probably on windows!
suffix = ".exe"
self.binary_name = self._release_dir.joinpath(self.config.name_pkg).with_suffix(
suffix
)
shutil.move(binary_path, self.binary_name)
self._binary_name = self._release_dir.joinpath(
self.config.name_pkg
).with_suffix(suffix)
shutil.move(binary_path, self._binary_name)

def _set_env(self):
"""Set the environment for packaging the project with PyApp."""
Expand Down
25 changes: 24 additions & 1 deletion tests/cli/test_cli_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_package_project(rye_project, mocker, verbose):
runner = CliRunner()
result = runner.invoke(cli, cmd)
assert result.exit_code == 0
assert result.output.__contains__("Project successfully packaged.")
assert "Project successfully packaged." in result.output

# assert system calls
sp_run_mock.assert_any_call(
Expand All @@ -63,6 +63,29 @@ def test_package_project(rye_project, mocker, verbose):
)


@pytest.mark.parametrize("pyapp_source_name", ["pyapp-source.tar.gz", "pyapp-v0.14.0"])
def test_package_project_local_pyapp(rye_project, mocker, data_dir, pyapp_source_name):
"""Package an initialized project with local pyapp source."""
mocker.patch("subprocess.run")
urllib_mock = mocker.patch.object(urllib.request, "urlretrieve") # not called

mocker.patch("box.packager.PackageApp._package_pyapp")
mocker.patch("box.packager.PackageApp.binary_name", return_value="pyapp")

# create dist folder and package
dist_folder = rye_project.joinpath("dist")
dist_folder.mkdir()
dist_folder.joinpath(f"{rye_project.name.replace('-', '_')}-v0.1.0.tar.gz").touch()

runner = CliRunner()
result = runner.invoke(cli, ["package", "-p", data_dir.joinpath(pyapp_source_name)])

assert result.exit_code == 0
urllib_mock.assert_not_called()

# assert rye_project.joinpath("build/pyapp-v0.14.0/source.txt").is_file()


def test_cargo_not_found(rye_project, mocker):
"""Test that cargo not found raises an exception."""
# mock $PATH to remove cargo
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
from box.config import pyproject_writer


@pytest.fixture
def data_dir():
"""Return the path to the data directory."""
return Path(__file__).parent.joinpath("data")


@pytest.fixture
def min_proj_no_box(tmp_path):
"""Create a minimal project with a `pyproject.toml` file."""
Expand Down
Empty file added tests/data/__init__.py
Empty file.
Binary file added tests/data/pyapp-source.tar.gz
Binary file not shown.
1 change: 1 addition & 0 deletions tests/data/pyapp-v0.14.0/source.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a mock source to assure tests with local sources grab the correct files.
23 changes: 23 additions & 0 deletions tests/func/test_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ def test_get_pyapp_wrong_no_pyapp_folder(rye_project, mocker):
assert e.value.args[0].__contains__("Error: no pyapp source code folder found.")


def test_get_pyapp_local_wrong_file(rye_project):
"""Raise an error if local file is not a .tar.gz."""
rye_project.joinpath("build/").mkdir()

wrong_source = rye_project.joinpath("wrong_source.txt")
wrong_source.touch()

packager = PackageApp()

with pytest.raises(click.ClickException):
packager._get_pyapp(local_source="wrong_source.txt")


def test_get_pyapp_local_invalid_file(rye_project):
"""Raise error if given file does not exist."""
rye_project.joinpath("build/").mkdir()

packager = PackageApp()

with pytest.raises(click.ClickException):
packager._get_pyapp(local_source="wrong_source.tar.gz")


@pytest.mark.parametrize("binary_extensions", [".exe", ""])
def test_package_pyapp_cargo_and_move(rye_project, mocker, binary_extensions):
"""Ensure cargo is called correctly and final binary moved to the right folder."""
Expand Down

0 comments on commit 849c7d1

Please sign in to comment.