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:
<project>/uv.lockwhen it exists;- otherwise the
uv.lockof the nearest ancestor that declares
[tool.uv.workspace] and whose members / exclude globs claim the project directory (:func:find_uv_workspace_root);
- 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
def label(self, project_root)How the lock is named in errors -- relative to the target.
#describe
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
def uv_workspace_table(pyproject_path)[tool.uv.workspace] of pyproject_path, or None when it has none.
#_glob_claims
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
def workspace_claims(workspace_root, table, target)uv's membership rule: a members glob hits, no exclude glob does.
#find_uv_workspace_root
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
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).