From 849c7d1165d4cac0ff773ec2100b24d4f55ba506 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Wed, 28 Feb 2024 20:18:42 +0100 Subject: [PATCH] Optionally select local PyApp source (#19) * allow for local pyapp source * add test/fix for when local file does not exist --- requirements-dev.lock | 10 +-- requirements.lock | 4 +- src/box/cli.py | 13 +++- src/box/packager.py | 96 ++++++++++++++++++---------- tests/cli/test_cli_packager.py | 25 +++++++- tests/conftest.py | 6 ++ tests/data/__init__.py | 0 tests/data/pyapp-source.tar.gz | Bin 0 -> 223 bytes tests/data/pyapp-v0.14.0/source.txt | 1 + tests/func/test_packager.py | 23 +++++++ 10 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 tests/data/__init__.py create mode 100644 tests/data/pyapp-source.tar.gz create mode 100644 tests/data/pyapp-v0.14.0/source.txt diff --git a/requirements-dev.lock b/requirements-dev.lock index bc991c9..8f2ab8f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -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 @@ -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 @@ -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 diff --git a/requirements.lock b/requirements.lock index 76045ec..7d717b2 100644 --- a/requirements.lock +++ b/requirements.lock @@ -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 diff --git a/src/box/cli.py b/src/box/cli.py index 2664b05..ddde469 100644 --- a/src/box/cli.py +++ b/src/box/cli.py @@ -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, @@ -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" diff --git a/src/box/packager.py b/src/box/packager.py index 55c4aa1..442c02d 100644 --- a/src/box/packager.py +++ b/src/box/packager.py @@ -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 @@ -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") @@ -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.""" @@ -81,33 +85,55 @@ 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 = [] @@ -115,22 +141,24 @@ def _get_pyapp(self): 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: @@ -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.""" diff --git a/tests/cli/test_cli_packager.py b/tests/cli/test_cli_packager.py index b1ba320..8eca61c 100644 --- a/tests/cli/test_cli_packager.py +++ b/tests/cli/test_cli_packager.py @@ -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( @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 327dc6d..80d8e47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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.""" diff --git a/tests/data/__init__.py b/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/pyapp-source.tar.gz b/tests/data/pyapp-source.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..66b4ffa194c6cd34dfdeeb44b183a8a60070f6fd GIT binary patch literal 223 zcmb2|=3oE==C_w}vzQD8*dF}(CQ{6J{m9wlu6rD$7VR$ao^~cDI{&?wcE-V9?wUux zD;hNNq#T-ED1W^0=Q`AgYx3)2;MB%2<5`MGLQPpMtS+glg3a(lMBO{_j+Fv;b-0}U^15T