Skip to content

Report an empty version for conda environments without Python - #1716

Open
Han (LH-and-FPGA) wants to merge 1 commit into
microsoft:mainfrom
LH-and-FPGA:conda-fix
Open

Report an empty version for conda environments without Python#1716
Han (LH-and-FPGA) wants to merge 1 commit into
microsoft:mainfrom
LH-and-FPGA:conda-fix

Conversation

@LH-and-FPGA

Copy link
Copy Markdown

Summary

A conda environment that has no Python interpreter makes all conda environments
disappear from the interpreter picker and the Jupyter kernel picker.

This happens with a prefix created as a toolchain rather than a Python environment,
e.g. conda create -n cuda cuda-toolkit. Reproduced on Linux with 5 conda
environments, one of them interpreter-less; only base survived.

Likely the same root cause as #1584 — that report's environment list shows a third
entry rendered as (no-python), and it was closed as info-needed.

Root cause

getCondaWithoutPython describes an interpreter-less environment with a placeholder
in a field that consumers parse:

// src/managers/conda/condaUtils.ts
version: 'no-python',

version is declared readonly version: string in src/api.ts, so a sentinel
compiles fine — but ms-python.python parses it as a PEP 440 version:

// ms-python.python 2026.7.2026080801, out/client/extension.js
function f(e){ const t = l.parseBasicVersionInfo(`ignored-${e}`);
  if(!t){ if("" === e) return [v(), ""];        // "unknown" — degrades gracefully
          throw Error(`invalid version ${e}`) } }  // 'no-python' lands here

The empty string is the one unparseable value it accepts. Any other placeholder
throws.

Why one bad environment takes down the rest. This repository is careful —
condaUtils.ts wraps each environment in its own try/catch during conversion. But
the sentinel is valid data, so it passes that guard and is published in the batch
fired from condaEnvManager.ts. The consumer has no per-item guard:

onDidChangeEnvironments(e){ e.forEach(e => {  this.addEnv(e.environment) }) }

Array.forEach cannot resume after a throw, so every environment ordered after the
offending one is silently dropped. Observed stack:

at f                              ← throw Error(`invalid version no-python`)
at _                              ← convert PythonEnvironmentInfo
at S.addEnv
at Array.forEach (<anonymous>)    ← no try/catch
at S.onDidChangeEnvironments
at T.fire

The fix

1. Don't publish a sentinel as a version (condaUtils.ts, condaEnvManager.ts)

version: 'no-python'version: ''. This is already how the rest of the repository
represents an unknown version:

  • src/features/interpreterSelection.tsversion: resolved.version ?? ''
  • src/common/inlineScript/interpreter.tsenv.version.length === 0 means not usable

Conda was the only one of the eight managers surfacing a sentinel. The (no-python)
marker stays in displayName / shortDisplayName, which are displayed but never
parsed, so the UI is unchanged. The two internal version === 'no-python' checks now
go through an exported isCondaEnvWithoutPython predicate.

This also fixes a smaller bug: pickPythonVersion builds its list with
.map(e => e.version).filter(Boolean), and 'no-python' is truthy, so "Select the
version of Python to install" offered no-python as a choice.

2. Make sortEnvironments a total order (managers/common/utils.ts)

return a.version ? 1 : -1;

Comparing a real version against an unparseable non-empty one returns 1 in both
directions, which breaks the antisymmetry Array.prototype.sort requires, so the
result is an implementation-defined permutation. Now: valid versions compare
descending, a known version sorts before an unknown one, and two unknowns fall back to
a string comparison.

getLatest had a related gap — seeded with candidates[0], both operands had to parse
for the seed to ever be replaced, so an unparseable seed always won.

Verification

Measured against the real @renovatebot/pep440, on the reported environment set
(base 3.13.13, an interpreter-less prefix, git 3.14.6, lh 3.14.7, vllm 3.14.6):

before after
antisymmetry violations over a 9-version vocabulary 36 0
distinct sort results over 120 input permutations 16 1
getLatest result the interpreter-less env lh
environments published before the consumer throws base only all 5

Tests

  • npm run unittest: 1582 passing, 0 failing (1577 before this change).
  • New utils.sortEnvironments.unit.test.ts — descending order, unknown versions last,
    and stability across all input permutations.
  • New condaUtils.noPythonEnv.unit.test.ts — an interpreter-less prefix is still
    discovered, reports version: '', and keeps its (no-python) display name.
  • Three of the four new tests fail on main and pass with this change; the fourth is a
    baseline that passes on both.
  • Updated the three existing fixtures that constructed environments with 'no-python'.

src/api.ts is unchanged, so no API version bump or changelog entry is needed.

`getCondaWithoutPython` described an interpreter-less conda prefix with
`version: 'no-python'`. `version` is public API and consumers parse it as a
PEP 440 version: `ms-python.python` throws `invalid version no-python` and
consumes the change event with a bare `Array.forEach`, so every environment
ordered after the offending one is dropped — one toolchain-only prefix hides
all conda environments from the interpreter and Jupyter kernel pickers.

Use `''` instead, which is what the rest of the repository already means by
"unknown version" and the only unparseable value `parseVersion` degrades on
rather than throwing. The `(no-python)` marker stays in the display strings,
which are shown but never parsed.

Also make `sortEnvironments` a total order: the `a.version ? 1 : -1` fallback
returned 1 in both directions when comparing a real version against an
unparseable one, leaving the sorted order implementation-defined. `getLatest`
could never replace a seed whose own version did not parse.
@LH-and-FPGA

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@heejaechang

Heejae Chang (heejaechang) commented Aug 19, 2026

Copy link
Copy Markdown

🔒 Automated review in progress — Heejae Chang (@heejaechang) is auto-reviewing this PR.


export function isCondaEnvWithoutPython(environment: PythonEnvironment): boolean {
return environment.version === '';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

version === '' also represents an unknown version generally, so it cannot reliably prove that Python is absent. Preserve a Conda-specific discriminator (or check executable availability), and cover an environment with unavailable version metadata that still has a runnable interpreter.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Do I need to separate "no version" and "no interpreter" into two different flag?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved via Review Center.

@heejaechang Heejae Chang (heejaechang) added the review-auto:approved Automated review: no blocking findings (approval posted). label Aug 19, 2026
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants