Skip to content
rlsbl.uv_workspace
On this page

Where the one uv.lock that resolves a given manifest lives: beside it, or at the nearest ancestor whose uv workspace globs claim the project directory.

#rlsbl.uv_workspace

#rlsbl.uv_workspace

Where the one uv.lock that resolves a given manifest lives.

There is exactly ONE lock that resolves a manifest. Usually it sits beside it. In a uv WORKSPACE it does not: members have no lock of their own, and one lock at the workspace root resolves every member. Anything that reads a locked version therefore has to resolve the lock's LOCATION first:

  1. <project>/uv.lock when it exists;
  2. otherwise the uv.lock of the nearest ancestor that declares

[tool.uv.workspace] and whose members / exclude globs claim the project directory (:func:find_uv_workspace_root);

  1. otherwise nothing -- and the search says which two locations it probed, so

the caller can name them.

This is location resolution for ONE authoritative answer, not a try-A-then-B strategy with two different answers: a workspace member has no second lock that could disagree. When both files do exist -- a member carrying a stale lock of its own -- the one beside the manifest wins outright, because that is the lock uv itself would use for a standalone project.

Readability is deliberately NOT decided here. A lock that exists but does not parse is still the location; refusing to read it is the reader's job, and a locator that walked past an unreadable lock would silently answer from a different file.

The one locator ---------------

Everything that asks "is this directory a uv workspace member, and whose?" asks :func:find_uv_workspace_root here -- the lock readers (rlsbl rewrite uv-path-sources, dep-floors, dep-locks), the test runner's uv invocation, the tool-group resolver, the overlay environment locator and the workspace-unbuildable check. There used to be a SECOND copy of this walk-up in rlsbl.utils, and it answered differently in four ways; where they disagreed, uv's own rules decide, so this one is what survived:

  • the walk starts at the project's PARENT. A directory is never a member of

the workspace it declares itself, so starting at the project could only produce an answer uv never gives.

  • the FIRST ancestor declaring [tool.uv.workspace] decides. When its

globs do not claim the directory the answer is None, not "keep looking higher": uv forbids nested workspaces, so a second declaration further up is not a fallback, it is a repository that is already invalid.

  • member globs are expanded RECURSIVELY, so ** crosses directory

separators the way uv reads it. Expanded without that, packages/** quietly behaved like packages/* and a member one level deeper was not found.

  • paths are compared with symlinks resolved, so a checkout reached through a

symlinked path resolves to the same workspace as the same checkout reached directly.

Before the lock location was shared, dep-floors looked only beside the project root, so every member of a uv workspace reported "no uv.lock" and its declared floors went unpoliced.

#LockLocation

The one lock that resolves a manifest, and how it was reached.

#label

python
def label(self, project_root)

How the lock is named in errors -- relative to the target.

#describe

python
def describe(self, project_root)

One fact line naming which lock a version came from.

#LockSearch

The outcome of looking for a manifest's lock.

location is None when no lock was found; probed then describes every location that was looked at, so a caller's refusal can name them.

#uv_workspace_table

python
def uv_workspace_table(pyproject_path)

[tool.uv.workspace] of pyproject_path, or None when it has none.

#_glob_claims

python
def _glob_claims(patterns, workspace_root, target)

True when any glob in patterns expands to target.

Expanded against the real filesystem, which is how uv reads them: the globs are relative to the workspace root, * stops at a directory separator and ** crosses them.

#workspace_claims

python
def workspace_claims(workspace_root, table, target)

uv's membership rule: a members glob hits, no exclude glob does.

#find_uv_workspace_root

python
def find_uv_workspace_root(project_root)

The uv workspace root that claims project_root, or None.

uv's own discovery, walked the same way: the FIRST ancestor declaring [tool.uv.workspace] decides. A directory that declaration does not claim -- no members glob matches it, or an exclude glob does -- is a standalone project, not a member of some further ancestor; uv forbids nested workspaces, so there is never a second declaration to consult.

#locate_uv_lock

python
def locate_uv_lock(project_root)

Return the :class:LockSearch for the manifest in project_root.

Existence only: a lock that is present but unparseable is still the location (see the module docstring).

Search