Skip to content

fix(python-setup): recover a uv workspace member from a constraint conflict - #2254

Merged
rugpanov merged 4 commits into
mainfrom
uv-workspace-shared-venv-2
Oct 15, 2026
Merged

rugpanov merged 4 commits into
mainfrom
uv-workspace-shared-venv-2

Conversation

@rugpanov

@rugpanov rugpanov commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Stack (merge in this order):

  1. fix(python-setup): select the shared uv workspace venv for a workspace member #2253 fix(python-setup): select the shared uv workspace venv for a workspace member (merged)
  2. fix(python-setup): recover a uv workspace member from a constraint conflict #2254 fix(python-setup): recover a uv workspace member from a constraint conflict (see diff) <- this PR

Changes

For a uv workspace member, setup-local writes the constraint pins into the root pyproject.toml and shares one venv at the root. The conflict recovery and the adoption gauge assumed one pyproject.toml and one .venv in the project folder.

  • Retry DB Connect setup: the retry also restores the root pyproject.toml from uvWorkspaceBackupPath. Without this, the retry with --no-constraints keeps the conflicting root pins and fails again.
    • The retry checks every backup before it restores the first one.
    • The extension trusts uvWorkspaceRoot only when it contains the project folder, as opened or symlink-resolved, because the restore writes there.
    • A member with no root backup gets no retry, because the root pins stay.
    • A later setup run inside the restored folder (the workspace root for a member, else the project) makes an older retry stale, because that run can rewrite the same files. A stale click tells the user to run the setup again.
  • Open pyproject.toml opens the root file for a member, because it holds the pins.
  • Adoption gauge: the saved setup state records the project folder and the absolute venv folder. The gauge checks that venv folder. It skips a reading while the state belongs to another bundle in the same workspace folder. State saved by older versions keeps the old behavior.

Tests

  • New unit tests cover the root restore, the check before the restore, a root-only backup, a member without a root backup, an untrusted root, a symlinked project folder, stale retries after a member run, after a root project run, and through a symlink, a live retry after an unrelated run, a backup that cannot be read, the "Open" target, the saved state, and the gauge.
  • Full unit suite: 1420 passing, 0 failing. yarn run test:lint reports 0 errors.

Known limitations

  • The stale-retry check works within one session. A run that starts outside the restored folder, for example a member opened through a symlink that points outside the workspace root, does not make the retry stale.
  • After a retry restores the member file, a failed copy of the root file can still leave the two files out of step. The check before the restore makes this rare.
  • Members share one venv, so a sync for member B can remove packages that only member A needs. Member A still shows as ready in this session.
  • Drift detection still reads the single setup state and does not use the new project folder.

This pull request and its description were written by Isaac.

@rugpanov

Copy link
Copy Markdown
Contributor Author

🤖 Integration tests ✅ passed for 4a3c1202.
View run

@rugpanov

Copy link
Copy Markdown
Contributor Author

🤖 Integration tests running for a5ce3bda — ⏳.
View run

@rugpanov
rugpanov force-pushed the uv-workspace-shared-venv-2 branch from a5ce3bd to 1201dd5 Compare October 13, 2026 21:37
@rugpanov

rugpanov commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Integration tests ✅ passed for 1201dd56.
View run

@rugpanov
rugpanov marked this pull request as ready for review October 14, 2026 07:55
@rugpanov
rugpanov requested a review from misha-db October 14, 2026 07:55

@misha-db misha-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (xhigh) findings, posted inline. All low-severity — no correctness blockers. Findings 1 and 3 are related (two divergent copies of a path-containment check). Nothing here blocks approval.

function assertBackupInside(folder: string, backupPath: string): string {
const root = path.resolve(folder);
const rel = path.relative(root, path.resolve(backupPath));
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Low · Category: Correctness (latent)

assertBackupInside uses rel.startsWith(".."), which also rejects a path legitimately inside the folder whose first segment merely begins with .. (e.g. /mono/..cache.bak gives rel = "..cache.bak"), throwing "Refusing to restore ... outside" and aborting a valid retry.

Unreachable in practice today because the CLI backup name is the fixed pyproject.toml.bak. The sibling containsPath helper in PythonSetupEnvironmentSetup.ts already uses the precise form, so the two containment checks disagree on this edge. Suggested: rel === ".." || rel.startsWith(".." + path.sep).

const recoverableConflict = conflictBackupPath !== undefined;
!invocation.skipConstraints;
const cwdForms = isPinConflict
? [cwd, await this.realPathOrSelf(cwd)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Low · Category: Efficiency

In the pin-conflict branch, cwdForms recomputes [cwd, await this.realPathOrSelf(cwd)], duplicating the entry already pushed to runFolders earlier in runResolved. Every conflicting run makes two fs.realpath(cwd) calls instead of one. Harmless (realpath is cheap, conflicts are rare), but the second could reuse the existing runFolders tail.

}

/** Whether `child` is `parent` or a path inside it, compared by text. */
function containsPath(parent: string, child: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Low · Category: Maintainability (duplication)

This lexical containsPath helper and the extracted assertBackupInside in pythonSetupDeps.ts do the same "is path inside dir" job with divergent edge semantics: this one uses rel === ".." || rel.startsWith(".." + path.sep), the other uses rel.startsWith(".."). Two copies in one feature means future fixes must land in both. Consider consolidating into one shared containment util.

{folder: root, backupPath: result.uvWorkspaceBackupPath},
];
const {backupPath} = result;
if (backupPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Low · Category: Simplification (duplication)

conflictBackupsOf builds {folder: backupFolder(bp, cwdForms), backupPath: bp} in both the member and non-member branches; the two could share one expression so that a change to how the folder for a project-file backup is derived lives in a single place. Pure cleanup, no behavioral defect.

@@ -1,3 +1,4 @@
import path from "node:path";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Low · Category: Consistency

Import spelling is mixed across files touched by this PR: this file and PythonSetupAdoptionManager.ts use node:path, while pythonSetupDeps.ts and setupSummary.ts use path. No runtime effect and yarn fix will not normalize it, but worth picking one spelling for the feature.

@misha-db
misha-db self-requested a review October 14, 2026 13:02
rugpanov added a commit that referenced this pull request Oct 14, 2026
…e member (#2253)

**Stack** (merge in this order):

1. #2253 fix(python-setup): select the shared uv workspace venv for a
workspace member ([see
diff](main...uv-workspace-shared-venv))
**<- this PR**
2. #2254 fix(python-setup): recover a uv workspace member from a
constraint conflict ([see
diff](uv-workspace-shared-venv...uv-workspace-shared-venv-2))

## Changes

In a monorepo with one uv workspace and one bundle per member, the
Python environment setup selects the wrong interpreter (#2157). The
Databricks CLI now supports uv workspace members in `databricks
environments setup-local` (databricks/cli#7018, databricks/cli#7026).
For a member, the CLI provisions one shared venv at the workspace root.
It reports `venvPath` relative to the member folder, for example
`../../.venv`, and it adds `uvWorkspaceRoot` and
`uvWorkspaceBackupPath`.

- The extension resolves `venvPath` to an absolute folder. It uses this
folder to select the interpreter and to read the venv name from
`pyvenv.cfg`.
- A value that climbs (`../../.venv`) counts from the symlink-resolved
project folder on Unix, as the CLI does.
  - `.venv` stays under the project folder as opened.
- An absolute value (from an absolute `UV_PROJECT_ENVIRONMENT`) stays as
it is.
- Before this change, the extension gave the Python extension a relative
interpreter path. It also read `pyvenv.cfg` relative to the working
folder of the extension host, so the venv name did not show in the
summary. These fixes also apply to a standalone project.
- The result parser reads `uvWorkspaceRoot` and `uvWorkspaceBackupPath`.
- For a member, the success summary names the shared venv and the root
`pyproject.toml`. The venv folder uses Windows separators on Windows.
- The warning histogram counts `W_UV_WORKSPACE_CONSTRAINTS_REPLACED`.

## Tests

- New unit tests cover the venv folder for a member, a symlinked project
folder, `.venv` under the opened folder, an absolute `venvPath`, the
venv name, the result fields, the summary text on Linux and Windows, and
the warning count.
- Full unit suite: 1401 passing, 0 failing. `yarn run test:lint` reports
0 errors.

## Known limitations

- The fix for #2157 needs a bundled CLI that contains
databricks/cli#7018. With the current CLI (1.20.0), `venvPath` is always
`.venv`, and this PR changes only the two standalone fixes above.
- On Windows, the extension does not resolve a directory symlink in the
project folder. The CLI resolves it for a member, so a member opened
through such a symlink gets a wrong venv folder.

This pull request and its description were written by Isaac.

---------

Co-authored-by: Isaac <no-reply@databricks.com>
rugpanov and others added 3 commits October 14, 2026 15:58
…nflict

*Why*:
* For a uv workspace member, setup-local writes the constraint pins into the root pyproject.toml. On a conflict, "Retry DB Connect setup" restored only the member file, so the retry with --no-constraints kept the conflicting root pins, and "Open pyproject.toml" opened the member file.
* The adoption gauge checked `<project>/.venv`, so it reported a member's shared venv as missing.

*What*:
* The retry also restores the root pyproject.toml from `uvWorkspaceBackupPath`, and it checks every backup before it restores the first one. It trusts `uvWorkspaceRoot` only when the root contains the project folder (as opened or symlink-resolved). A member with no root backup gets no retry.
* "Open pyproject.toml" opens the root file for a member.
* Any later setup run makes an older retry stale, because it can rewrite the shared root file. A stale click tells the user to run the setup again.
* The saved setup state records the project folder and the absolute venv folder. The adoption gauge checks that venv folder, and it skips a reading while the state belongs to another bundle in the same workspace folder.

Co-authored-by: Isaac <no-reply@databricks.com>
…tore

* A later setup run makes an older retry stale only when it ran inside the folder whose files the retry restores: the uv workspace root for a member, else the project. A run of an unrelated project no longer disables the retry.
* Pick the backup folder in the form the CLI named the backup, also for a standalone project opened through a symlink.
* checkProjectFileBackup checks that the backup is readable, not only that it exists.

Co-authored-by: Isaac <no-reply@databricks.com>
Each run records its project folder as opened and symlink-resolved, so a later run of a sibling member counts as stale also when the workspace is open through a symlink and the CLI reports the real root. The stale message no longer says that another setup ran when the retry itself changed the files.

Co-authored-by: Isaac <no-reply@databricks.com>
@rugpanov
rugpanov force-pushed the uv-workspace-shared-venv-2 branch from 1201dd5 to 5728708 Compare October 14, 2026 14:00
@rugpanov

rugpanov commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Integration tests ❌ failed for 57287082.
View run

assertBackupInside rejected a backup inside the folder whose name starts
with "..", because it used rel.startsWith(".."). Move containsPath to
utils and use it for both the backup check and the conflict scope.

Co-authored-by: Isaac <no-reply@databricks.com>
@rugpanov

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

@github-actions

Copy link
Copy Markdown
Contributor

If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/vscode

Inputs:

  • PR number: 2254
  • Commit SHA: 70b70e794420bba416dd0ba4644a30dd3c62e415

Checks will be approved automatically on success.

@rugpanov

rugpanov commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Integration tests ✅ passed for 70b70e79.
View run

@rugpanov
rugpanov merged commit 7752f5d into main Oct 15, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 22, 2026
@github-actions github-actions Bot mentioned this pull request Oct 22, 2026
@rugpanov
rugpanov deleted the uv-workspace-shared-venv-2 branch October 22, 2026 15:02
abdelrahmanomar1515 pushed a commit that referenced this pull request Oct 22, 2026
## packages/databricks-vscode
##  (2026-10-22)

- fix(bundle): find sub-projects without blocking the extension host
(#2249)
([5ebf39f](5ebf39f))
- fix(network): let VS Code handle proxies and certificates for SDK
calls (#2266)
([589041a](589041a))
- fix(python-setup): recover a uv workspace member from a constraint
conflict (#2254)
([7752f5d](7752f5d))
- fix(python-setup): select the shared uv workspace venv for a workspace
member (#2253)
([faa6f58](faa6f58))
- Expose the bundle Configuration view and Bundle Resource Explorer in
Databricks Remote SSH mode (#2236)
([375dd8d](375dd8d))
- Update Databricks CLI to v1.22.1 (#2270)
([bc837b5](bc837b5))


## packages/databricks-vscode-types
##  (2026-10-22)

Co-authored-by: releasebot <noreply@github.com>

This branch was previously deployed

1 inactive deployment
test-trigger-is — 70b70e79 Deployed Oct 15, 2026 by rugpanov via Trigger Tests #2379
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants