Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,94 @@ describe("PythonSetupEnvironmentSetup.setup", () => {
]);
});

it("offers manual interpreter selection without retrying a failed download", async () => {
const shown: {actions?: PythonSetupErrorAction[]}[] = [];
const telemetry = makeTelemetryRecorder();
const pythonInstallFailure: PythonSetupResult = {
schemaVersion: 1,
command: "environments setup-local",
ok: false,
mode: "default",
dryRun: false,
greenfield: false,
phases: [],
warnings: [],
durationMs: 0,
error: {
code: "E_PYTHON_INSTALL",
failurePhase: "provision",
message: "Python download failed",
diskMutated: false,
},
};
const cli = makeCli({resolve: pythonInstallFailure});
const setup = new PythonSetupEnvironmentSetup(
makeDeps({
cli,
recordSetupAttempt: telemetry.recordSetupAttempt,
showError: async (_message, _detail, actions) => {
shown.push({actions});
},
})
);

await setup.setup();

expect(cli.calls).to.have.length(1);
expect(telemetry.results[0].pythonSetupFlow).to.equal(
"manual_selection_requested"
);
expect(shown[0].actions).to.deep.equal([
{
label: "Select Python interpreter",
command: "databricks.environment.selectPythonInterpreter",
},
]);
});

it("prioritizes manual selection after installed fallback and keeps report in logs", async () => {
const shown: {
detail?: string;
actions?: PythonSetupErrorAction[];
}[] = [];
const fallbackFailure: PythonSetupResult = {
schemaVersion: 1,
command: "environments setup-local",
ok: false,
mode: "default",
dryRun: false,
pythonResolution: "installed_fallback",
greenfield: false,
phases: [],
warnings: [],
durationMs: 0,
error: {
code: "E_VALIDATE",
failurePhase: "validate",
message: "selected Python could not be validated",
diskMutated: false,
},
};
const setup = new PythonSetupEnvironmentSetup(
makeDeps({
cli: makeCli({resolve: fallbackFailure}),
showError: async (_message, detail, actions) => {
shown.push({detail, actions});
},
})
);

await setup.setup();

expect(shown[0].actions).to.deep.equal([
{
label: "Select Python interpreter",
command: "databricks.environment.selectPythonInterpreter",
},
]);
expect(shown[0].detail).to.contain("Report this problem:");
});

it("offers the mapped documentation action for a doc-linked failure", async () => {
const shown: {actions?: PythonSetupErrorAction[]}[] = [];
const setup = new PythonSetupEnvironmentSetup(
Expand Down Expand Up @@ -1167,7 +1255,9 @@ describe("PythonSetupEnvironmentSetup telemetry", () => {
await setup.setup();

// Distinct from `failed`: the user gave up, nothing broke.
expect(telemetry.results).to.deep.equal([{outcome: "cancelled"}]);
expect(telemetry.results).to.deep.equal([
{outcome: "cancelled", pythonSetupFlow: "cancelled"},
]);
});

it("reports not_started when the CLI produces no result", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
isIndexUnreachableFailure,
NO_COMPUTE_TARGET_MESSAGE,
PythonSetupErrorAction,
SELECT_PYTHON_INTERPRETER_COMMAND_ID,
} from "../utils/errorMessages";
import {
buildExtensionFailureReportAction,
Expand Down Expand Up @@ -385,7 +386,10 @@ export class PythonSetupEnvironmentSetup implements Disposable {
} catch (e) {
// A cancelled run is a user action, not a failure: stay quiet.
if (e instanceof PythonSetupCancelledError) {
reportResult({outcome: "cancelled"});
reportResult({
outcome: "cancelled",
pythonSetupFlow: "cancelled",
});
return;
}
// Spawn/parse errors reject with a real Error carrying CLI stderr;
Expand Down Expand Up @@ -418,8 +422,23 @@ export class PythonSetupEnvironmentSetup implements Disposable {
// its doc-link button, unchanged.
const reportAction = getPythonSetupReportAction(result, reportEnv);
const reportRepo = reportRepoForResult(result);
const remediationActions = getPythonSetupErrorActions(result);
const pythonSetupFlow =
result.error?.code === "E_PYTHON_INSTALL" ||
result.pythonResolution === "installed_fallback"
? "manual_selection_requested"
: result.pythonResolution;
const actions = remediationActions.some(
(candidate) =>
candidate.command === SELECT_PYTHON_INTERPRETER_COMMAND_ID
)
? remediationActions
: reportAction
? [reportAction]
: remediationActions;
reportResult({
outcome: "failed",
...(pythonSetupFlow !== undefined ? {pythonSetupFlow} : {}),
failurePhase: result.error?.failurePhase,
errorCode: result.error?.code,
envKey: result.compute?.envKey,
Expand All @@ -435,9 +454,7 @@ export class PythonSetupEnvironmentSetup implements Disposable {
result,
reportRepo ? reportLogLink(reportRepo) : undefined
),
reportAction
? [reportAction]
: getPythonSetupErrorActions(result)
actions
)
);
return;
Expand Down Expand Up @@ -466,6 +483,9 @@ export class PythonSetupEnvironmentSetup implements Disposable {
});
reportResult({
outcome: "failed",
...(result.pythonResolution !== undefined
? {pythonSetupFlow: result.pythonResolution}
: {}),
failurePhase: "adopt",
envKey: result.compute.envKey,
reportOffered: true,
Expand Down Expand Up @@ -501,6 +521,9 @@ export class PythonSetupEnvironmentSetup implements Disposable {
const message = e instanceof Error ? e.message : String(e);
reportResult({
outcome: "failed",
...(result.pythonResolution !== undefined
? {pythonSetupFlow: result.pythonResolution}
: {}),
failurePhase: "persist",
envKey: result.compute.envKey,
reportOffered: true,
Expand All @@ -527,6 +550,9 @@ export class PythonSetupEnvironmentSetup implements Disposable {
// releases regardless of whether the user dismisses the notification.
reportResult({
outcome: "ok",
...(result.pythonResolution !== undefined
? {pythonSetupFlow: result.pythonResolution}
: {}),
envKey: result.compute.envKey,
warnings: result.warnings,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
/** Provisioning mode. `constraints-only` omits the databricks-connect dep. */
export type PythonSetupMode = "default" | "constraints-only";

/** How the CLI obtained the Python interpreter used for provisioning. */
export type PythonResolution = "uv_install_succeeded" | "installed_fallback";

/** Canonical execution phases, always reported in this order. */
export type PythonSetupPhaseName =
| "preflight"
Expand Down Expand Up @@ -97,6 +100,7 @@ export interface PythonSetupResult {
ok: boolean;
mode: PythonSetupMode;
dryRun: boolean;
pythonResolution?: PythonResolution;
/**
* The resolved compute the environment was provisioned against. Mirrors the
* CLI's `compute` result key (renamed from `target` in databricks/cli#6100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,29 @@ describe("getPythonSetupErrorAction", () => {
});
});

it("points E_PYTHON_INSTALL at the uv install-Python docs", () => {
it("asks for manual interpreter selection after Python download fails", () => {
expect(
getPythonSetupErrorAction(failure("E_PYTHON_INSTALL"))
).to.deep.equal({
label: "Install a Python version",
url: "https://docs.astral.sh/uv/guides/install-python/",
label: "Select Python interpreter",
command: "databricks.environment.selectPythonInterpreter",
});
});

it("asks for manual selection when provisioning fails after installed fallback", () => {
expect(
getPythonSetupErrorAction(
failure(
"E_PROVISION",
{},
{
pythonResolution: "installed_fallback",
}
)
)
).to.deep.equal({
label: "Select Python interpreter",
command: "databricks.environment.selectPythonInterpreter",
});
});

Expand Down Expand Up @@ -406,13 +423,13 @@ describe("getPythonSetupErrorActions", () => {
]);
});

it("wraps a non-uv code's single action in a one-element list", () => {
it("wraps Python install recovery in a one-element list", () => {
expect(
getPythonSetupErrorActions(failure("E_PYTHON_INSTALL"))
).to.deep.equal([
{
label: "Install a Python version",
url: "https://docs.astral.sh/uv/guides/install-python/",
label: "Select Python interpreter",
command: "databricks.environment.selectPythonInterpreter",
},
]);
});
Expand Down
21 changes: 13 additions & 8 deletions packages/databricks-vscode/src/python-setup/utils/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ export const UV_INDEX_DOCS_URL =
export const UV_PROJECTS_DOCS_URL =
"https://docs.astral.sh/uv/concepts/projects/";

/** uv's Python-version guide — for E_PYTHON_INSTALL. */
export const UV_PYTHON_INSTALL_DOCS_URL =
"https://docs.astral.sh/uv/guides/install-python/";

/** uv's resolution concept page — for a genuine E_PROVISION dependency conflict. */
export const UV_RESOLUTION_DOCS_URL =
"https://docs.astral.sh/uv/concepts/resolution/";
Expand Down Expand Up @@ -142,6 +138,10 @@ export function isIndexUnreachableFailure(result: PythonSetupResult): boolean {
export const USE_MANUAL_SETUP_COMMAND_ID =
"databricks.environment.useManualPythonSetup";

/** Existing extension command that opens the Python interpreter picker. */
export const SELECT_PYTHON_INTERPRETER_COMMAND_ID =
"databricks.environment.selectPythonInterpreter";

/**
* Command that runs uv's official installer in a terminal for the current
* platform, surfaced as the primary E_UV_MISSING remediation button (see
Expand Down Expand Up @@ -251,10 +251,6 @@ const DOC_LINKS: Partial<Record<PythonSetupErrorCode, PythonSetupErrorAction>> =
label: "Set up a uv project",
url: UV_PROJECTS_DOCS_URL,
},
E_PYTHON_INSTALL: {
label: "Install a Python version",
url: UV_PYTHON_INSTALL_DOCS_URL,
},
E_PROVISION: {
label: "Resolve dependency conflicts",
url: UV_RESOLUTION_DOCS_URL,
Expand Down Expand Up @@ -287,6 +283,15 @@ export function getPythonSetupErrorAction(
if (!err) {
return undefined;
}
if (
err.code === "E_PYTHON_INSTALL" ||
result.pythonResolution === "installed_fallback"
) {
return {
label: "Select Python interpreter",
command: SELECT_PYTHON_INTERPRETER_COMMAND_ID,
};
}
// A blocked package index arrives as E_PROVISION and is distinguished by the
// CLI's message, not its code — so resolve it before the code-keyed map,
// ahead of E_PROVISION's generic dependency-conflict link.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ describe("formatSetupNotification", () => {
expect(message).to.contain("with 1 warning —");
expect(message).to.not.contain("1 warnings");
});

it("explains when setup used an installed Python after download failed", () => {
const fallback = {
...SUCCESS_DEFAULT,
pythonResolution: "installed_fallback",
} as PythonSetupResult;

expect(formatSetupNotification(fallback)).to.equal(
"Python environment ready — Python download failed; used a " +
"compatible installed Python instead. .venv created and " +
"selected for your Databricks project."
);
});
});

describe("formatSetupLog", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import {venvInterpreterPath} from "./venvInterpreterPath";
*/
export function formatSetupNotification(result: PythonSetupResult): string {
const tail = ".venv created and selected for your Databricks project.";
const fallback =
result.pythonResolution === "installed_fallback"
? "Python download failed; used a compatible installed Python " +
"instead. "
: "";
const n = result.warnings.length;
if (n === 0) {
return `Python environment ready — ${tail}`;
return `Python environment ready — ${fallback}${tail}`;
}
return (
`Python environment ready, with ${n} ` +
`warning${n === 1 ? "" : "s"} — ${tail}`
`warning${n === 1 ? "" : "s"} — ${fallback}${tail}`
);
}

Expand Down
14 changes: 14 additions & 0 deletions packages/databricks-vscode/src/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ export type SetupTrigger = "auto_open" | "explicit_command" | "run" | "debug";
*/
export type PythonSetupRunTrigger = "initial" | "rerun";

/** Categorical outcome of Python acquisition and its user recovery path. */
export type PythonSetupFlow =
| "uv_install_succeeded"
| "installed_fallback"
| "manual_selection_requested"
| "cancelled";

// The uv-native ("VPEX") python-setup flow mirrors the CLI's `environments
// setup-local --output json` contract, so the setup event unions are owned by
// the result model (the TypeScript view of that contract) and re-exported here.
Expand Down Expand Up @@ -528,6 +535,7 @@ export class EventTypes {
};
[Events.PYTHON_ENV_SETUP_RESULT]: EventType<{
outcome: PythonSetupOutcome;
pythonSetupFlow?: PythonSetupFlow;
failurePhase?: PythonSetupFailurePhase;
errorCode?: PythonSetupErrorCode;
envKey?: string;
Expand Down Expand Up @@ -556,6 +564,12 @@ export class EventTypes {
"ok | failed | cancelled (user aborted) | not_started (the CLI produced no " +
"result) | no_compute (the CTA was a dead end: nothing was attached to set up for)",
},
pythonSetupFlow: {
comment:
"Whether uv downloaded Python, an installed compatible Python was used, " +
"manual interpreter selection was requested, or the operation was cancelled. " +
"Categorical only; never contains an interpreter path or version",
},
failurePhase: {
comment:
"Which phase broke: the CLI's preflight/resolve/fetch/merge/provision/validate, " +
Expand Down
Loading
Loading