Skip to content
rlsbl.commands.rewrite.uv_path_sources
On this page

Converts path- and workspace-sourced dependencies into registry floors at the version the lock resolves, deleting the source entry that overrode them.

#rlsbl.commands.rewrite.uv_path_sources

#rlsbl.commands.rewrite.uv_path_sources

Convert path/workspace-sourced dependencies into registry floors.

rlsbl rewrite uv-path-sources takes a working tree whose pyproject.toml resolves internal dependencies from local checkouts and turns each one into a registry constraint floored at the version the lock already resolves:

  • [project].dependencies, every [project.optional-dependencies] extra

and every PEP 735 [dependency-groups] group have their direct references (dep @ file:///...) replaced by dep>=<locked version>;

  • the matching [tool.uv.sources] path/workspace entry is deleted, because

a source entry left behind keeps overriding the constraint that was just written. A source declared as a LIST of marker-gated tables is PRUNED instead: only its path/workspace elements go, so an index sibling covering the platforms the checkout does not is left standing;

  • .rlsbl/config.json's internal_dep_floors gains every converted name,

so rlsbl's dep-floors preflight check starts policing the floor it just created (the key is created when absent).

Where the lock is read from ---------------------------

The floor is the version uv.lock resolves, and there is exactly ONE lock that resolves a given manifest. Which file that is comes from :func:rlsbl.uv_workspace.locate_uv_lock -- the shared locator this command originally grew and now shares with the dep-floors and dep-locks checks: the lock beside the manifest, else the lock of the uv workspace root that claims the directory, else nothing. A search that finds nothing is a hard error here, naming both locations probed.

Whichever lock is read, the WRITES stay in the target directory: its own pyproject.toml and its own .rlsbl/config.json (created when absent).

Release-first, enforced -----------------------

A floor is only meaningful if the registry can satisfy it. Before writing anything the command probes PyPI for the exact locked version of each dependency, and:

  • not published is a hard error naming the remedy -- release that

dependency first. Writing the floor anyway would produce a manifest no consumer can resolve, which is the exact failure this command exists to prevent.

  • probe failure (network, HTTP 5xx, anything that is not a clean 404) is

ALSO a hard error. Fail-closed: "we could not ask" is not evidence of publication, and a floor written on a failed probe is indistinguishable from one written on a lie.

The probe is a plain HTTPS GET through effects.urlopen, which executes in every mode including --dry-run -- a read changes nothing on the far side, and a preview that could not probe would have nothing to preview.

Preview and apply -----------------

One verdict item per dependency, plus one for the config-key update. Each dependency's item carries the number of manifest entries it occupies, and the apply re-counts them from disk before writing: a count that moved between preview and apply is a hard abort with nothing further written.

#UvPathSourceError

A hard error in the path-source conversion.

#Conversion

One dependency's pending conversion.

#occurrences

python
def occurrences(self)

Everything the apply would rewrite for this dependency.

#constraint

python
def constraint(self)

#resolve_lock

python
def resolve_lock(project_root)

(locked versions, LockLocation) for the manifest in project_root.

The location comes from :func:rlsbl.uv_workspace.locate_uv_lock; this function adds the two refusals that belong to READING it: no lock at all, and a lock that is present but does not parse. The second is a hard error rather than an absence, because treating it as absent would walk past it to a different lock -- exactly the silent switch this command must not make.

#path_sourced_names

python
def path_sourced_names(doc)

{normalized: declared} for every path/workspace-sourced dependency.

The union of two declarations, because uv accepts either shape:

  • a [tool.uv.sources] entry with path/workspace, whose

dependency array entry is an ORDINARY requirement (often bare "sibling" with no constraint at all) -- the common uv shape;

  • a direct reference in the dependency array itself

("sibling @ file:///...") with no sources entry.

#count_entries

python
def count_entries(doc, name)

(dependency-array entries, sources entries) naming name.

#collect_conversions

python
def collect_conversions(doc, locked, lock_label='uv.lock')

Every path/workspace-sourced dependency, with its locked version.

lock_label names the lock locked was read from, so a refusal points at the file that failed to resolve the package -- which is not always the one beside the manifest (see :func:resolve_lock).

#probe_published

python
def probe_published(name, version)

Hard-error unless version of name is published on PyPI.

Fail-closed: anything that is not a definitive "found" refuses.

#observe

python
def observe(project_root, *, probe=probe_published)

Build the plan: one item per dependency, plus the config update.

#apply_item

python
def apply_item(item, project_root, applied=None)

Apply one item, refusing when its count moved since the preview.

applied is an optional list the caller passes through every item; each successful write appends its key, so an abort can name what is already on disk (this command has no rollback -- see :mod:.abort).

#_apply_config

python
def _apply_config(root, additions)

Add additions to internal_dep_floors. True when it wrote.

#cmd_uv_path_sources

python
def cmd_uv_path_sources(flags, project_root)

rlsbl rewrite uv-path-sources -- path sources become registry floors.

flags["dry-run"] -- plan only.

Search