PY-91265 [debugger]: Activate the virtualenv for the debugged process

The debugpy/DAP backend does not launch the debuggee through
PythonCommandLineState.startProcess: DebugpyConfigProvider.createConfig
assembles the environment from scratch and serializes it into the DAP
launch request. Compared with PythonCommandLineState.prepareEnv it was
missing setupVirtualEnvVariables (VIRTUAL_ENV and the venv-prefixed
PATH) and addCommonEnvironmentVariables (PYCHARM_HOSTED,
PYTHONUNBUFFERED, resetHomePathChanges), so a debugged process ran
without its virtualenv activated and subprocess calls to tools installed
in the venv failed with FileNotFoundError. The pydevd backend was
unaffected because it goes through PythonCommandLineState.execute.

Extract those two steps into
PythonCommandLineState.addCommonAndVirtualEnvVariables, along with the
PYCHARM_HOSTED decision inlined in initEnvironment
(shouldAddPyCharmHosted), and call them from the default createConfig at
the same ordering point, so run-configuration envs still win over the
activated ones and PATH is prefix-merged. Placing the call in the
interface default means every provider inherits it.

Add PyDebuggerVirtualEnvTest, which runs against both backends and so
asserts the parity directly.

IJ-MR-216263


(cherry picked from commit 21b59a59a1a1f9550c68e4c1d61d4e27a376ead2)

IJ-MR-216551

GitOrigin-RevId: 81080faed737880d84ffdab470791be629a293b0
This commit is contained in:
Maxim Popov
2026-07-31 20:58:23 +00:00
committed by intellij-monorepo-bot
parent 44843e443b
commit 0b269a2edd
@@ -743,11 +743,7 @@ public abstract class PythonCommandLineState extends CommandLineState {
boolean isDebug,
@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest,
@Nullable Sdk sdk) {
boolean addPyCharmHosted = true;
if (sdk != null && !CondaPythonExecKt.getUsePythonForLocalConda()) {
addPyCharmHosted = PySdkExtKt.getOrCreateAdditionalData(sdk).getFlavor().providePyCharmHosted();
}
final Map<String, String> env = prepareEnv(project, runParams, addPyCharmHosted);
final Map<String, String> env = prepareEnv(project, runParams, shouldAddPyCharmHosted(sdk));
setupEncodingEnvs(commandLine, commandLine.getCharset());
@@ -772,12 +768,40 @@ public abstract class PythonCommandLineState extends CommandLineState {
if (runParams.getEnvs() != null) {
env.putAll(runParams.getEnvs());
}
addCommonEnvironmentVariables(getInterpreterPath(project, runParams), env, addPyCharmHosted);
setupVirtualEnvVariables(runParams, env);
addCommonAndVirtualEnvVariables(project, runParams, env, addPyCharmHosted);
return env;
}
/**
* @see PythonSdkFlavor#providePyCharmHosted()
*/
@ApiStatus.Internal
public static boolean shouldAddPyCharmHosted(@Nullable Sdk sdk) {
if (sdk == null || CondaPythonExecKt.getUsePythonForLocalConda()) return true;
return PySdkExtKt.getOrCreateAdditionalData(sdk).getFlavor().providePyCharmHosted();
}
/**
* Adds the environment variables every launched Python process is expected to have: the common ones
* ({@code PYTHONUNBUFFERED}, {@code PYCHARM_HOSTED}, …) and the ones produced by activating the
* virtualenv of {@link PythonRunParams#getSdk()}.
* <p>
* {@code env} is expected to already contain the environment variables from the run configuration
* (env files and {@link PythonRunParams#getEnvs()}), because the user-specified ones win over the
* activated ones.
* <p>
* Meant for launchers that don't build their command line through {@link #startProcess}, most notably
* the debugpy DAP backend, which passes the environment to the debug adapter instead.
*/
@ApiStatus.Internal
public static void addCommonAndVirtualEnvVariables(@NotNull Project project,
@NotNull PythonRunParams runParams,
@NotNull Map<String, String> env,
boolean addPyCharmHosted) {
addCommonEnvironmentVariables(getInterpreterPath(project, runParams), env, addPyCharmHosted);
setupVirtualEnvVariables(runParams, env);
}
private static void setupVirtualEnvVariables(PythonRunParams runParams, Map<String, String> env) {
Sdk sdk = runParams.getSdk();
if (sdk == null) return;