How It Works ============ This page explains the technical details of how rind works. PEP 517 Build Backend --------------------- rind is a `PEP 517 `_ build backend. When you run ``python -m build``, the build frontend: 1. Creates an isolated virtual environment 2. Installs the packages listed in ``build-system.requires`` 3. Imports the module specified in ``build-system.build-backend`` 4. Calls the appropriate hook functions (``build_wheel``, ``build_sdist``) The backend implements these hooks to generate packages without any Python code. Wheel Structure --------------- A typical Python wheel contains: .. code-block:: text mypackage-1.0.0-py3-none-any.whl ├── mypackage/ │ ├── __init__.py │ └── module.py └── mypackage-1.0.0.dist-info/ ├── METADATA ├── WHEEL └── RECORD A rind wheel contains **only the metadata**: .. code-block:: text mypackage-1.0.0-py3-none-any.whl └── mypackage-1.0.0.dist-info/ ├── METADATA ├── WHEEL └── RECORD This is perfectly valid according to the wheel specification. When pip installs this wheel, it: 1. Records that ``mypackage==1.0.0`` is installed 2. Installs all packages listed in ``Requires-Dist`` 3. Leaves no Python files (because there are none) Version Pinning --------------- rind automatically uses whatever versioning system your core package uses. It reads the core package's ``pyproject.toml`` and detects: - **Static versions**: If ``version`` is in ``[project]`` and not in ``dynamic``, it's used directly. - **setuptools_scm / hatch-vcs**: If detected in build requirements or tool config, rind calls setuptools_scm directly for fast version detection. - **Other backends**: As a fallback, rind calls the core's build backend via ``prepare_metadata_for_build_wheel`` to get the version. When you tag a release and build both packages: .. code-block:: bash $ git tag v1.2.3 $ python -m build . # In repo root $ python -m build meta/ # In meta/ directory Both builds use the same version source, so both get version ``1.2.3``. The metapackage's ``METADATA`` file contains: .. code-block:: text Requires-Dist: mypackage-core==1.2.3 This ensures that ``pip install mypackage==1.2.3`` always installs ``mypackage-core==1.2.3``. .. important:: Version pinning only works correctly when both packages are built from the same git commit. Always build and release them together. Metadata Inheritance -------------------- When ``inherit-metadata`` is true (the default), the backend: 1. Reads the core package's ``pyproject.toml`` (via ``core-path``) 2. Extracts the ``[project]`` table 3. Uses those values as defaults for the metapackage The inheritance priority is: 1. ``[tool.rind]`` values (highest priority) 2. ``[project]`` values in the metapackage's own ``pyproject.toml`` 3. Inherited values from core's ``pyproject.toml`` (lowest priority) Set ``inherit-metadata = false`` to disable this behavior and only use the core's pyproject.toml for version detection. Sdist Structure ~~~~~~~~~~~~~~~ When building a wheel from an sdist, the core package's ``pyproject.toml`` isn't available (the sdist is extracted to a temporary directory). To handle this, rind generates a **resolved** ``pyproject.toml`` in the sdist: 1. During ``build_sdist``, all metadata is computed from the core package 2. A new ``pyproject.toml`` is generated with all values hardcoded (version, dependencies, inherited metadata, etc.) 3. This resolved file has no ``core-path`` setting, which tells rind to read values directly from ``[project]`` instead of computing them This ensures wheels built from sdists have the same metadata as wheels built directly from the repository, without needing access to the core package. Import Name vs Package Name --------------------------- A key feature of this setup is that the **import name stays the same** while the **package name changes**. Consider this structure: .. code-block:: text myproject/ ├── pyproject.toml # name = "mypackage-core" └── src/ └── mypackage/ # Import name: mypackage └── __init__.py The package installed via pip is ``mypackage-core``, but the directory containing the code is ``mypackage/``, so users write: .. code-block:: python import mypackage # Works! When ``mypackage`` (the metapackage) is installed, it pulls in ``mypackage-core``, which provides the ``mypackage/`` directory. The metapackage itself provides no Python files, so there's no conflict. Comparison with Alternatives ---------------------------- **Why not use extras on a single package?** You could use optional dependencies: .. code-block:: toml [project] name = "mypackage" dependencies = ["numpy"] # minimal [project.optional-dependencies] recommended = ["pandas", "matplotlib"] Users install with ``pip install mypackage[recommended]``. The metapackage approach is better when: - You want the "batteries included" experience to be the default - The package name without brackets should give the full experience - You want clear separation between "essential" and "recommended" **Why not two repositories?** You could maintain separate repos for core and meta packages. The single-repo approach is better because: - Versions are automatically synchronized via git tags - No coordination needed between repos for releases - Single source of truth for metadata - Easier CI/CD setup Standalone Mode --------------- rind also supports a **standalone mode** for creating metapackages without a core package. In this mode: - All metadata is specified directly in ``[project]`` - No ``core-path`` is needed - Version must be static (no dynamic versioning) - No metadata inheritance This is useful for creating curated dependency bundles like "my-data-science-stack" that group related packages together. Limitations ----------- - **Both packages must be released together** (core-package mode only): Since versions are synchronized, you can't release one without the other. - **No code in metapackage**: The metapackage cannot contain any Python code. If you need wrapper code, it should go in the core package. - **Static version in standalone mode**: Standalone metapackages require a static version in ``[project]``.