Skip to content

Commit

Permalink
Incorporate two special actions: info and uninstall.
Browse files Browse the repository at this point in the history
  • Loading branch information
facundobatista committed Dec 31, 2023
1 parent e3442ed commit 103b659
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 3 deletions.
53 changes: 53 additions & 0 deletions pyempaq/unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
# so it's easily patchable by tests
MAGIC_NUMBER = importlib.util.MAGIC_NUMBER[:-2].hex()

# the environment variable to specify a different action
ACTION_ENVVAR = "PYEMPAQ_ACTION"

# setup logging
logger = logging.getLogger()
handler = logging.StreamHandler()
Expand All @@ -58,12 +61,49 @@ class ReturnCode(enum.IntEnum):
restrictions_not_met = 64
unpack_basedir_missing = 65
unpack_basedir_notdir = 66
bad_action = 67

def __init__(self, code):
self.returncode = code
super().__init__("")


# -- special PyEmpaq actions

def special_action_info(pyempaq_dir, metadata):
"""Provide information about different installations for the project."""
print("Base PyEmpaq directory:", pyempaq_dir)
subdirs = sorted(pyempaq_dir.glob(f"{metadata['project_name']}-*"))
if not subdirs:
print("No installation found!")
return

print("Current installations:")
for subdir in subdirs:
print(" ", subdir.name)


def special_action_uninstall(pyempaq_dir, metadata):
"""Remove all installations for the given project."""
subdirs = list(pyempaq_dir.glob(f"{metadata['project_name']}-*"))
if not subdirs:
print("No installation found!")
return

print("Removing installation:")
for subdir in subdirs:
print(" ", subdir.name)
shutil.rmtree(subdir)


ACTION_CHOICES = {
"info": special_action_info,
"uninstall": special_action_uninstall,
}

# --


def get_python_exec(project_dir: pathlib.Path) -> pathlib.Path:
"""Return the Python exec to use.
Expand Down Expand Up @@ -265,6 +305,19 @@ def run():
pyempaq_dir = get_base_dir(platformdirs)
logger.info("Base directory: %r", str(pyempaq_dir))

indicated_action = os.environ.get(ACTION_ENVVAR)
if indicated_action is not None:
indicated_action = indicated_action.lower()
print(f"Running {indicated_action!r} action")
try:
func = ACTION_CHOICES[indicated_action]
except KeyError:
logger.error(
"Bad specified action %s=%r; valid options: %s",
ACTION_ENVVAR, indicated_action, ", ".join(ACTION_CHOICES.keys()))
raise FatalError(FatalError.ReturnCode.bad_action)
return func(pyempaq_dir, metadata)

# create a temp dir and extract the project there
project_dir = pyempaq_dir / build_project_install_dir(pyempaq_filepath, metadata)
original_project_dir = project_dir / "orig"
Expand Down
78 changes: 75 additions & 3 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
"""Integration tests."""

import os
import shutil
import subprocess
import sys
import textwrap

import pytest
import yaml

from pyempaq.unpacker import FatalError
from pyempaq.unpacker import FatalError, ACTION_ENVVAR, get_file_hexdigest


def _pack(tmp_path, monkeypatch, config_text):
Expand Down Expand Up @@ -40,8 +41,9 @@ def _unpack(packed_filepath, basedir, *, extra_env=None, expected_rc=None):
"""
# run the packed file in a clean directory
cleandir = basedir / "cleandir"
cleandir.mkdir()
new_path = packed_filepath.rename(cleandir / "testproject.pyz")
cleandir.mkdir(exist_ok=True)
new_path = cleandir / "testproject.pyz"
shutil.copy(packed_filepath, new_path)
os.chdir(cleandir)

# set the install basedir so the user real one is not used, and then any extra
Expand Down Expand Up @@ -276,3 +278,73 @@ def test_using_entrypoint(tmp_path, monkeypatch):

assert proc.returncode == 0
assert proc.stdout.strip() == "crazyarg"


# -- check special actions


def test_action_info(tmp_path, monkeypatch):
"""Use the special action 'info'."""
# set up a basic test project, with an entrypoint that shows access to internals
projectpath = tmp_path / "fakeproject"
entrypoint = projectpath / "ep.py"
entrypoint.parent.mkdir()
entrypoint.write_text("print(42)")

packed_filepath = _pack(tmp_path, monkeypatch, f"""
name: testproject
basedir: {projectpath}
exec:
script: ep.py
""")

# unpack it once so it's already installed
proc, run_path = _unpack(packed_filepath, tmp_path, expected_rc=0)
assert proc.returncode == 0

# run the action
extra_env = {ACTION_ENVVAR: "info"}
proc, run_path = _unpack(packed_filepath, tmp_path, expected_rc=0, extra_env=extra_env)
assert proc.returncode == 0

hexdigest = get_file_hexdigest(packed_filepath)
assert proc.stdout == textwrap.dedent(f"""\
Running 'info' action
Base PyEmpaq directory: {tmp_path}
Current installations:
testproject-{hexdigest[:20]}-cpython.3.10.6f0d
""")


def test_action_uninstall(tmp_path, monkeypatch):
"""Use the special action 'uninstall'."""
# set up a basic test project, with an entrypoint that shows access to internals
projectpath = tmp_path / "fakeproject"
entrypoint = projectpath / "ep.py"
entrypoint.parent.mkdir()
entrypoint.write_text("print(42)")

packed_filepath = _pack(tmp_path, monkeypatch, f"""
name: testproject
basedir: {projectpath}
exec:
script: ep.py
""")

# unpack it once so it's already installed
proc, run_path = _unpack(packed_filepath, tmp_path, expected_rc=0)
assert proc.returncode == 0
assert len(list(tmp_path.glob("testproject-*"))) == 1

# run the action
extra_env = {ACTION_ENVVAR: "uninstall"}
proc, run_path = _unpack(packed_filepath, tmp_path, expected_rc=0, extra_env=extra_env)
assert proc.returncode == 0
assert len(list(tmp_path.glob("testproject-*"))) == 0

hexdigest = get_file_hexdigest(packed_filepath)
assert proc.stdout == textwrap.dedent(f"""\
Running 'uninstall' action
Removing installation:
testproject-{hexdigest[:20]}-cpython.3.10.6f0d
""")
60 changes: 60 additions & 0 deletions tests/test_unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import json
import os
import platform
import textwrap
import time
import zipfile
from pathlib import Path
Expand All @@ -28,6 +29,8 @@
get_base_dir,
run_command,
setup_project_directory,
special_action_info,
special_action_uninstall,
)


Expand Down Expand Up @@ -399,3 +402,60 @@ def test_installdirname_custombase_not_dir(monkeypatch, tmp_path):
with pytest.raises(FatalError) as cm:
get_base_dir(platformdirs)
assert cm.value.returncode is FatalError.ReturnCode.unpack_basedir_notdir


# --- tests for the special actions


def test_specialaction_info_simple(tmp_path, capsys):
"""A couple of installs to show."""
(tmp_path / "testproj-whatever-123").mkdir()
(tmp_path / "testproj-another one").mkdir()
special_action_info(tmp_path, {"project_name": "testproj"})

out, _ = capsys.readouterr()
assert out == textwrap.dedent(f"""\
Base PyEmpaq directory: {tmp_path}
Current installations:
testproj-another one
testproj-whatever-123
""")


def test_specialaction_info_nothing(tmp_path, capsys):
"""No install to show information."""
special_action_info(tmp_path, {"project_name": "testproj"})

out, _ = capsys.readouterr()
assert out == textwrap.dedent(f"""\
Base PyEmpaq directory: {tmp_path}
No installation found!
""")


def test_specialaction_uninstall_simple(tmp_path, capsys):
"""Some installs to remove."""
inst1 = tmp_path / "testproj-whatever-123"
inst1.mkdir()
inst2 = tmp_path / "testproj-another one"
inst2.mkdir()
special_action_uninstall(tmp_path, {"project_name": "testproj"})

out, _ = capsys.readouterr()
assert out == textwrap.dedent("""\
Removing installation:
testproj-another one
testproj-whatever-123
""")
assert not inst1.exists()
assert not inst2.exists()


def test_specialaction_uninstall_nothing(tmp_path, capsys):
"""No install to remove."""
special_action_uninstall(tmp_path, {"project_name": "testproj"})

out, _ = capsys.readouterr()
assert out == textwrap.dedent("""\
No installation found!
""")

0 comments on commit 103b659

Please sign in to comment.