Repository: zed-industries/zed
Pull request #31339: debugger: Fix install detection for debugpy
Author: login: raphaelluethy
Labels: name: cla-signed
description: The user has signed the Contributor License Agreement
Created: 2025-05-24T15:05:51Z | Merged: 2025-05-27T09:45:55Z

Closes: #31383 

Based on my report on discord when chatting with Anthony and Remco: https://discord.com/channels/869392257814519848/1375129714645012530

Root Cause: Zed was incorrectly trying to execute a directory path instead of properly invoking the debugpy module when debugpy was installed via package managers (pip, conda, etc.) rather than downloaded from GitHub releases.

Solution:

- Automatic Detection: Zed now automatically detects whether debugpy is installed via pip/conda or downloaded from GitHub
- Correct Invocation: For pip-installed debugpy, Zed now uses python -m debugpy.adapter instead of trying to execute file paths
- Added a `installed_in_venv` flag to differentiate the setup properly
- Backward Compatibility: GitHub-downloaded debugpy releases continue to work as before
- Enhanced Logging: Added logging to show which debugpy installation method is being used (I had to verify it somehow)

I verified with the following setups (can be confirmed with the debug logs):
- `conda` with installed debugpy, went to installed instance
- `uv` with installed debugpy, went to installed instance
- `uv` without installed debugpy, went to github releases
- Homebrew global python install, went to github releases

Release Notes:

- Fix issue where debugpy from different environments won't load as intended



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 310c9d1fdd4b65..22ae71839bd42e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4056,10 +4056,10 @@ dependencies = [
  "gpui",
  "json_dotpath",
  "language",
+ "log",
  "paths",
  "serde",
  "serde_json",
- "smol",
  "task",
  "util",
  "workspace-hack",
diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml
index 9eafb6ef407426..f669d781cddb15 100644
--- a/crates/dap_adapters/Cargo.toml
+++ b/crates/dap_adapters/Cargo.toml
@@ -28,10 +28,10 @@ futures.workspace = true
 gpui.workspace = true
 json_dotpath.workspace = true
 language.workspace = true
+log.workspace = true
 paths.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-smol.workspace = true
 task.workspace = true
 util.workspace = true
 workspace-hack.workspace = true
diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs
index 009a05938d796f..5213829f4f717e 100644
--- a/crates/dap_adapters/src/python.rs
+++ b/crates/dap_adapters/src/python.rs
@@ -8,6 +8,7 @@ use gpui::{AsyncApp, SharedString};
 use json_dotpath::DotPaths;
 use language::{LanguageName, Toolchain};
 use serde_json::Value;
+use std::net::Ipv4Addr;
 use std::{
     collections::HashMap,
     ffi::OsStr,
@@ -27,6 +28,60 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    async fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+    ) -> Result<Vec<String>> {
+        if let Some(user_installed_path) = user_installed_path {
+            log::debug!(
+                "Using user-installed debugpy adapter from: {}",
+                user_installed_path.display()
+            );
+            Ok(vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ])
+        } else if installed_in_venv {
+            log::debug!("Using venv-installed debugpy");
+            Ok(vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ])
+        } else {
+            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
+            let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
+
+            let debugpy_dir =
+                util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
+                    file_name.starts_with(&file_name_prefix)
+                })
+                .await
+                .context("Debugpy directory not found")?;
+
+            log::debug!(
+                "Using GitHub-downloaded debugpy adapter from: {}",
+                debugpy_dir.display()
+            );
+            Ok(vec![
+                debugpy_dir
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ])
+        }
+    }
+
     fn request_args(
         &self,
         task_definition: &DebugTaskDefinition,
@@ -93,24 +148,12 @@ impl PythonDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         toolchain: Option<Toolchain>,
+        installed_in_venv: bool,
     ) -> Result<DebugAdapterBinary> {
         const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
         let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
-        let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
-            user_installed_path
-        } else {
-            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
-            let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
-
-            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
-                file_name.starts_with(&file_name_prefix)
-            })
-            .await
-            .context("Debugpy directory not found")?
-        };
-
         let python_path = if let Some(toolchain) = toolchain {
             Some(toolchain.path.to_string())
         } else {
@@ -128,16 +171,27 @@ impl PythonDebugAdapter {
             name
         };
 
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::debug!("Using Python executable: {}", python_command);
+
+        let arguments = self
+            .generate_debugpy_arguments(
+                &host,
+                port,
+                user_installed_path.as_deref(),
+                installed_in_venv,
+            )
+            .await?;
+
+        log::debug!(
+            "Starting debugpy adapter with command: {} {}",
+            python_command,
+            arguments.join(" ")
+        );
+
         Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
-                debugpy_dir
-                    .join(Self::ADAPTER_PATH)
-                    .to_string_lossy()
-                    .to_string(),
-                format!("--port={}", port),
-                format!("--host={}", host),
-            ],
+            command: python_command,
+            arguments,
             connection: Some(adapters::TcpArguments {
                 host,
                 port,
@@ -558,6 +612,16 @@ impl DebugAdapter for PythonDebugAdapter {
         user_installed_path: Option<PathBuf>,
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
+        if let Some(local_path) = &user_installed_path {
+            log::debug!(
+                "Using user-installed debugpy adapter from: {}",
+                local_path.display()
+            );
+            return self
+                .get_installed_binary(delegate, &config, Some(local_path.clone()), None, false)
+                .await;
+        }
+
         let toolchain = delegate
             .toolchain_store()
             .active_toolchain(
@@ -571,13 +635,18 @@ impl DebugAdapter for PythonDebugAdapter {
         if let Some(toolchain) = &toolchain {
             if let Some(path) = Path::new(&toolchain.path.to_string()).parent() {
                 let debugpy_path = path.join("debugpy");
-                if smol::fs::metadata(&debugpy_path).await.is_ok() {
+                if delegate.fs().is_file(&debugpy_path).await {
+                    log::debug!(
+                        "Found debugpy in toolchain environment: {}",
+                        debugpy_path.display()
+                    );
                     return self
                         .get_installed_binary(
                             delegate,
                             &config,
-                            Some(debugpy_path.to_path_buf()),
+                            None,
                             Some(toolchain.clone()),
+                            true,
                         )
                         .await;
                 }
@@ -591,7 +660,49 @@ impl DebugAdapter for PythonDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, &config, user_installed_path, toolchain)
+        self.get_installed_binary(delegate, &config, None, None, false)
+            .await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::{net::Ipv4Addr, path::PathBuf};
+
+    #[gpui::test]
+    async fn test_debugpy_install_path_cases() {
+        let adapter = PythonDebugAdapter::default();
+        let host = Ipv4Addr::new(127, 0, 0, 1);
+        let port = 5678;
+
+        // Case 1: User-defined debugpy path (highest precedence)
+        let user_path = PathBuf::from("/custom/path/to/debugpy");
+        let user_args = adapter
+            .generate_debugpy_arguments(&host, port, Some(&user_path), false)
             .await
+            .unwrap();
+
+        // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
+        let venv_args = adapter
+            .generate_debugpy_arguments(&host, port, None, true)
+            .await
+            .unwrap();
+
+        assert!(user_args[0].ends_with("src/debugpy/adapter"));
+        assert_eq!(user_args[1], "--host=127.0.0.1");
+        assert_eq!(user_args[2], "--port=5678");
+
+        assert_eq!(venv_args[0], "-m");
+        assert_eq!(venv_args[1], "debugpy.adapter");
+        assert_eq!(venv_args[2], "--host=127.0.0.1");
+        assert_eq!(venv_args[3], "--port=5678");
+
+        // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
+    }
+
+    #[test]
+    fn test_adapter_path_constant() {
+        assert_eq!(PythonDebugAdapter::ADAPTER_PATH, "src/debugpy/adapter");
     }
 }


Reviews:
user: login: RemcoSmitsDev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T15:22:37Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T16:14:03Z
user: login: RemcoSmitsDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T16:23:16Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T16:49:32Z
user: login: RemcoSmitsDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T17:21:12Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T17:39:15Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T17:44:34Z
user: login: RemcoSmitsDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T17:50:56Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T20:31:00Z
user: login: Anthony-Eid
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T20:38:26Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T21:37:43Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T21:40:43Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T21:45:19Z
user: login: raphaelluethy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T21:46:28Z
user: login: Anthony-Eid
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T09:45:54Z

Review comments:
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: RemcoSmitsDev
body: I think this assumption is not right, because I can have debugpy locally installed without it being installed via a package manager (so not in my venv). Maybe passing in a bool flag that we know it's installed via package manager is better.
created_at: 2025-05-24T15:20:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
+                .file_name()
+                .and_then(|name| name.to_str())
+                == Some("debugpy")
+            {
+                log::info!(
+                    "Using pip-installed debugpy from: {}",
+                    user_installed_path.display()
+                );
+                vec![
+                    "-m".to_string(),
+                    "debugpy.adapter".to_string(),
+                    "--host".to_string(),
+                    host.to_string(),
path: crates/dap_adapters/src/python.rs
user: login: RemcoSmitsDev
body: Just a nit, but can have the same style as we do for the fallback.
`format!("--port={}", port)`
created_at: 2025-05-24T15:21:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: So would you like this to be a setting? Or should we just use the `debugAdapterPath` variable?
created_at: 2025-05-24T16:14:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: RemcoSmitsDev
body: So my idea was instead of passing in the venv debugpy path (..venv/bin/debugpy) inside the `get_installed_binary` method we could pass in a boolean when we detected that debugpy is installed inside the user's venv. So we could add a second parameter to the `get_installed_binary` method.

So instead of checking `if let some(...) = user_installed_path` we should do `if installed_in_venv` and do the `debugpy.adapter` code path. And if it's false we can check for the old code path with the user_installed_path variable and if that does not exist we can do the download from GitHub code.
created_at: 2025-05-24T16:23:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: I added the argument, please have a look :)
created_at: 2025-05-24T16:49:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: RemcoSmitsDev
body: Sorry, but I wrote down the wrong order. We should do the `user_installed_path` check first so you can always override it.
created_at: 2025-05-24T17:21:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,16 +115,75 @@ impl PythonDebugAdapter {
             name
         };
 
-        Ok(DebugAdapterBinary {
-            command: python_path.context("failed to find binary path for Python")?,
-            arguments: vec![
+        let python_command = python_path.context("failed to find binary path for Python")?;
+        log::info!("Using Python executable: {}", python_command);
+
+        let arguments = if let Some(user_installed_path) = user_installed_path {
+            if user_installed_path
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: I extracted the arguments builder and added tests for it
created_at: 2025-05-24T17:39:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,45 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+        github_adapter_path: Option<&Path>,
+    ) -> Vec<String> {
+        if installed_in_venv {
+            vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ]
+        } else if let Some(user_installed_path) = user_installed_path {
+            vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else if let Some(github_path) = github_adapter_path {
+            vec![
+                github_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else {
+            // This shouldn't happen in practice, but provide a fallback
+            vec![format!("--port={}", port), format!("--host={}", host)]
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: We might need to handle this error since this should never happen, or do we just leave it as is?
created_at: 2025-05-24T17:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,45 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+        github_adapter_path: Option<&Path>,
+    ) -> Vec<String> {
+        if installed_in_venv {
+            vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ]
+        } else if let Some(user_installed_path) = user_installed_path {
+            vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else if let Some(github_path) = github_adapter_path {
+            vec![
+                github_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else {
+            // This shouldn't happen in practice, but provide a fallback
+            vec![format!("--port={}", port), format!("--host={}", host)]
path: crates/dap_adapters/src/python.rs
user: login: RemcoSmitsDev
body: I think we can just error out here
created_at: 2025-05-24T17:50:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,60 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    async fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+    ) -> Result<Vec<String>> {
+        if installed_in_venv {
+            log::debug!("Using venv-installed debugpy");
+            Ok(vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ])
+        } else if let Some(user_installed_path) = user_installed_path {
path: crates/dap_adapters/src/python.rs
user: login: Anthony-Eid
body: Because `user_installed_path` is a setting I take it should take precedence over a virtual environment installation. Do you have strong feelings about checking for venv first?

Edit: 

Based on your test and the callers of this function `user_installed_path` does take precedence over `installed_in_venv`. However, I still think you should change the order of the if statements to increase readability and in case this function is called from someone else in the future. 
created_at: 2025-05-24T20:03:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,12 +636,17 @@ impl DebugAdapter for PythonDebugAdapter {
             if let Some(path) = Path::new(&toolchain.path.to_string()).parent() {
                 let debugpy_path = path.join("debugpy");
                 if smol::fs::metadata(&debugpy_path).await.is_ok() {
path: crates/dap_adapters/src/python.rs
user: login: Anthony-Eid
body: ```suggestion
                if delegate.fs().is_file(&debugpy_path).await {
```

We have a `Fs` trait that is usually accessible from `AppState`, which is on `Workspace` among other types. 

If you need to do file system operations, it's a good idea to see if the `Fs` trait has a method that does what you need. It's more idiomatic and more readable (in this case).
created_at: 2025-05-24T20:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -591,7 +660,52 @@ impl DebugAdapter for PythonDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, &config, user_installed_path, toolchain)
+        self.get_installed_binary(delegate, &config, None, None, false)
+            .await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::{net::Ipv4Addr, path::PathBuf};
+
+    #[gpui::test]
+    async fn test_debugpy_install_path_cases() {
+        let adapter = PythonDebugAdapter::default();
+        let host = Ipv4Addr::new(127, 0, 0, 1);
+        let port = 5678;
+
+        // Case 1: User-defined debugpy path (highest precedence)
+        let user_path = PathBuf::from("/custom/path/to/debugpy");
+        let user_args = adapter
+            .generate_debugpy_arguments(&host, port, Some(&user_path), false)
             .await
+            .unwrap();
+
+        // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
+        let venv_args = adapter
+            .generate_debugpy_arguments(&host, port, None, true)
+            .await
+            .unwrap();
+
+        assert!(user_args[0].ends_with("src/debugpy/adapter"));
+        assert_eq!(user_args[1], "--host=127.0.0.1");
+        assert_eq!(user_args[2], "--port=5678");
+
+        assert_eq!(venv_args[0], "-m");
+        assert_eq!(venv_args[1], "debugpy.adapter");
+        assert_eq!(venv_args[2], "--host=127.0.0.1");
+        assert_eq!(venv_args[3], "--port=5678");
+
+        // Note: Case 3 (GitHub-downloaded debugpy) is now handled internally
+        // by the generate_debugpy_arguments function when neither user_installed_path
+        // nor installed_in_venv are provided, so we don't test it separately here
+        // as it would require setting up the actual file system structure.
path: crates/dap_adapters/src/python.rs
user: login: Anthony-Eid
body: Just for future noticed we have a `FakeFs` struct that's used for tests. I don't expected you to add that edge case to merge this PR though. I don't think it would be a good idea that would rely on a GitHub API to pass or mocking the behavior we would be testing. 
created_at: 2025-05-24T20:16:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,45 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+        github_adapter_path: Option<&Path>,
+    ) -> Vec<String> {
+        if installed_in_venv {
+            vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ]
+        } else if let Some(user_installed_path) = user_installed_path {
+            vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else if let Some(github_path) = github_adapter_path {
+            vec![
+                github_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else {
+            // This shouldn't happen in practice, but provide a fallback
path: crates/dap_adapters/src/python.rs
user: login: Anthony-Eid
body: ```suggestion
            debug_panic!("Failed to download/get debugpy from venv, user installed path, and github")
```

This will only panic in debug builds and log an error message in production builds. I like using these panics a lot because it's a great way to find bugs and check that the code's logic is correct.
created_at: 2025-05-24T20:27:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,45 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+        github_adapter_path: Option<&Path>,
+    ) -> Vec<String> {
+        if installed_in_venv {
+            vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ]
+        } else if let Some(user_installed_path) = user_installed_path {
+            vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else if let Some(github_path) = github_adapter_path {
+            vec![
+                github_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else {
+            // This shouldn't happen in practice, but provide a fallback
+            vec![format!("--port={}", port), format!("--host={}", host)]
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: should be resolved
created_at: 2025-05-24T20:31:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,60 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    async fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+    ) -> Result<Vec<String>> {
+        if installed_in_venv {
+            log::debug!("Using venv-installed debugpy");
+            Ok(vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ])
+        } else if let Some(user_installed_path) = user_installed_path {
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: I had the venv one first in order to reduce the work being done with checking the options. I have changed it to represent the actual workflow.
created_at: 2025-05-24T21:37:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -572,12 +636,17 @@ impl DebugAdapter for PythonDebugAdapter {
             if let Some(path) = Path::new(&toolchain.path.to_string()).parent() {
                 let debugpy_path = path.join("debugpy");
                 if smol::fs::metadata(&debugpy_path).await.is_ok() {
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: done 👍
created_at: 2025-05-24T21:40:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -591,7 +660,52 @@ impl DebugAdapter for PythonDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, &config, user_installed_path, toolchain)
+        self.get_installed_binary(delegate, &config, None, None, false)
+            .await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::{net::Ipv4Addr, path::PathBuf};
+
+    #[gpui::test]
+    async fn test_debugpy_install_path_cases() {
+        let adapter = PythonDebugAdapter::default();
+        let host = Ipv4Addr::new(127, 0, 0, 1);
+        let port = 5678;
+
+        // Case 1: User-defined debugpy path (highest precedence)
+        let user_path = PathBuf::from("/custom/path/to/debugpy");
+        let user_args = adapter
+            .generate_debugpy_arguments(&host, port, Some(&user_path), false)
             .await
+            .unwrap();
+
+        // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
+        let venv_args = adapter
+            .generate_debugpy_arguments(&host, port, None, true)
+            .await
+            .unwrap();
+
+        assert!(user_args[0].ends_with("src/debugpy/adapter"));
+        assert_eq!(user_args[1], "--host=127.0.0.1");
+        assert_eq!(user_args[2], "--port=5678");
+
+        assert_eq!(venv_args[0], "-m");
+        assert_eq!(venv_args[1], "debugpy.adapter");
+        assert_eq!(venv_args[2], "--host=127.0.0.1");
+        assert_eq!(venv_args[3], "--port=5678");
+
+        // Note: Case 3 (GitHub-downloaded debugpy) is now handled internally
+        // by the generate_debugpy_arguments function when neither user_installed_path
+        // nor installed_in_venv are provided, so we don't test it separately here
+        // as it would require setting up the actual file system structure.
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: I updated the comment 👍
created_at: 2025-05-24T21:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +28,45 @@ impl PythonDebugAdapter {
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
+    fn generate_debugpy_arguments(
+        &self,
+        host: &Ipv4Addr,
+        port: u16,
+        user_installed_path: Option<&Path>,
+        installed_in_venv: bool,
+        github_adapter_path: Option<&Path>,
+    ) -> Vec<String> {
+        if installed_in_venv {
+            vec![
+                "-m".to_string(),
+                "debugpy.adapter".to_string(),
+                format!("--host={}", host),
+                format!("--port={}", port),
+            ]
+        } else if let Some(user_installed_path) = user_installed_path {
+            vec![
+                user_installed_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else if let Some(github_path) = github_adapter_path {
+            vec![
+                github_path
+                    .join(Self::ADAPTER_PATH)
+                    .to_string_lossy()
+                    .to_string(),
+                format!("--port={}", port),
+                format!("--host={}", host),
+            ]
+        } else {
+            // This shouldn't happen in practice, but provide a fallback
path: crates/dap_adapters/src/python.rs
user: login: raphaelluethy
body: This not necessary anymore since the fallback is the github download. I changed this when extracting the if statement to the new function.
created_at: 2025-05-24T21:46:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: Anthony-Eid
created_at: 2025-05-24T20:21:01Z
body: @raphaelluethy Thank you so much for fixing this bug! This PR is looking really good so far, but there are some small changes I think we could make to further improve it. I can push this PR across the finish line on Tuesday if you don't have time to make the changes.  Otherwise, I'll be happy to merge it once the feedback from me and Remco has been addressed.

@RemcoSmitsDev Thank you for taking the initiative to start a PR review! You did a fantastic job, and I really appreciate all your help. 
author_association: CONTRIBUTOR
user: login: raphaelluethy
created_at: 2025-05-24T21:51:48Z
body: > @raphaelluethy Thank you so much for fixing this bug! This PR is looking really good so far, but there are some small changes I think we could make to further improve it. I can push this PR across the finish line on Tuesday if you don't have time to make the changes. Otherwise, I'll be happy to merge it once the feedback from me and Remco has been addressed.
> 
> @RemcoSmitsDev Thank you for taking the initiative to start a PR review! You did a fantastic job, and I really appreciate all your help.

I think I addressed everything except the case 3 test with github, but I added a comment there.
Thank you both for your help! Hopefully this can be shipped soon :)
author_association: CONTRIBUTOR
user: login: Anthony-Eid
created_at: 2025-05-27T09:46:27Z
body: Thank you for working on this!
author_association: CONTRIBUTOR