The six distributions

nab is one command built out of six distributions. Installing nab pulls all of them, so the split matters only when you embed part of nab elsewhere.

Except for nab-resolver’s documented surface, these APIs are experimental. Pin exact versions when embedding them.

What each one is

nab-resolver is the solver: a generic PubGrub implementation over abstract versions and ranges, with no knowledge of Python packaging. It does not import packaging.

nab-markersets is the marker algebra: a PEP 508 marker read as the set of environments it selects. Its parser and evaluator come from whichever packaging is installed, released or nab’s fork. Reasoning about markers works through it.

nab-provider is the resolution logic: the provider the solver asks for candidates, the target and tag model, marker evaluation, extras expansion, the metadata parser, packaging policies with their per-package and per-index overrides, and the result store. It does no I/O: everything arrives through nab_provider.fetch_port.FetchPort, which its host implements.

nab-index is the index client: the Simple API reader, the on-disk HTTP cache, the lazy-wheel range reader, the archive and VCS fetchers, and the local file:// index.

nab-project is nab’s own host. It implements FetchPort over nab-index in nab_project.fetch.FetchCoordinator, then adds workspace discovery, the PEP 517 build path, the lockfile writer, the downloader, and resolve orchestration. Its host supplies a target list and nab_project.inputs.ResolveInputs.

nab is the CLI. It owns the [tool.nab] config ladder in nab.config, reads the project options, and turns them into the targets and inputs nab-project resolves under.

How they depend on each other

nab-resolver   ->  (nothing outside the standard library)
nab-markersets ->  packaging, or nab-provider, by extra
nab-provider   ->  nab-markersets, nab-resolver
nab-index      ->  nab-provider, packaging
nab-project    ->  nab-markersets, nab-provider, nab-index, nab-resolver,
                   packaging
nab            ->  nab-markersets, nab-provider, nab-index, nab-project,
                   nab-resolver

Only packaging is shown among the third-party dependencies because it exists here in two copies.

nab-index depends on nab-provider because WheelFile, SdistFile, IndexConfig, and the fetch errors live with the side that must never fetch.

Why the provider is separate

Resolution logic with no HTTP client in its import graph can be tested alone. CI runs nab-provider/tests in a workspace that installs only nab-provider, nab-markersets, and nab-resolver.

A host that owns a session and download path can implement FetchPort and use nab’s resolution without its networking, cache, or lock output.

Driving the provider on its own

nab_provider.testing ships a FetchPort over a store you fill, so nothing below reaches the network:

from nab_provider.provider import Provider
from nab_provider.records import WheelFile
from nab_provider.testing import make_coordinator
from nab_provider._vendor.packaging.ranges import VersionRange
from nab_resolver.resolver import Resolver


def wheel(name: str, version: str) -> WheelFile:
    return WheelFile(
        filename=f"{name}-{version}-py3-none-any.whl",
        url=f"https://example.invalid/{name}-{version}-py3-none-any.whl",
        version=version,
        requires_python=None,
        has_metadata=True,
        upload_time=None,
    )


port = make_coordinator(
    listings={"app": [wheel("app", "1.0")], "lib": [wheel("lib", "2.0")]},
    metadata_by_version={
        "1.0": "Metadata-Version: 2.1\nName: app\nVersion: 1.0\nRequires-Dist: lib\n",
        "2.0": "Metadata-Version: 2.1\nName: lib\nVersion: 2.0\n",
    },
)
provider = Provider(port)
resolver = Resolver(provider, range_type=VersionRange, root_version="0")
print(resolver.resolve({"app": VersionRange.full()}))

nab_project.resolve.resolve_for_targets is the entry point that takes a project path; the provider has none.

Where the vendored packaging fork lives

nab-provider carries nab’s fork of packaging at nab_provider._vendor.packaging. nab-project and nab.config use it to build Version, Requirement, and VersionRange objects consumed by the provider. A second copy would produce distinct classes, breaking isinstance checks and dictionary keys.

tasks/check_boundaries.py forbids other packages from reaching into a package’s _vendor tree and lists these two exceptions in VENDOR_ALLOWANCES.

nab_markersets._packaging prefers the vendored tree, then released packaging, and requires version 26.3 or newer. Inside nab the fork wins, so provider markers and exceptions retain the classes the algebra expects.

The nab-markersets[nab-vendored-packaging] extra installs the fork; standalone users install nab-markersets[packaging]. nab-index and nab-project still use released packaging for other APIs, but no marker crosses between the two copies.