Implement sjnetpkg 0.1.0, a Git-based package manager

Packages are ordinary Git repositories carrying a sjnetpkg.toml, so sjnetpkg
only needs to resolve, clone, track and run them instead of inventing an
archive format or build system. This adds the v0.1 CLI (install from registry
or Git URL, remove, list/inspect, registry update, run) on top of the Python
standard library, together with a test suite.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
2026-09-24 16:51:29 +02:00
parent 71343f7886
commit f1250881f4
16 changed files with 1515 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
"""Test bootstrap: make the ``src`` layout importable.
This is used by pytest. When running with the standard library test runner,
pass the source tree explicitly, e.g.::
PYTHONPATH=src python3 -m unittest discover -s tests -t tests
"""
from __future__ import annotations
import sys
from pathlib import Path
_SRC = Path(__file__).resolve().parents[1] / "src"
if str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
+265
View File
@@ -0,0 +1,265 @@
from __future__ import annotations
import contextlib
import io
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from sjnetpkg import cli, config
GIT_AVAILABLE = shutil.which("git") is not None
def run_cli(*args: str) -> tuple[int, str, str]:
"""Invoke the CLI, capturing stdout/stderr and the exit code."""
stdout, stderr = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
code = cli.main(list(args))
return code, stdout.getvalue(), stderr.getvalue()
def write_package(root: Path, directory: str, manifest: str, files: dict[str, str] | None = None) -> Path:
target = root / directory
target.mkdir(parents=True, exist_ok=True)
(target / "sjnetpkg.toml").write_text(manifest, encoding="utf-8")
for name, content in (files or {}).items():
(target / name).write_text(content, encoding="utf-8")
return target
def make_git_repo(path: Path, manifest: str, files: dict[str, str] | None = None) -> Path:
write_package(path.parent, path.name, manifest, files)
subprocess.run(["git", "init", "-q", str(path)], check=True)
subprocess.run(["git", "-C", str(path), "add", "-A"], check=True)
subprocess.run(
[
"git", "-C", str(path),
"-c", "user.email=test@example.com",
"-c", "user.name=Test",
"commit", "-q", "-m", "init",
],
check=True,
)
return path
class CliTestCase(unittest.TestCase):
"""Base class that isolates the sjnetpkg home directory."""
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
self.home = self.root / ".sjnetpkg"
patcher = mock.patch.dict(os.environ, {"SJNETPKG_HOME": str(self.home)})
patcher.start()
self.addCleanup(patcher.stop)
self.paths = config.get_paths()
def tearDown(self) -> None:
self._tmp.cleanup()
def install_fixture(self, name: str = "cool-tool", manifest: str | None = None,
files: dict[str, str] | None = None) -> Path:
manifest = manifest or f'name = "{name}"\nversion = "1.0.0"\n'
repo = make_git_repo(self.root / "repos" / name, manifest, files)
code, _, err = run_cli("-U", repo.as_uri())
self.assertEqual(code, 0, err)
return repo
class VersionAndHelpTests(CliTestCase):
def test_version(self) -> None:
code, out, _ = run_cli("--version")
self.assertEqual(code, 0)
self.assertEqual(out.strip(), "sjnetpkg 0.1.0")
def test_help(self) -> None:
code, out, _ = run_cli("--help")
self.assertEqual(code, 0)
self.assertIn("Usage:", out)
for line in ("sjnetpkg -S <package>", "sjnetpkg -Sy", "sjnetpkg run <package>"):
self.assertIn(line, out)
def test_no_arguments_is_a_usage_error(self) -> None:
code, _, err = run_cli()
self.assertEqual(code, 2)
self.assertIn("Usage:", err)
def test_unknown_command(self) -> None:
code, _, err = run_cli("frobnicate")
self.assertEqual(code, 2)
self.assertIn("unknown command", err)
class QueryTests(CliTestCase):
def test_list_without_packages(self) -> None:
code, out, _ = run_cli("-Q")
self.assertEqual(code, 0)
self.assertIn("No packages installed.", out)
def test_list_alignment(self) -> None:
config.ensure_home(self.paths)
write_package(self.paths.pkg, "cool-tool", 'name = "cool-tool"\nversion = "1.0.0"\n')
write_package(self.paths.pkg, "another-tool", 'name = "another-tool"\nversion = "2.4.1"\n')
code, out, _ = run_cli("-Q")
self.assertEqual(code, 0)
lines = out.strip().splitlines()
self.assertEqual(len(lines), 2)
self.assertRegex(lines[0], r"^another-tool\s+2\.4\.1$")
self.assertRegex(lines[1], r"^cool-tool\s+1\.0\.0$")
# The versions are printed in the same column.
self.assertEqual(lines[0].index("2.4.1"), lines[1].index("1.0.0"))
def test_info_for_unknown_package(self) -> None:
code, _, err = run_cli("-Q", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class InstallTests(CliTestCase):
def test_install_from_url(self) -> None:
repo = self.install_fixture()
target = self.home / "pkg" / "cool-tool"
self.assertTrue((target / ".git").is_dir())
self.assertTrue((target / "sjnetpkg.toml").is_file())
def test_install_reports_package(self) -> None:
repo = make_git_repo(
self.root / "repos" / "cool-tool",
'name = "cool-tool"\nversion = "1.0.0"\n',
)
code, out, _ = run_cli("-U", repo.as_uri())
self.assertEqual(code, 0)
self.assertIn("Installed cool-tool 1.0.0", out)
def test_second_install_is_rejected(self) -> None:
repo = self.install_fixture()
code, _, err = run_cli("-U", repo.as_uri())
self.assertEqual(code, 1)
self.assertIn("package 'cool-tool' is already installed.", err)
def test_invalid_url_is_rejected(self) -> None:
code, _, err = run_cli("-U", "not-a-url")
self.assertEqual(code, 1)
self.assertIn("invalid git URL", err)
def test_missing_manifest_does_not_leave_a_directory(self) -> None:
repo_dir = self.root / "repos" / "empty-tool"
repo_dir.mkdir(parents=True)
(repo_dir / "README.md").write_text("no manifest here")
subprocess.run(["git", "init", "-q", str(repo_dir)], check=True)
subprocess.run(["git", "-C", str(repo_dir), "add", "-A"], check=True)
subprocess.run(
["git", "-C", str(repo_dir), "-c", "user.email=t@e", "-c", "user.name=T",
"commit", "-q", "-m", "init"],
check=True,
)
code, _, err = run_cli("-U", repo_dir.as_uri())
self.assertEqual(code, 1)
self.assertIn("sjnetpkg.toml", err)
self.assertFalse((self.home / "pkg" / "empty-tool").exists())
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class RegistryInstallTests(CliTestCase):
def _write_registry(self, url: str, name: str = "cool-tool") -> None:
config.ensure_home(self.paths)
self.paths.registry.write_text(
f'[packages.{name}]\nrepo = "{url}"\n', encoding="utf-8"
)
def test_install_from_registry(self) -> None:
repo = make_git_repo(
self.root / "repos" / "cool-tool",
'name = "cool-tool"\nversion = "1.0.0"\n',
)
self._write_registry(repo.as_uri())
code, out, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 0, err)
self.assertIn("Installed cool-tool 1.0.0", out)
def test_unknown_package(self) -> None:
self._write_registry("https://example.com/other.git", name="other")
code, _, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("package 'cool-tool' was not found.", err)
def test_missing_local_registry(self) -> None:
code, _, err = run_cli("-S", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("no local registry found", err)
def test_update_registry(self) -> None:
source = self.root / "remote-registry.toml"
source.write_text('[packages.cool-tool]\nrepo = "https://example.com/cool-tool.git"\n')
config.ensure_home(self.paths)
self.paths.config.write_text(f'registry = "{source.as_uri()}"\n', encoding="utf-8")
code, out, _ = run_cli("-Sy")
self.assertEqual(code, 0)
self.assertIn("Registry updated", out)
self.assertTrue(self.paths.registry.is_file())
@unittest.skipUnless(GIT_AVAILABLE, "git is required for these tests")
class RunAndRemoveTests(CliTestCase):
def test_run_returns_process_exit_code(self) -> None:
manifest = (
'name = "cool-tool"\nversion = "1.0.0"\n\n'
f'[run]\ncommand = \'"{sys.executable}" main.py\'\n'
)
self.install_fixture(manifest=manifest, files={"main.py": "import sys\nsys.exit(7)\n"})
code, _, _ = run_cli("run", "cool-tool")
self.assertEqual(code, 7)
def test_run_uses_package_directory_as_working_directory(self) -> None:
manifest = (
'name = "cool-tool"\nversion = "1.0.0"\n\n'
f'[run]\ncommand = \'"{sys.executable}" check_cwd.py\'\n'
)
script = (
"import pathlib, sys\n"
"sys.exit(0 if pathlib.Path('sjnetpkg.toml').exists() else 3)\n"
)
self.install_fixture(manifest=manifest, files={"check_cwd.py": script})
code, _, _ = run_cli("run", "cool-tool")
self.assertEqual(code, 0)
def test_run_without_run_command(self) -> None:
self.install_fixture()
code, _, err = run_cli("run", "cool-tool")
self.assertEqual(code, 1)
self.assertIn("does not define a run command", err)
def test_run_unknown_package(self) -> None:
code, _, err = run_cli("run", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
def test_info_and_remove(self) -> None:
self.install_fixture()
code, out, _ = run_cli("-Q", "cool-tool")
self.assertEqual(code, 0)
self.assertIn("Name: cool-tool", out)
self.assertIn("Version: 1.0.0", out)
self.assertIn("Repository:", out)
code, out, _ = run_cli("-R", "cool-tool")
self.assertEqual(code, 0)
self.assertIn("Removed cool-tool", out)
self.assertFalse((self.home / "pkg" / "cool-tool").exists())
def test_remove_unknown_package(self) -> None:
code, _, err = run_cli("-R", "missing")
self.assertEqual(code, 1)
self.assertIn("package 'missing' is not installed.", err)
if __name__ == "__main__":
unittest.main()
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from sjnetpkg import config
class DefaultHomeTests(unittest.TestCase):
def test_env_override_is_used(self) -> None:
with mock.patch.dict(os.environ, {"SJNETPKG_HOME": "/tmp/custom-home"}):
self.assertEqual(config.default_home(), Path("/tmp/custom-home"))
def test_falls_back_to_home_directory(self) -> None:
env = {k: v for k, v in os.environ.items() if k != "SJNETPKG_HOME"}
with mock.patch.dict(os.environ, env, clear=True):
self.assertEqual(config.default_home(), Path.home() / ".sjnetpkg")
class PathsTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.home = Path(self._tmp.name) / ".sjnetpkg"
self.paths = config.get_paths(self.home)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_layout(self) -> None:
self.assertEqual(self.paths.config, self.home / "config.toml")
self.assertEqual(self.paths.registry, self.home / "registry.toml")
self.assertEqual(self.paths.pkg, self.home / "pkg")
self.assertEqual(self.paths.package("cool-tool"), self.home / "pkg" / "cool-tool")
def test_ensure_home_creates_directories_and_config(self) -> None:
config.ensure_home(self.paths)
self.assertTrue(self.paths.pkg.is_dir())
self.assertTrue(self.paths.config.is_file())
self.assertEqual(config.registry_url(self.paths), config.DEFAULT_REGISTRY_URL)
def test_ensure_home_keeps_existing_config(self) -> None:
self.home.mkdir(parents=True)
self.paths.config.write_text('registry = "https://example.org/r.toml"\n')
config.ensure_home(self.paths)
self.assertEqual(config.registry_url(self.paths), "https://example.org/r.toml")
def test_missing_config_uses_default_registry(self) -> None:
self.assertEqual(config.registry_url(self.paths), config.DEFAULT_REGISTRY_URL)
def test_invalid_config_raises(self) -> None:
self.home.mkdir(parents=True)
self.paths.config.write_text("this is = not toml\n")
with self.assertRaises(config.ConfigError):
config.registry_url(self.paths)
def test_empty_registry_value_raises(self) -> None:
self.home.mkdir(parents=True)
self.paths.config.write_text('registry = ""\n')
with self.assertRaises(config.ConfigError):
config.registry_url(self.paths)
if __name__ == "__main__":
unittest.main()
+140
View File
@@ -0,0 +1,140 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from sjnetpkg import config, package
def write_package(root: Path, directory: str, manifest: str) -> Path:
target = root / directory
target.mkdir(parents=True, exist_ok=True)
(target / "sjnetpkg.toml").write_text(manifest, encoding="utf-8")
return target
class ValidNameTests(unittest.TestCase):
def test_valid_names(self) -> None:
for name in ("cool-tool", "a", "tool_2", "tool.py", "A1"):
self.assertTrue(package.is_valid_name(name), name)
def test_invalid_names(self) -> None:
for name in ("", ".", "..", "-tool", "tool-", "bad name", "../escape", "a/b"):
self.assertFalse(package.is_valid_name(name), name)
class ManifestTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_reads_name_version_and_run_command(self) -> None:
directory = write_package(
self.root,
"cool-tool",
'name = "cool-tool"\nversion = "1.0.0"\n\n[run]\ncommand = "cargo run"\n',
)
manifest = package.read_manifest(directory)
self.assertEqual(manifest.name, "cool-tool")
self.assertEqual(manifest.version, "1.0.0")
self.assertEqual(manifest.run_command, "cargo run")
self.assertEqual(manifest.path, directory)
def test_run_section_is_optional(self) -> None:
directory = write_package(
self.root, "lib", 'name = "lib"\nversion = "0.1.0"\n'
)
self.assertIsNone(package.read_manifest(directory).run_command)
def test_missing_manifest_raises(self) -> None:
directory = self.root / "empty"
directory.mkdir()
with self.assertRaises(package.PackageError):
package.read_manifest(directory)
def test_invalid_toml_raises(self) -> None:
directory = write_package(self.root, "broken", "name = = \n")
with self.assertRaises(package.PackageError):
package.read_manifest(directory)
def test_missing_name_raises(self) -> None:
directory = write_package(self.root, "noname", 'version = "1.0.0"\n')
with self.assertRaises(package.PackageError):
package.read_manifest(directory)
def test_missing_version_raises(self) -> None:
directory = write_package(self.root, "noversion", 'name = "noversion"\n')
with self.assertRaises(package.PackageError):
package.read_manifest(directory)
def test_invalid_name_raises(self) -> None:
directory = write_package(
self.root, "bad", 'name = "../escape"\nversion = "1.0.0"\n'
)
with self.assertRaises(package.PackageError):
package.read_manifest(directory)
class InstalledPackagesTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.paths = config.get_paths(Path(self._tmp.name) / ".sjnetpkg")
self.paths.pkg.mkdir(parents=True)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_list_is_empty_without_packages_directory(self) -> None:
self.assertEqual(package.list_installed(config.get_paths(self.paths.home / "x")), [])
def test_list_ignores_invalid_and_non_directory_entries(self) -> None:
write_package(self.paths.pkg, "cool-tool", 'name = "cool-tool"\nversion = "1.0.0"\n')
write_package(self.paths.pkg, "broken", "nonsense")
(self.paths.pkg / "loose-file").write_text("hello")
names = [m.name for m in package.list_installed(self.paths)]
self.assertEqual(names, ["cool-tool"])
def test_find_installed_by_directory_name(self) -> None:
directory = write_package(
self.paths.pkg, "repo-name", 'name = "manifest-name"\nversion = "1.0.0"\n'
)
self.assertEqual(package.find_installed(self.paths, "repo-name"), directory)
def test_find_installed_by_manifest_name(self) -> None:
directory = write_package(
self.paths.pkg, "repo-name", 'name = "manifest-name"\nversion = "1.0.0"\n'
)
self.assertEqual(package.find_installed(self.paths, "manifest-name"), directory)
def test_find_installed_unknown_raises(self) -> None:
with self.assertRaises(package.NotInstalledError):
package.find_installed(self.paths, "missing")
def test_remove_package(self) -> None:
directory = write_package(self.paths.pkg, "cool-tool", 'name = "cool-tool"\nversion = "1.0.0"\n')
self.assertEqual(package.remove_package(self.paths, "cool-tool"), directory)
self.assertFalse(directory.exists())
def test_remove_missing_package_raises(self) -> None:
with self.assertRaises(package.NotInstalledError):
package.remove_package(self.paths, "missing")
def test_remove_refuses_to_leave_package_directory(self) -> None:
outside = Path(self._tmp.name) / "outside"
outside.mkdir()
link = self.paths.pkg / "link"
try:
link.symlink_to(outside, target_is_directory=True)
except (OSError, NotImplementedError): # pragma: no cover - platform dependent
self.skipTest("symlinks are not supported on this platform")
with self.assertRaises(package.PackageError):
package.remove_package(self.paths, "link")
self.assertTrue(outside.exists())
if __name__ == "__main__":
unittest.main()
+123
View File
@@ -0,0 +1,123 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from sjnetpkg import config, registry
class LoadRegistryTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.paths = config.get_paths(Path(self._tmp.name) / ".sjnetpkg")
self.paths.home.mkdir(parents=True)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_loads_packages(self) -> None:
self.paths.registry.write_text(
'[packages.cool-tool]\nrepo = "https://example.com/cool-tool.git"\n'
'[packages.other]\nrepo = "https://example.com/other.git"\n'
)
self.assertEqual(
registry.load_registry(self.paths),
{
"cool-tool": "https://example.com/cool-tool.git",
"other": "https://example.com/other.git",
},
)
def test_missing_registry_raises(self) -> None:
with self.assertRaises(registry.RegistryError):
registry.load_registry(self.paths)
def test_invalid_toml_raises(self) -> None:
self.paths.registry.write_text("not = = toml\n")
with self.assertRaises(registry.RegistryError):
registry.load_registry(self.paths)
def test_missing_packages_table_raises(self) -> None:
self.paths.registry.write_text('something = "else"\n')
with self.assertRaises(registry.RegistryError):
registry.load_registry(self.paths)
def test_entry_without_repo_raises(self) -> None:
self.paths.registry.write_text('[packages.cool-tool]\nname = "cool-tool"\n')
with self.assertRaises(registry.RegistryError):
registry.load_registry(self.paths)
def test_resolve_returns_repo(self) -> None:
self.assertEqual(
registry.resolve({"cool-tool": "https://example.com/cool-tool.git"}, "cool-tool"),
"https://example.com/cool-tool.git",
)
def test_resolve_unknown_raises(self) -> None:
with self.assertRaises(registry.PackageNotFoundError):
registry.resolve({}, "missing")
class DownloadRegistryTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
self.paths = config.get_paths(self.root / ".sjnetpkg")
self.paths.home.mkdir(parents=True)
def tearDown(self) -> None:
self._tmp.cleanup()
def _source(self, name: str, content: str) -> str:
path = self.root / name
path.write_text(content, encoding="utf-8")
return path.as_uri()
def test_download_replaces_local_registry(self) -> None:
url = self._source(
"remote.toml",
'[packages.cool-tool]\nrepo = "https://example.com/cool-tool.git"\n',
)
registry.download_registry(url, self.paths.registry)
self.assertEqual(
registry.load_registry(self.paths),
{"cool-tool": "https://example.com/cool-tool.git"},
)
def test_failed_download_keeps_existing_registry(self) -> None:
self.paths.registry.write_text('[packages.old]\nrepo = "https://example.com/old.git"\n')
missing = (self.root / "missing.toml").as_uri()
with self.assertRaises(registry.RegistryError):
registry.download_registry(missing, self.paths.registry)
self.assertEqual(
registry.load_registry(self.paths),
{"old": "https://example.com/old.git"},
)
def test_invalid_download_keeps_existing_registry(self) -> None:
self.paths.registry.write_text('[packages.old]\nrepo = "https://example.com/old.git"\n')
url = self._source("broken.toml", "this = = invalid\n")
with self.assertRaises(registry.RegistryError):
registry.download_registry(url, self.paths.registry)
self.assertEqual(
registry.load_registry(self.paths),
{"old": "https://example.com/old.git"},
)
def test_update_uses_configured_url(self) -> None:
url = self._source(
"remote.toml",
'[packages.cool-tool]\nrepo = "https://example.com/cool-tool.git"\n',
)
config.ensure_home(self.paths)
self.paths.config.write_text(f'registry = "{url}"\n', encoding="utf-8")
registry.update_registry(self.paths)
self.assertEqual(
registry.load_registry(self.paths),
{"cool-tool": "https://example.com/cool-tool.git"},
)
if __name__ == "__main__":
unittest.main()