Skip to content
Merged
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 @@ -6,6 +6,7 @@ import {
getPythonSetupErrorMessage,
INSTALL_UV_COMMAND_ID,
isIndexUnreachableFailure,
isMissingProjectTableFailure,
USE_MANUAL_SETUP_COMMAND_ID,
} from "./errorMessages";
import {
Expand All @@ -27,6 +28,15 @@ const INDEX_UNREACHABLE_CLI_MSG =
"error: Failed to fetch: `https://pypi.org/simple/ipykernel/`\n" +
" Caused by: tcp connect error: Connection refused (os error 61)";

/**
* The real CLI text for `errNoProjectTable` (libs/localenv): the merge phase
* refuses to write requires-python because the pyproject.toml has no [project]
* table. Wrapped by the "merge managed regions failed:" prefix exactly as it
* reaches the user (see databricks/databricks-vscode#2177).
*/
const NO_PROJECT_TABLE_CLI_MSG =
"merge managed regions failed: pyproject.toml has no [project] table to hold requires-python";

/** Build a minimal failed result carrying a specific error. */
function failure(
code: PythonSetupErrorCode,
Expand Down Expand Up @@ -153,6 +163,31 @@ describe("getPythonSetupErrorMessage", () => {
);
});

it("maps a [project]-less E_MERGE to actionable copy, not the generic merge text", () => {
// The errNoProjectTable variant: a valid dependency-groups-only manifest,
// so the copy names the concrete fix instead of the generic merge failure.
const msg = getPythonSetupErrorMessage(
failure("E_MERGE", {
message: NO_PROJECT_TABLE_CLI_MSG,
failurePhase: "merge",
})
);
expect(msg).to.match(/\[project\] table/i);
expect(msg).to.match(/requires-python/i);
expect(msg).to.contain("databricks.python.environmentSetup");
expect(msg).to.not.match(/failed to merge the runtime constraints/i);
});

it("keeps the generic merge copy for an E_MERGE that is not the [project]-table case", () => {
const msg = getPythonSetupErrorMessage(
failure("E_MERGE", {
message: "cannot merge: unsupported TOML multi-line string",
failurePhase: "merge",
})
);
expect(msg).to.match(/failed to merge the runtime constraints/i);
});

it("reassures nothing changed when diskMutated is false", () => {
const msg = getPythonSetupErrorMessage(
failure("E_FETCH", {diskMutated: false, failurePhase: "fetch"})
Expand Down Expand Up @@ -388,6 +423,20 @@ describe("getPythonSetupErrorAction", () => {
});
});

it("offers no remediation button for a [project]-less E_MERGE (the fix is a manual edit)", () => {
// Option A: the primary fix (add a [project] table) is a manual edit with
// no one-click command, so this variant carries no button — its guidance
// lives in the message and the output-channel detail instead.
expect(
getPythonSetupErrorAction(
failure("E_MERGE", {
message: NO_PROJECT_TABLE_CLI_MSG,
failurePhase: "merge",
})
)
).to.equal(undefined);
});

it("offers no action for codes with no clear remediation doc", () => {
for (const code of [
"E_USAGE",
Expand Down Expand Up @@ -541,6 +590,33 @@ describe("formatSetupFailureDetail", () => {
expect(detail).to.not.contain("databricks.python.environmentSetup");
});

it("spells out the [project]-table + manual-mode fixes for a [project]-less E_MERGE", () => {
const detail = formatSetupFailureDetail(
failure("E_MERGE", {
message: NO_PROJECT_TABLE_CLI_MSG,
failurePhase: "merge",
})
);
// Still carries the raw CLI error …
expect(detail).to.contain("requires-python");
// … plus both remediation paths (a concrete [project] table + manual mode).
expect(detail).to.contain("[project]");
expect(detail).to.contain("databricks.python.environmentSetup");
expect(detail).to.match(/manual/i);
// This variant has no button (Option A), so no "label: undefined" line.
expect(detail).to.not.contain("undefined");
});

it("adds no [project]-table block for a generic E_MERGE", () => {
const detail = formatSetupFailureDetail(
failure("E_MERGE", {
message: "cannot merge: unsupported TOML multi-line string",
})
);
expect(detail).to.not.match(/add a minimal \[project\] table/i);
expect(detail).to.not.contain("databricks.python.environmentSetup");
});

it("adds no remediation block for a non-connectivity E_PROVISION", () => {
const detail = formatSetupFailureDetail(
failure("E_PROVISION", {
Expand Down Expand Up @@ -784,3 +860,52 @@ describe("isIndexUnreachableFailure", () => {
expect(isIndexUnreachableFailure(ok)).to.equal(false);
});
});

describe("isMissingProjectTableFailure", () => {
it("is true for E_MERGE whose message names a missing [project] table", () => {
expect(
isMissingProjectTableFailure(
failure("E_MERGE", {message: NO_PROJECT_TABLE_CLI_MSG})
)
).to.equal(true);
});

it("is false for a generic E_MERGE (e.g. a multiline-string rejection)", () => {
expect(
isMissingProjectTableFailure(
failure("E_MERGE", {
message: "cannot merge: unsupported TOML multi-line string",
})
)
).to.equal(false);
});

it("is false for an E_MERGE that references a [project] table without saying there is none", () => {
// Guards the match against a hypothetical future merge defect whose
// message merely mentions the [project] table (e.g. an invalid value in
// it): that is a real bug and must keep its generic copy and bug-report
// prompt, not be relabeled as the user-fixable missing-table case.
expect(
isMissingProjectTableFailure(
failure("E_MERGE", {
message: "failed to rewrite [project] table: invalid TOML",
})
)
).to.equal(false);
});

it("is false for a non-E_MERGE code even with a matching message", () => {
// Scoped to E_MERGE: only the merge phase produces errNoProjectTable.
expect(
isMissingProjectTableFailure(
failure("E_WRITE", {message: NO_PROJECT_TABLE_CLI_MSG})
)
).to.equal(false);
});

it("is false when there is no error object", () => {
const ok = failure("E_MERGE");
ok.error = null;
expect(isMissingProjectTableFailure(ok)).to.equal(false);
});
});
70 changes: 65 additions & 5 deletions packages/databricks-vscode/src/python-setup/utils/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ export function isIndexUnreachableFailure(result: PythonSetupResult): boolean {
return INDEX_CONNECTIVITY_SYMPTOMS.some((s) => msg.includes(s));
}

/**
* True when an E_MERGE failure is the CLI refusing to write requires-python
* because the pyproject.toml has no `[project]` table (libs/localenv's
* `errNoProjectTable`) — a valid, user-fixable manifest shape (a PEP 735
* dependency-groups-only file), not a merge defect. It gets actionable copy and
* is excluded from the report-a-bug routing, while a generic E_MERGE is
* untouched. The CLI emits no distinct code, so we read its message (mirroring
* {@link isIndexUnreachableFailure}); matching `no [project] table` — not the
* bare phrase — keeps a future merge bug that merely mentions the table from
* being misclassified as this user-fixable case.
*/
export function isMissingProjectTableFailure(
result: PythonSetupResult
): boolean {
const err = result.error;
if (!err || err.code !== "E_MERGE") {
return false;
}
return (err.message?.toLowerCase() ?? "").includes("no [project] table");
}

/**
* Command that flips `databricks.python.environmentSetup` to `manual` for the
* current project. Surfaced as the E_FETCH remediation button so a user whose
Expand Down Expand Up @@ -217,16 +238,36 @@ const INDEX_UNREACHABLE_MESSAGE =
"environment variable, or add an index-url to your pip config), then try " +
"again. See the logs for details.";

/**
* Popup copy for the `[project]`-less variant of E_MERGE, replacing the generic
* "failed to merge" text. The primary fix is a manual edit (add a `[project]`
* table), so there is no remediation button; {@link formatSetupFailureDetail}
* spells out both fixes for the log.
*/
const MISSING_PROJECT_TABLE_MESSAGE =
"Your pyproject.toml has no [project] table, so setup can't record the " +
"runtime's required Python version (requires-python) there. Add a minimal " +
"[project] table (a name and version) and setup will fill it in — or set " +
'"databricks.python.environmentSetup" to "manual" to skip automated setup ' +
"and use your existing environment as-is.";

export function getPythonSetupErrorMessage(result: PythonSetupResult): string {
const err = result.error;
if (!err) {
return GENERIC;
}
// Checked before the per-code map: a blocked index arrives as E_PROVISION,
// whose generic "dependency conflict" copy points at the wrong cause.
const base = isIndexUnreachableFailure(result)
? INDEX_UNREACHABLE_MESSAGE
: BASE_MESSAGE[err.code]?.(result) ?? GENERIC;
// Checked before the per-code map: both a blocked index (arriving as
// E_PROVISION) and a [project]-less pyproject (arriving as E_MERGE) are told
// apart by the CLI's message, not a distinct code, and their per-code copy
// would misdirect.
let base: string;
if (isIndexUnreachableFailure(result)) {
base = INDEX_UNREACHABLE_MESSAGE;
} else if (isMissingProjectTableFailure(result)) {
base = MISSING_PROJECT_TABLE_MESSAGE;
} else {
base = BASE_MESSAGE[err.code]?.(result) ?? GENERIC;
}
return base + diskStateSuffix(result, err);
}

Expand Down Expand Up @@ -403,6 +444,25 @@ export function formatSetupFailureDetail(
'setting to "manual". The extension then uses your existing interpreter/.venv (with its databricks-connect) as-is.'
);
}
// The [project]-less variant of E_MERGE: spell out both fixes here so they
// survive the notification being dismissed. The popup only summarises them.
if (isMissingProjectTableFailure(result)) {
lines.push(
"",
"Automated setup writes the runtime's required Python version into " +
"your pyproject.toml's [project] table, but this file has none (a " +
"valid dependency-groups-only manifest). You have two options:",
"",
" 1. Add a minimal [project] table so the pin has a home, for example:",
" [project]",
' name = "my-project"',
' version = "0.0.0"',
" Setup fills in requires-python for you — you don't need to set it.",
"",
' 2. Or skip automated setup and manage the environment yourself: set the "databricks.python.environmentSetup" ' +
'setting to "manual". The extension then uses your existing interpreter/.venv as-is.'
);
}
// A genuine E_PROVISION conflict gets no report button (it is usually the
// user's own dependencies). But if the *published constraints* are what
// conflict, that is a defect worth reporting — so offer a soft, conditional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const INDEX_UNREACHABLE_CLI_MSG =
"error: Failed to fetch: `https://pypi.org/simple/ipykernel/`\n" +
" Caused by: tcp connect error: Connection refused (os error 61)";

/**
* The CLI text for the errNoProjectTable variant of E_MERGE — a valid
* dependency-groups-only pyproject with no [project] table. Used to prove this
* user-fixable manifest shape is NOT report-worthy, unlike a generic E_MERGE.
*/
const NO_PROJECT_TABLE_CLI_MSG =
"merge managed regions failed: pyproject.toml has no [project] table to hold requires-python";

/** Build a minimal failed result carrying a specific error. */
function failure(
code: PythonSetupErrorCode,
Expand Down Expand Up @@ -99,6 +107,19 @@ describe("reportRepoForResult / isReportWorthy", () => {
expect(isReportWorthy(r)).to.equal(false);
});

it("does NOT treat a [project]-less E_MERGE as report-worthy", () => {
// A valid PEP 735 dependency-groups-only manifest is user-fixable, not a
// merge bug (errorMessages gives actionable copy), so it must not prompt a
// bug report — that mis-prompt is what auto-filed issue #2177. Generic
// E_MERGE (above) stays report-worthy.
const r = failure("E_MERGE", {
message: NO_PROJECT_TABLE_CLI_MSG,
failurePhase: "merge",
});
expect(reportRepoForResult(r)).to.equal(undefined);
expect(isReportWorthy(r)).to.equal(false);
});

it("does NOT offer a report for preflight/local/network codes", () => {
for (const code of [
"E_USAGE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
PythonSetupErrorCode,
} from "../models/PythonSetupResult";
import type {PrimaryManager} from "../../language/packageManagerDetection";
import {isMissingProjectTableFailure} from "./errorMessages";
import type {PythonSetupErrorAction} from "./errorMessages";

/**
Expand All @@ -27,7 +28,9 @@ export type ReportRepo =
* constraints have no entry for the runtime (`E_ENV_UNSUPPORTED`) or don't
* validate after provisioning (`E_VALIDATE`).
* - Extension/CLI behaviour defects → `databricks/databricks-vscode`: merging
* into or writing the user's pyproject.toml broke.
* into or writing the user's pyproject.toml broke. Exception: the
* `[project]`-less variant of `E_MERGE` is a valid, user-fixable manifest
* shape, not a defect — `reportRepoForResult` filters it out (see there).
*
* `E_PROVISION` is deliberately absent: a uv resolution conflict is usually the
* user's own declared dependencies (possibly private packages), not a constraint
Expand Down Expand Up @@ -77,6 +80,13 @@ export function reportRepoForResult(
if (!err) {
return undefined;
}
// The [project]-less variant of E_MERGE is a valid, user-fixable manifest
// shape (errorMessages gives it actionable copy), not a merge defect — so it
// is not report-worthy, even though a generic E_MERGE is. Prompting a bug
// report for it is what auto-filed databricks/databricks-vscode#2177.
if (isMissingProjectTableFailure(result)) {
return undefined;
}
return REPORT_ROUTING[err.code];
}

Expand Down
Loading