pypilot-cli 1.0.4__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: py${{ matrix.python }} (${{ matrix.os }})
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest]
16
+ python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
17
+ include:
18
+ # The oldest and newest supported Pythons on the other platforms the
19
+ # engine ships for, since archive handling differs between them.
20
+ - { os: windows-latest, python: "3.8" }
21
+ - { os: windows-latest, python: "3.14" }
22
+ - { os: macos-latest, python: "3.8" }
23
+ - { os: macos-latest, python: "3.14" }
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+
30
+ # Warnings are errors so a deprecation that a new Python release turns
31
+ # into a behaviour change (as 3.14 did for tarfile) fails here first.
32
+ - name: Test
33
+ run: uv run --python ${{ matrix.python }} python -m pytest -W error
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pypilot-cli
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Deterministic Python environment doctor, dependency solver, and hardware compatibility manager.
5
5
  Project-URL: Homepage, https://github.com/Abdullah-Masood-05/pypilot
6
6
  Project-URL: Repository, https://github.com/Abdullah-Masood-05/pypilot
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
31
31
  [![PyPI version](https://img.shields.io/pypi/v/pypilot-cli?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/pypilot-cli/)
32
32
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
33
33
  [![Rust](https://img.shields.io/badge/Engine-Rust-orange.svg?logo=rust&logoColor=white)](https://github.com/Abdullah-Masood-05/pypilot)
34
- [![Python](https://img.shields.io/badge/Python-3.8+-green.svg?logo=python&logoColor=white)](https://python.org)
34
+ [![Python](https://img.shields.io/badge/Python-3.8%20to%203.14-green.svg?logo=python&logoColor=white)](https://python.org)
35
35
 
36
36
  **PyPilot** is a deterministic Python environment doctor, dependency solver, and toolchain manager. Powered by a high-speed compiled **Rust engine**, PyPilot calculates dependency intersections directly from PyPI wheel metadata, resolves hardware and CUDA build compatibility, and sets up working virtual environments without external AI or API keys.
37
37
 
@@ -3,7 +3,7 @@
3
3
  [![PyPI version](https://img.shields.io/pypi/v/pypilot-cli?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/pypilot-cli/)
4
4
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
5
5
  [![Rust](https://img.shields.io/badge/Engine-Rust-orange.svg?logo=rust&logoColor=white)](https://github.com/Abdullah-Masood-05/pypilot)
6
- [![Python](https://img.shields.io/badge/Python-3.8+-green.svg?logo=python&logoColor=white)](https://python.org)
6
+ [![Python](https://img.shields.io/badge/Python-3.8%20to%203.14-green.svg?logo=python&logoColor=white)](https://python.org)
7
7
 
8
8
  **PyPilot** is a deterministic Python environment doctor, dependency solver, and toolchain manager. Powered by a high-speed compiled **Rust engine**, PyPilot calculates dependency intersections directly from PyPI wheel metadata, resolves hardware and CUDA build compatibility, and sets up working virtual environments without external AI or API keys.
9
9
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pypilot-cli"
7
- version = "1.0.4"
7
+ version = "1.0.6"
8
8
  description = "Deterministic Python environment doctor, dependency solver, and hardware compatibility manager."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -10,7 +10,7 @@ from typing import List, Optional
10
10
 
11
11
  from .binary import ensure_binary, get_cache_dir
12
12
 
13
- __version__ = "1.0.4"
13
+ __version__ = "1.0.6"
14
14
 
15
15
 
16
16
  def run(args: Optional[List[str]] = None, capture_output: bool = False) -> subprocess.CompletedProcess[str]:
@@ -17,7 +17,7 @@ import zipfile
17
17
  from pathlib import Path
18
18
 
19
19
  GITHUB_REPO = "Abdullah-Masood-05/pypilot"
20
- DEFAULT_TAG = "v1.0.4"
20
+ DEFAULT_TAG = "v1.0.5"
21
21
  # Oldest helper accepted from ~/.cargo/bin or PATH.
22
22
  MIN_VERSION = (1, 0, 2)
23
23
  # The release this package ships with. A cached download older than this is
@@ -87,7 +87,10 @@ def probe_binary(path: Path | str) -> tuple[int, int, int] | None:
87
87
  [str(path), "--version"],
88
88
  capture_output=True,
89
89
  text=True,
90
- timeout=5,
90
+ # macOS scans a freshly downloaded binary on its first launch, which
91
+ # can take several seconds. Timing out here made a good engine look
92
+ # broken and triggered a needless re-download.
93
+ timeout=30,
91
94
  check=False,
92
95
  )
93
96
  if res.returncode == 0:
@@ -97,6 +100,23 @@ def probe_binary(path: Path | str) -> tuple[int, int, int] | None:
97
100
  return None
98
101
 
99
102
 
103
+ def extract_archive(archive: Path | str, dest: Path | str) -> None:
104
+ """Extract a downloaded `.zip` or `.tar.gz` release archive into `dest`."""
105
+ if str(archive).endswith(".zip"):
106
+ with zipfile.ZipFile(archive, "r") as zf:
107
+ zf.extractall(dest)
108
+ return
109
+ with tarfile.open(archive, "r:gz") as tf:
110
+ # Python 3.14 filters tar members by default and 3.12/3.13 warn that it
111
+ # is coming. Ask for the same behaviour everywhere it is available (it
112
+ # was backported to the late 3.8-3.11 releases): plain files only, and
113
+ # nothing may land outside `dest`.
114
+ if hasattr(tarfile, "data_filter"):
115
+ tf.extractall(dest, filter="data")
116
+ else:
117
+ tf.extractall(dest)
118
+
119
+
100
120
  def download_binary(tag: str = DEFAULT_TAG) -> Path:
101
121
  """Download prebuilt native binary from GitHub releases."""
102
122
  slug, ext = get_platform_slug()
@@ -115,12 +135,7 @@ def download_binary(tag: str = DEFAULT_TAG) -> Path:
115
135
  with urllib.request.urlopen(req) as resp, open(tmp_archive, "wb") as f:
116
136
  shutil.copyfileobj(resp, f)
117
137
 
118
- if ext == "zip":
119
- with zipfile.ZipFile(tmp_archive, "r") as zf:
120
- zf.extractall(tmpdir)
121
- else:
122
- with tarfile.open(tmp_archive, "r:gz") as tf:
123
- tf.extractall(tmpdir)
138
+ extract_archive(tmp_archive, tmpdir)
124
139
 
125
140
  extracted_bin = Path(tmpdir) / bin_name
126
141
  if not extracted_bin.exists():
@@ -131,14 +146,37 @@ def download_binary(tag: str = DEFAULT_TAG) -> Path:
131
146
  else:
132
147
  raise FileNotFoundError(f"{bin_name} not found in downloaded archive {archive_name}")
133
148
 
134
- shutil.copy2(extracted_bin, dest_binary)
135
-
136
- if sys.platform != "win32":
137
- dest_binary.chmod(dest_binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
149
+ install_binary(extracted_bin, dest_binary)
138
150
 
139
151
  return dest_binary
140
152
 
141
153
 
154
+ def install_binary(src: Path, dest: Path) -> None:
155
+ """Put `src` at `dest` as a new file, never by rewriting `dest` in place.
156
+
157
+ macOS caches what it knows about an executable per file. Overwriting a
158
+ binary that was just run leaves the old cache entry pointing at new bytes,
159
+ and the next launch fails with "Exec format error". Copying to a temporary
160
+ name and renaming over `dest` gives it a fresh file instead.
161
+
162
+ Windows has no such cache, but its antivirus briefly holds a freshly
163
+ written .exe open, which makes an immediate rename fail with "Access is
164
+ denied". There, a plain copy is both sufficient and reliable.
165
+ """
166
+ if sys.platform == "win32":
167
+ shutil.copy2(src, dest)
168
+ return
169
+
170
+ tmp = dest.with_name(f".{dest.name}.{os.getpid()}.tmp")
171
+ try:
172
+ shutil.copy2(src, tmp)
173
+ tmp.chmod(tmp.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
174
+ os.replace(tmp, dest)
175
+ finally:
176
+ if tmp.exists():
177
+ tmp.unlink()
178
+
179
+
142
180
  def ensure_binary() -> Path:
143
181
  """
144
182
  Locates an existing binary on PATH or in cache, or downloads it from GitHub.
@@ -0,0 +1,112 @@
1
+ import subprocess
2
+ import pypilot
3
+ from pypilot.binary import ensure_binary, get_platform_slug
4
+
5
+
6
+ def test_platform_slug():
7
+ slug, ext = get_platform_slug()
8
+ assert slug in ("linux-x64", "darwin-x64", "darwin-arm64", "windows-x64")
9
+ assert ext in ("tar.gz", "zip")
10
+
11
+
12
+ def test_ensure_binary():
13
+ bin_path = ensure_binary()
14
+ assert bin_path.exists()
15
+
16
+
17
+ def test_pypilot_version():
18
+ res = pypilot.run(["--version"], capture_output=True)
19
+ assert res.returncode == 0
20
+ assert "pypilot" in res.stdout
21
+
22
+
23
+ def test_bundled_version_matches_default_tag():
24
+ from pypilot.binary import BUNDLED_VERSION, DEFAULT_TAG, MIN_VERSION
25
+
26
+ assert DEFAULT_TAG == "v" + ".".join(map(str, BUNDLED_VERSION))
27
+ assert BUNDLED_VERSION >= MIN_VERSION
28
+
29
+
30
+ def test_extract_archive_reads_tar_gz_without_warnings(tmp_path):
31
+ import io
32
+ import tarfile
33
+ import warnings
34
+
35
+ from pypilot.binary import extract_archive
36
+
37
+ archive = tmp_path / "pypilot-linux-x64.tar.gz"
38
+ payload = b"#!/bin/sh\n"
39
+ with tarfile.open(archive, "w:gz") as tf:
40
+ info = tarfile.TarInfo("pypilot")
41
+ info.size = len(payload)
42
+ info.mode = 0o755
43
+ tf.addfile(info, io.BytesIO(payload))
44
+
45
+ out = tmp_path / "out"
46
+ out.mkdir()
47
+ with warnings.catch_warnings():
48
+ warnings.simplefilter("error") # 3.12/3.13 warn about the 3.14 default
49
+ extract_archive(archive, out)
50
+ assert (out / "pypilot").read_bytes() == payload
51
+
52
+
53
+ def test_extract_archive_reads_zip(tmp_path):
54
+ import zipfile
55
+
56
+ from pypilot.binary import extract_archive
57
+
58
+ archive = tmp_path / "pypilot-windows-x64.zip"
59
+ with zipfile.ZipFile(archive, "w") as zf:
60
+ zf.writestr("pypilot.exe", b"MZ")
61
+
62
+ out = tmp_path / "out"
63
+ out.mkdir()
64
+ extract_archive(archive, out)
65
+ assert (out / "pypilot.exe").read_bytes() == b"MZ"
66
+
67
+
68
+ def test_extract_archive_refuses_members_outside_the_destination(tmp_path):
69
+ import io
70
+ import tarfile
71
+
72
+ import pytest
73
+
74
+ from pypilot.binary import extract_archive
75
+
76
+ if not hasattr(tarfile, "data_filter"):
77
+ pytest.skip("this Python predates tarfile extraction filters")
78
+
79
+ archive = tmp_path / "evil.tar.gz"
80
+ with tarfile.open(archive, "w:gz") as tf:
81
+ info = tarfile.TarInfo("../escaped")
82
+ info.size = 1
83
+ tf.addfile(info, io.BytesIO(b"x"))
84
+
85
+ out = tmp_path / "out"
86
+ out.mkdir()
87
+ with pytest.raises(tarfile.TarError):
88
+ extract_archive(archive, out)
89
+ assert not (tmp_path / "escaped").exists()
90
+
91
+
92
+ def test_install_binary_replaces_instead_of_rewriting_in_place(tmp_path):
93
+ import os
94
+ import sys
95
+
96
+ from pypilot.binary import install_binary
97
+
98
+ dest = tmp_path / "pypilot"
99
+ dest.write_bytes(b"old engine")
100
+ old_inode = os.stat(dest).st_ino
101
+
102
+ src = tmp_path / "new"
103
+ src.write_bytes(b"new engine")
104
+ install_binary(src, dest)
105
+
106
+ assert dest.read_bytes() == b"new engine"
107
+ # A rename gives `dest` a new file; rewriting in place would keep the inode.
108
+ if sys.platform != "win32":
109
+ assert os.stat(dest).st_ino != old_inode
110
+ assert os.access(dest, os.X_OK)
111
+ # No temporary file is left behind.
112
+ assert sorted(p.name for p in tmp_path.iterdir()) == ["new", "pypilot"]
@@ -1157,7 +1157,7 @@ wheels = [
1157
1157
 
1158
1158
  [[package]]
1159
1159
  name = "pypilot-cli"
1160
- version = "1.0.4"
1160
+ version = "1.0.6"
1161
1161
  source = { editable = "." }
1162
1162
 
1163
1163
  [package.dev-dependencies]
@@ -1,27 +0,0 @@
1
- import subprocess
2
- import pypilot
3
- from pypilot.binary import ensure_binary, get_platform_slug
4
-
5
-
6
- def test_platform_slug():
7
- slug, ext = get_platform_slug()
8
- assert slug in ("linux-x64", "darwin-x64", "darwin-arm64", "windows-x64")
9
- assert ext in ("tar.gz", "zip")
10
-
11
-
12
- def test_ensure_binary():
13
- bin_path = ensure_binary()
14
- assert bin_path.exists()
15
-
16
-
17
- def test_pypilot_version():
18
- res = pypilot.run(["--version"], capture_output=True)
19
- assert res.returncode == 0
20
- assert "pypilot" in res.stdout
21
-
22
-
23
- def test_bundled_version_matches_default_tag():
24
- from pypilot.binary import BUNDLED_VERSION, DEFAULT_TAG, MIN_VERSION
25
-
26
- assert DEFAULT_TAG == "v" + ".".join(map(str, BUNDLED_VERSION))
27
- assert BUNDLED_VERSION >= MIN_VERSION
File without changes
File without changes
File without changes