Skip to content

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

Open
Han (LH-and-FPGA) wants to merge 5 commits 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 5 commits 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
Member

Choose a reason for hiding this comment

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

I think these need to remain two distinct states, but not necessarily as two new public flags. "" is already the generic "version metadata unavailable" value: tryResolveInterpreterPath creates a runnable environment with version: resolved.version ?? "" and a real executable. If that resolves to the Conda manager, this predicate sends an already-runnable interpreter through the install-Python flow and can reject it as the base fallback. Please classify no-interpreter independently—e.g. through the existing error/capability representation—and add a regression test with a valid executable plus an empty 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.

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.

@edvilme

Copy link
Copy Markdown
Contributor

Hello Han (@LH-and-FPGA)
Thanks for your contribution. Since your last commit, we have introduced PythonVersion and PythonVersionSpecifier for parsing and comparing Python versions, which relates to your changes. Please solve the merge conflicts and we can make another review. Thx!

Use upstream PythonVersion sorting and latest-version selection while retaining the Conda no-Python fix. Combine upstream version tests with empty-version and discovery-order regressions.
@LH-and-FPGA

Copy link
Copy Markdown
Author

Hello Eduardo Villalpando Mello (@edvilme). Thank you for your message, I've updated my branch and resolved the conflicts.

Copilot AI 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.

🟡 Changes recommended

Executable-name heuristics can still misclassify valid interpreters with unknown versions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Prevents interpreter-less Conda environments from disrupting environment discovery.

Changes:

  • Reports missing Python versions as empty strings.
  • Improves unknown-version sorting and latest selection.
  • Adds and updates Conda regression tests and mocks.
File summaries
File Description
src/managers/conda/condaUtils.ts Adds missing-interpreter detection and empty versions.
src/managers/conda/condaEnvManager.ts Avoids selecting interpreter-less base environments.
src/managers/common/utils.ts Handles invalid or missing versions deterministically.
src/test/mocks/pythonEnvironment.ts Adds an interpreter-less Conda mock.
src/test/managers/conda/condaUtils.noPythonEnv.unit.test.ts Tests missing and unknown interpreter versions.
src/test/managers/conda/condaEnvManager.setGlobal.unit.test.ts Updates global-selection fixture.
src/test/managers/conda/condaEnvManager.initialize.unit.test.ts Updates initialization fixtures.
src/test/managers/common/utils.sortEnvironments.unit.test.ts Tests deterministic ordering.
src/test/managers/common/utils.getLatest.unit.test.ts Tests selection after an unknown version.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +783 to +784
const runner = environment.execInfo?.run?.executable ?? '';
return !PYTHON_EXECUTABLE_NAME.test(path.basename(runner));
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.

5 participants