Skip to content

earth-mover/spare-tire

Repository files navigation

🛞 spare-tire

A tool to rename Python wheel packages for multi-version installation.

Use Case

When you need to install multiple versions of the same Python package in a single environment (e.g., for regression testing), you can use this tool to rename one version's wheel so both can coexist:

# In your test code:
import icechunk_v1  # The v1 version
import icechunk     # The v2 version

# Test that v2 can read data written by v1

Installation

# Use directly with uvx (recommended)
uvx spare-tire --help

# Or install globally
pip install spare-tire

End-to-End Example: icechunk v1 + v2

Here's a complete example of setting up both icechunk versions for regression testing:

# 1. Download and rename v1 in one command (specify target Python version for uvx)
uvx spare-tire download icechunk \
    -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
    --version "<2" \
    --rename icechunk_v1 \
    --python-version 3.12 \
    -o ./wheels/

# 2. Download v2 wheel from nightly builds
uvx spare-tire download icechunk \
    -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
    --version ">=2.0.0.dev0" \
    --python-version 3.12 \
    -o ./wheels/

# 3. Create a venv and install both versions
uv venv
uv pip install ./wheels/icechunk_v1-*.whl  # v1 as icechunk_v1
uv pip install ./wheels/icechunk-2*.whl    # v2 as icechunk

# 4. Verify both work
uv run python -c "import icechunk_v1; print(f'v1: {icechunk_v1.__version__}')"
uv run python -c "import icechunk; print(f'v2: {icechunk.__version__}')"

Optional: Inspect a wheel before renaming to verify it uses underscore-prefix extensions:

uvx spare-tire inspect ./wheels/icechunk-*.whl

Commands

🛞 rename

Rename a wheel package:

spare-tire rename <wheel_path> <new_name> [-o <output_dir>]

# Examples:
spare-tire rename icechunk-1.0.0-cp312-cp312-linux_x86_64.whl icechunk_v1
spare-tire rename ./downloads/pkg.whl my_pkg_old -o ./renamed/

Options:

  • -o, --output: Output directory (default: same as input)
  • --no-update-imports: Don't update import statements in Python files

🛞 download

Download a compatible wheel from a package index:

spare-tire download <package> [-o <output_dir>] [-i <index_url>] [--version <spec>] [--rename <new_name>]

# Examples:
spare-tire download numpy -o ./wheels/
spare-tire download icechunk -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
spare-tire download requests --version ">=2.0,<3"
spare-tire download icechunk --version "<2" -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple

# Download and rename in one command:
spare-tire download icechunk --version "<2" --rename icechunk_v1 -o ./wheels/ \
    -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple

Options:

  • -o, --output: Output directory (default: current directory)
  • -i, --index-url: Package index URL (default: PyPI)
  • --version: PEP 440 version specifier (e.g., ==1.0.0, <2, >=1.0,<2)
  • --list: List available wheels without downloading
  • --rename: Rename the downloaded wheel to this package name (combines download + rename)
  • --python-version: Target Python version (e.g., 3.12). Useful with uvx to download wheels for a different Python than the one running spare-tire.

🔧 inspect

Inspect a wheel's structure before renaming:

spare-tire inspect <wheel_path> [--json]

# Example output:
# Wheel: icechunk-1.1.14-cp312-cp312-macosx_11_0_arm64.whl
# Distribution: icechunk
# Version: 1.1.14
#
# Compiled extensions (1):
#   - icechunk/_icechunk_python.cpython-312-darwin.so (underscore prefix - renamable)
#
# This wheel uses underscore-prefix extensions.
# Renaming should work correctly.

🛞 serve

Start a PEP 503 proxy server that renames packages on-the-fly:

# Install with server extras
pip install spare-tire[server]

# Start proxy with CLI options
spare-tire serve \
    -u https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
    -r "icechunk=icechunk_v1:<2" \
    --port 8000

# Or use a config file
spare-tire serve -c proxy.toml

Options:

  • -c, --config: Path to TOML config file
  • -u, --upstream: Upstream index URL (can be specified multiple times)
  • -r, --rename: Rename rule in format original=new_name[:version_spec]
  • --host: Host to bind to (default: 127.0.0.1)
  • --port: Port to listen on (default: 8000)

Config file format (proxy.toml):

[proxy]
host = "127.0.0.1"
port = 8000

[[proxy.upstreams]]
url = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"

[renames]
icechunk = { name = "icechunk_v1", version = "<2" }

Using with uv:

# Start the proxy
spare-tire serve -u https://pypi.org/simple/ -r "requests=requests_old:<2"

# In another terminal, install from the proxy
uv pip install requests_old --index-url http://127.0.0.1:8000/simple/

The proxy:

  1. Lists virtual packages (renamed packages) at /simple/
  2. Fetches the original package from upstream when requested
  3. Filters by version constraint if specified
  4. Renames the wheel on-the-fly during download
  5. Serves the renamed wheel to the client

🔧 How It Works

  1. Extracts the wheel (which is a ZIP file)
  2. Renames the package directory (pkg/pkg_v1/)
  3. Renames the .dist-info directory
  4. Updates METADATA with the new package name
  5. Updates imports in all Python files (from pkg importfrom pkg_v1 import)
  6. Regenerates RECORD with new file paths and SHA256 hashes
  7. Repacks as a new wheel with the renamed filename

🔧 Compiled Extensions

For wheels with compiled extensions (.so/.pyd files), renaming works only if the extension uses an underscore-prefix naming pattern:

Pattern Example Renamable?
_modulename.cpython-*.so _icechunk_python.cpython-312-darwin.so Yes
modulename.cpython-*.so icechunk.cpython-312-darwin.so No

Why underscore prefix matters

Python's import system requires the PyInit_<name> function inside the .so file to match the filename. When you have _mymodule.cpython-*.so:

  • Python looks for PyInit__mymodule (matches!)
  • The parent package directory can be renamed freely
  • from newpkg._mymodule import ... works because the .so name is unchanged

If the extension doesn't use the underscore prefix pattern, the tool will warn you and you should rebuild from source instead.

Limitations

  • Compiled extensions without underscore prefix: Cannot be renamed without rebuilding
  • Hardcoded package names in strings: Not automatically updated (only import statements are)
  • Entry points: Updated in metadata but external scripts may need adjustment

Development

# Clone and setup
git clone <repo>
cd spare-tire
uv sync --all-extras

# Run tests
uv run pytest

# Lint and format
uv run ruff check src tests
uv run ruff format src tests

License

BSD-3-Clause

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages