From d72904524f413d50d0949cfdd13a079ec012d55b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:18:32 +0000 Subject: [PATCH 01/13] Initial plan From faf5a7bf4ce8637e328587d99fd14890b9321c02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:40:10 +0000 Subject: [PATCH 02/13] Implement .venv lookup in both Python app and AppHost directories Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 41 ++++- .../AddPythonAppTests.cs | 165 ++++++++++++++++++ 2 files changed, 203 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index f4734988f93..96eed88b91d 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -660,9 +660,44 @@ public static IResourceBuilder WithVirtualEnvironment( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(virtualEnvironmentPath); - var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath) - ? virtualEnvironmentPath - : Path.GetFullPath(virtualEnvironmentPath, builder.Resource.WorkingDirectory)); + string resolvedVirtualEnvironmentPath; + if (Path.IsPathRooted(virtualEnvironmentPath)) + { + // If the path is absolute, use it as-is + resolvedVirtualEnvironmentPath = virtualEnvironmentPath; + } + else + { + // For relative paths, check if the virtual environment exists in the app directory first + var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.Resource.WorkingDirectory); + + // Only check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost + // This prevents picking up unrelated .venv directories from test fixtures or other sources + var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.ApplicationBuilder.AppHostDirectory); + + // Check if the app directory is under or near the AppHost directory + var appDirRelativeToAppHost = Path.GetRelativePath(builder.ApplicationBuilder.AppHostDirectory, builder.Resource.WorkingDirectory); + var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && + !Path.IsPathRooted(appDirRelativeToAppHost); + + if (Directory.Exists(appDirVenvPath)) + { + // Use the app directory if it exists there + resolvedVirtualEnvironmentPath = appDirVenvPath; + } + else if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) + { + // Use the AppHost directory if it exists there and the app is nearby + resolvedVirtualEnvironmentPath = appHostDirVenvPath; + } + else + { + // Default to app directory if neither exists (for cases where the venv will be created later) + resolvedVirtualEnvironmentPath = appDirVenvPath; + } + } + + var virtualEnvironment = new VirtualEnvironment(resolvedVirtualEnvironmentPath); // Get the entrypoint annotation to determine how to update the command if (!builder.Resource.TryGetLastAnnotation(out var entrypointAnnotation)) diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index 422bdf45598..fc185528357 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -508,6 +508,171 @@ public async Task WithVirtualEnvironment_CanBeChainedWithOtherExtensions() Assert.Equal("test_value", environmentVariables["TEST_VAR"]); } + [Fact] + public void WithVirtualEnvironment_UsesAppDirectoryWhenVenvExistsThere() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + using var tempAppDir = new TempDirectory(); + + // Create .venv in the app directory + var appVenvPath = Path.Combine(tempAppDir.Path, ".venv"); + Directory.CreateDirectory(appVenvPath); + + var scriptName = "main.py"; + var resourceBuilder = builder.AddPythonScript("pythonProject", tempAppDir.Path, scriptName); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should use the app directory .venv since it exists there + var expectedProjectDirectory = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, tempAppDir.Path)); + var expectedVenvPath = Path.Combine(expectedProjectDirectory, ".venv"); + + if (OperatingSystem.IsWindows()) + { + Assert.Equal(Path.Join(expectedVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); + } + else + { + Assert.Equal(Path.Join(expectedVenvPath, "bin", "python"), pythonProjectResource.Command); + } + } + + [Fact] + public void WithVirtualEnvironment_UsesAppHostDirectoryWhenVenvOnlyExistsThere() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + + // Create app directory as a subdirectory of AppHost (realistic scenario) + var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); + Directory.CreateDirectory(appDirPath); + + // Create .venv in the AppHost directory (not in app directory) + var appHostVenvPath = Path.Combine(builder.AppHostDirectory, ".venv"); + Directory.CreateDirectory(appHostVenvPath); + + try + { + var scriptName = "main.py"; + var resourceBuilder = builder.AddPythonScript("pythonProject", appDirName, scriptName); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should use the AppHost directory .venv since it only exists there + if (OperatingSystem.IsWindows()) + { + Assert.Equal(Path.Join(appHostVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); + } + else + { + Assert.Equal(Path.Join(appHostVenvPath, "bin", "python"), pythonProjectResource.Command); + } + } + finally + { + // Clean up + if (Directory.Exists(appDirPath)) + { + Directory.Delete(appDirPath, true); + } + if (Directory.Exists(appHostVenvPath)) + { + Directory.Delete(appHostVenvPath, true); + } + } + } + + [Fact] + public void WithVirtualEnvironment_PrefersAppDirectoryWhenVenvExistsInBoth() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + + // Create app directory as a subdirectory of AppHost (realistic scenario) + var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); + Directory.CreateDirectory(appDirPath); + + // Create .venv in both directories + var appVenvPath = Path.Combine(appDirPath, ".venv"); + Directory.CreateDirectory(appVenvPath); + + var appHostVenvPath = Path.Combine(builder.AppHostDirectory, ".venv"); + Directory.CreateDirectory(appHostVenvPath); + + try + { + var scriptName = "main.py"; + var resourceBuilder = builder.AddPythonScript("pythonProject", appDirName, scriptName); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should prefer the app directory .venv when it exists in both locations + if (OperatingSystem.IsWindows()) + { + Assert.Equal(Path.Join(appVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); + } + else + { + Assert.Equal(Path.Join(appVenvPath, "bin", "python"), pythonProjectResource.Command); + } + } + finally + { + // Clean up + if (Directory.Exists(appDirPath)) + { + Directory.Delete(appDirPath, true); + } + if (Directory.Exists(appHostVenvPath)) + { + Directory.Delete(appHostVenvPath, true); + } + } + } + + [Fact] + public void WithVirtualEnvironment_DefaultsToAppDirectoryWhenVenvExistsInNeither() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + using var tempAppDir = new TempDirectory(); + + // Don't create .venv in either directory + + var scriptName = "main.py"; + var resourceBuilder = builder.AddPythonScript("pythonProject", tempAppDir.Path, scriptName); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should default to app directory when it doesn't exist in either location + var expectedProjectDirectory = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, tempAppDir.Path)); + var expectedVenvPath = Path.Combine(expectedProjectDirectory, ".venv"); + + if (OperatingSystem.IsWindows()) + { + Assert.Equal(Path.Join(expectedVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); + } + else + { + Assert.Equal(Path.Join(expectedVenvPath, "bin", "python"), pythonProjectResource.Command); + } + } + [Fact] public void WithUvEnvironment_CreatesUvEnvironmentResource() { From 2d05caebb41449c6d8eafe359fd619935ad2b342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:43:07 +0000 Subject: [PATCH 03/13] Update documentation for WithVirtualEnvironment fallback behavior Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 96eed88b91d..0a325002dc1 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -633,7 +633,8 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// The resource builder. /// /// The path to the virtual environment. Can be absolute or relative to the app directory. - /// When relative, it is resolved from the working directory of the Python application. + /// When relative, the virtual environment is resolved from the working directory of the Python application. + /// If not found there, it falls back to checking the AppHost directory (when the Python app is nearby). /// Common values include ".venv", "venv", or "myenv". /// /// A reference to the for method chaining. @@ -646,6 +647,15 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// Virtual environments allow Python applications to have isolated dependencies separate from /// the system Python installation. This is the recommended approach for Python applications. /// + /// + /// When using a relative path (like ".venv"), the method checks multiple locations: + /// + /// First, it checks if the virtual environment exists in the Python app directory. + /// If not found and the Python app is a subdirectory or sibling of the AppHost, it checks the AppHost directory. + /// If neither exists, it defaults to the Python app directory (for future creation). + /// + /// This allows sharing a single virtual environment across multiple Python apps in a workspace. + /// /// /// /// Configure a Python app to use a custom virtual environment: From 9d9c5217afd396968016f9303d1a97afcbe64381 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:49:20 +0000 Subject: [PATCH 04/13] Refactor: Move .venv lookup logic to AddPythonAppCore, use explicit paths verbatim Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 103 ++++++++++-------- .../AddPythonAppTests.cs | 56 ++++++++++ 2 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 0a325002dc1..e7f7b4158a1 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -353,6 +353,14 @@ private static IResourceBuilder AddPythonAppCore( // python will be replaced with the resolved entrypoint based on the virtualEnvironmentPath var resource = createResource(name, "python", Path.GetFullPath(appDirectory, builder.AppHostDirectory)); + // When using the default virtual environment path, intelligently look for existing virtual environments + // in multiple locations: app directory first, then AppHost directory as fallback + var resolvedVenvPath = virtualEnvironmentPath; + if (virtualEnvironmentPath == DefaultVirtualEnvFolder && !Path.IsPathRooted(virtualEnvironmentPath)) + { + resolvedVenvPath = ResolveDefaultVirtualEnvironmentPath(builder.AppHostDirectory, resource.WorkingDirectory, virtualEnvironmentPath); + } + var resourceBuilder = builder .AddResource(resource) // Order matters, we need to bootstrap the entrypoint before setting the entrypoint @@ -362,7 +370,7 @@ private static IResourceBuilder AddPythonAppCore( Entrypoint = entrypoint }) // This will resolve the correct python executable based on the virtual environment - .WithVirtualEnvironment(virtualEnvironmentPath) + .WithVirtualEnvironment(resolvedVenvPath) // This will set up the the entrypoint based on the PythonEntrypointAnnotation .WithEntrypoint(entrypointType, entrypoint); @@ -627,14 +635,51 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) } } + /// + /// Resolves the default virtual environment path by checking multiple candidate locations. + /// + /// The AppHost directory. + /// The Python app working directory. + /// The relative virtual environment path (e.g., ".venv"). + /// The resolved virtual environment path. + private static string ResolveDefaultVirtualEnvironmentPath(string appHostDirectory, string appWorkingDirectory, string virtualEnvironmentPath) + { + // Check if the virtual environment exists in the app directory first + var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appWorkingDirectory); + + // Only check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost + // This prevents picking up unrelated .venv directories from test fixtures or other sources + var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appHostDirectory); + + // Check if the app directory is under or near the AppHost directory + var appDirRelativeToAppHost = Path.GetRelativePath(appHostDirectory, appWorkingDirectory); + var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && + !Path.IsPathRooted(appDirRelativeToAppHost); + + if (Directory.Exists(appDirVenvPath)) + { + // Use the app directory if it exists there + return appDirVenvPath; + } + else if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) + { + // Use the AppHost directory if it exists there and the app is nearby + return appHostDirVenvPath; + } + else + { + // Default to app directory if neither exists (for cases where the venv will be created later) + return appDirVenvPath; + } + } + /// /// Configures a custom virtual environment path for the Python application. /// /// The resource builder. /// /// The path to the virtual environment. Can be absolute or relative to the app directory. - /// When relative, the virtual environment is resolved from the working directory of the Python application. - /// If not found there, it falls back to checking the AppHost directory (when the Python app is nearby). + /// When relative, it is resolved from the working directory of the Python application. /// Common values include ".venv", "venv", or "myenv". /// /// A reference to the for method chaining. @@ -648,13 +693,9 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// the system Python installation. This is the recommended approach for Python applications. /// /// - /// When using a relative path (like ".venv"), the method checks multiple locations: - /// - /// First, it checks if the virtual environment exists in the Python app directory. - /// If not found and the Python app is a subdirectory or sibling of the AppHost, it checks the AppHost directory. - /// If neither exists, it defaults to the Python app directory (for future creation). - /// - /// This allows sharing a single virtual environment across multiple Python apps in a workspace. + /// When you explicitly specify a virtual environment path using this method, the path is used verbatim. + /// The automatic multi-location lookup (checking both app and AppHost directories) only applies when + /// using the default ".venv" path during initial app creation via AddPythonScript, AddPythonModule, or AddPythonExecutable. /// /// /// @@ -670,44 +711,12 @@ public static IResourceBuilder WithVirtualEnvironment( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(virtualEnvironmentPath); - string resolvedVirtualEnvironmentPath; - if (Path.IsPathRooted(virtualEnvironmentPath)) - { - // If the path is absolute, use it as-is - resolvedVirtualEnvironmentPath = virtualEnvironmentPath; - } - else - { - // For relative paths, check if the virtual environment exists in the app directory first - var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.Resource.WorkingDirectory); - - // Only check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost - // This prevents picking up unrelated .venv directories from test fixtures or other sources - var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.ApplicationBuilder.AppHostDirectory); - - // Check if the app directory is under or near the AppHost directory - var appDirRelativeToAppHost = Path.GetRelativePath(builder.ApplicationBuilder.AppHostDirectory, builder.Resource.WorkingDirectory); - var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && - !Path.IsPathRooted(appDirRelativeToAppHost); - - if (Directory.Exists(appDirVenvPath)) - { - // Use the app directory if it exists there - resolvedVirtualEnvironmentPath = appDirVenvPath; - } - else if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) - { - // Use the AppHost directory if it exists there and the app is nearby - resolvedVirtualEnvironmentPath = appHostDirVenvPath; - } - else - { - // Default to app directory if neither exists (for cases where the venv will be created later) - resolvedVirtualEnvironmentPath = appDirVenvPath; - } - } + // Use the provided path verbatim - resolve relative paths against the app working directory + var resolvedPath = Path.IsPathRooted(virtualEnvironmentPath) + ? virtualEnvironmentPath + : Path.GetFullPath(virtualEnvironmentPath, builder.Resource.WorkingDirectory); - var virtualEnvironment = new VirtualEnvironment(resolvedVirtualEnvironmentPath); + var virtualEnvironment = new VirtualEnvironment(resolvedPath); // Get the entrypoint annotation to determine how to update the command if (!builder.Resource.TryGetLastAnnotation(out var entrypointAnnotation)) diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index fc185528357..a67ae832013 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -673,6 +673,62 @@ public void WithVirtualEnvironment_DefaultsToAppDirectoryWhenVenvExistsInNeither } } + [Fact] + public void WithVirtualEnvironment_ExplicitPath_UsesVerbatim() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + + // Create app directory as a subdirectory of AppHost + var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); + Directory.CreateDirectory(appDirPath); + + // Create .venv in the AppHost directory + var appHostVenvPath = Path.Combine(builder.AppHostDirectory, ".venv"); + Directory.CreateDirectory(appHostVenvPath); + + // Create a custom venv in the app directory + var customVenvPath = Path.Combine(appDirPath, "custom-venv"); + Directory.CreateDirectory(customVenvPath); + + try + { + var scriptName = "main.py"; + + // Explicitly specify a custom venv path - should use it verbatim, not fall back to AppHost .venv + var resourceBuilder = builder.AddPythonScript("pythonProject", appDirName, scriptName) + .WithVirtualEnvironment("custom-venv"); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should use the explicitly specified path, NOT the AppHost .venv + if (OperatingSystem.IsWindows()) + { + Assert.Equal(Path.Join(customVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); + } + else + { + Assert.Equal(Path.Join(customVenvPath, "bin", "python"), pythonProjectResource.Command); + } + } + finally + { + // Clean up + if (Directory.Exists(appDirPath)) + { + Directory.Delete(appDirPath, true); + } + if (Directory.Exists(appHostVenvPath)) + { + Directory.Delete(appHostVenvPath, true); + } + } + } + [Fact] public void WithUvEnvironment_CreatesUvEnvironmentResource() { From 7c32cb477d16166253644cab6479cf1623de2c4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:41:49 +0000 Subject: [PATCH 05/13] Refactor tests: Add helper method for Python path assertions and simplify directory naming Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../AddPythonAppTests.cs | 61 ++++++------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index a67ae832013..ad34c605a79 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -358,6 +358,15 @@ private static void CopyStreamToTestOutput(string label, StreamReader reader, IT outputHelper.WriteLine($"{label}:\n\n{output}"); } + private static void AssertPythonCommandPath(string expectedVenvPath, string actualCommand) + { + var expectedCommand = OperatingSystem.IsWindows() + ? Path.Join(expectedVenvPath, "Scripts", "python.exe") + : Path.Join(expectedVenvPath, "bin", "python"); + + Assert.Equal(expectedCommand, actualCommand); + } + private const string PythonApp = """" import logging @@ -531,23 +540,17 @@ public void WithVirtualEnvironment_UsesAppDirectoryWhenVenvExistsThere() var expectedProjectDirectory = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, tempAppDir.Path)); var expectedVenvPath = Path.Combine(expectedProjectDirectory, ".venv"); - if (OperatingSystem.IsWindows()) - { - Assert.Equal(Path.Join(expectedVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); - } - else - { - Assert.Equal(Path.Join(expectedVenvPath, "bin", "python"), pythonProjectResource.Command); - } + AssertPythonCommandPath(expectedVenvPath, pythonProjectResource.Command); } [Fact] public void WithVirtualEnvironment_UsesAppHostDirectoryWhenVenvOnlyExistsThere() { using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + using var tempAppDir = new TempDirectory(); // Create app directory as a subdirectory of AppHost (realistic scenario) - var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirName = "python-app"; var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); Directory.CreateDirectory(appDirPath); @@ -567,14 +570,7 @@ public void WithVirtualEnvironment_UsesAppHostDirectoryWhenVenvOnlyExistsThere() var pythonProjectResource = Assert.Single(executableResources); // Should use the AppHost directory .venv since it only exists there - if (OperatingSystem.IsWindows()) - { - Assert.Equal(Path.Join(appHostVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); - } - else - { - Assert.Equal(Path.Join(appHostVenvPath, "bin", "python"), pythonProjectResource.Command); - } + AssertPythonCommandPath(appHostVenvPath, pythonProjectResource.Command); } finally { @@ -596,7 +592,7 @@ public void WithVirtualEnvironment_PrefersAppDirectoryWhenVenvExistsInBoth() using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); // Create app directory as a subdirectory of AppHost (realistic scenario) - var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirName = "python-app"; var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); Directory.CreateDirectory(appDirPath); @@ -619,14 +615,7 @@ public void WithVirtualEnvironment_PrefersAppDirectoryWhenVenvExistsInBoth() var pythonProjectResource = Assert.Single(executableResources); // Should prefer the app directory .venv when it exists in both locations - if (OperatingSystem.IsWindows()) - { - Assert.Equal(Path.Join(appVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); - } - else - { - Assert.Equal(Path.Join(appVenvPath, "bin", "python"), pythonProjectResource.Command); - } + AssertPythonCommandPath(appVenvPath, pythonProjectResource.Command); } finally { @@ -663,14 +652,7 @@ public void WithVirtualEnvironment_DefaultsToAppDirectoryWhenVenvExistsInNeither var expectedProjectDirectory = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, tempAppDir.Path)); var expectedVenvPath = Path.Combine(expectedProjectDirectory, ".venv"); - if (OperatingSystem.IsWindows()) - { - Assert.Equal(Path.Join(expectedVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); - } - else - { - Assert.Equal(Path.Join(expectedVenvPath, "bin", "python"), pythonProjectResource.Command); - } + AssertPythonCommandPath(expectedVenvPath, pythonProjectResource.Command); } [Fact] @@ -679,7 +661,7 @@ public void WithVirtualEnvironment_ExplicitPath_UsesVerbatim() using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); // Create app directory as a subdirectory of AppHost - var appDirName = "python-app-" + Path.GetRandomFileName(); + var appDirName = "python-app"; var appDirPath = Path.Combine(builder.AppHostDirectory, appDirName); Directory.CreateDirectory(appDirPath); @@ -706,14 +688,7 @@ public void WithVirtualEnvironment_ExplicitPath_UsesVerbatim() var pythonProjectResource = Assert.Single(executableResources); // Should use the explicitly specified path, NOT the AppHost .venv - if (OperatingSystem.IsWindows()) - { - Assert.Equal(Path.Join(customVenvPath, "Scripts", "python.exe"), pythonProjectResource.Command); - } - else - { - Assert.Equal(Path.Join(customVenvPath, "bin", "python"), pythonProjectResource.Command); - } + AssertPythonCommandPath(customVenvPath, pythonProjectResource.Command); } finally { From af3065934d90e54d5e0cb133e7c6c2af3baa247e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:25:24 +0000 Subject: [PATCH 06/13] Add VIRTUAL_ENV configuration support to virtual environment lookup Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 51 +++++++++++-------- .../AddPythonAppTests.cs | 31 +++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index e7f7b4158a1..b62fbc04b1a 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -76,7 +76,13 @@ public static IResourceBuilder AddPythonApp( /// /// /// This method executes a Python script directly using python script.py. - /// By default, the virtual environment folder is expected to be named .venv and located in the app directory. + /// By default, the virtual environment is resolved using the following priority: + /// + /// If the VIRTUAL_ENV environment variable is set and the directory exists, use it. + /// If .venv exists in the app directory, use it. + /// If .venv exists in the AppHost directory (and the app is nearby), use it. + /// Otherwise, default to .venv in the app directory. + /// /// Use to specify a different virtual environment path. /// Use WithArgs to pass arguments to the script. /// @@ -358,7 +364,7 @@ private static IResourceBuilder AddPythonAppCore( var resolvedVenvPath = virtualEnvironmentPath; if (virtualEnvironmentPath == DefaultVirtualEnvFolder && !Path.IsPathRooted(virtualEnvironmentPath)) { - resolvedVenvPath = ResolveDefaultVirtualEnvironmentPath(builder.AppHostDirectory, resource.WorkingDirectory, virtualEnvironmentPath); + resolvedVenvPath = ResolveDefaultVirtualEnvironmentPath(builder, resource.WorkingDirectory, virtualEnvironmentPath); } var resourceBuilder = builder @@ -638,39 +644,42 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// /// Resolves the default virtual environment path by checking multiple candidate locations. /// - /// The AppHost directory. + /// The distributed application builder. /// The Python app working directory. /// The relative virtual environment path (e.g., ".venv"). /// The resolved virtual environment path. - private static string ResolveDefaultVirtualEnvironmentPath(string appHostDirectory, string appWorkingDirectory, string virtualEnvironmentPath) + private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicationBuilder builder, string appWorkingDirectory, string virtualEnvironmentPath) { - // Check if the virtual environment exists in the app directory first + // Priority 1: Check if VIRTUAL_ENV is set in configuration (standard Python convention) + var virtualEnvFromConfig = builder.Configuration["VIRTUAL_ENV"]; + if (!string.IsNullOrEmpty(virtualEnvFromConfig) && Directory.Exists(virtualEnvFromConfig)) + { + return virtualEnvFromConfig; + } + + // Priority 2: Check if the virtual environment exists in the app directory var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appWorkingDirectory); + if (Directory.Exists(appDirVenvPath)) + { + return appDirVenvPath; + } - // Only check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost + // Priority 3: Check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost // This prevents picking up unrelated .venv directories from test fixtures or other sources - var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appHostDirectory); - + var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.AppHostDirectory); + // Check if the app directory is under or near the AppHost directory - var appDirRelativeToAppHost = Path.GetRelativePath(appHostDirectory, appWorkingDirectory); + var appDirRelativeToAppHost = Path.GetRelativePath(builder.AppHostDirectory, appWorkingDirectory); var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(appDirRelativeToAppHost); - if (Directory.Exists(appDirVenvPath)) + if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) { - // Use the app directory if it exists there - return appDirVenvPath; - } - else if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) - { - // Use the AppHost directory if it exists there and the app is nearby return appHostDirVenvPath; } - else - { - // Default to app directory if neither exists (for cases where the venv will be created later) - return appDirVenvPath; - } + + // Default: Return app directory path (for cases where the venv will be created later) + return appDirVenvPath; } /// diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index ad34c605a79..f98ccf894bd 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -517,6 +517,37 @@ public async Task WithVirtualEnvironment_CanBeChainedWithOtherExtensions() Assert.Equal("test_value", environmentVariables["TEST_VAR"]); } + [Fact] + public void WithVirtualEnvironment_UsesVirtualEnvEnvironmentVariableWhenSet() + { + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); + using var tempAppDir = new TempDirectory(); + using var tempVirtualEnvDir = new TempDirectory(); + + // Create a .venv directory from VIRTUAL_ENV + var virtualEnvPath = Path.Combine(tempVirtualEnvDir.Path, "custom-venv"); + Directory.CreateDirectory(virtualEnvPath); + + // Also create .venv in the app directory to verify VIRTUAL_ENV takes priority + var appVenvPath = Path.Combine(tempAppDir.Path, ".venv"); + Directory.CreateDirectory(appVenvPath); + + // Set VIRTUAL_ENV in configuration + builder.Configuration["VIRTUAL_ENV"] = virtualEnvPath; + + var scriptName = "main.py"; + var resourceBuilder = builder.AddPythonScript("pythonProject", tempAppDir.Path, scriptName); + + var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var executableResources = appModel.GetExecutableResources(); + + var pythonProjectResource = Assert.Single(executableResources); + + // Should use the VIRTUAL_ENV path since it has highest priority + AssertPythonCommandPath(virtualEnvPath, pythonProjectResource.Command); + } + [Fact] public void WithVirtualEnvironment_UsesAppDirectoryWhenVenvExistsThere() { From a78085f05d19a2b317a687d4a2eef020b64e7c8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:49:50 +0000 Subject: [PATCH 07/13] Remove VIRTUAL_ENV configuration check from virtual environment lookup Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 12 ++----- .../AddPythonAppTests.cs | 31 ------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index b62fbc04b1a..7e228778f16 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -78,7 +78,6 @@ public static IResourceBuilder AddPythonApp( /// This method executes a Python script directly using python script.py. /// By default, the virtual environment is resolved using the following priority: /// - /// If the VIRTUAL_ENV environment variable is set and the directory exists, use it. /// If .venv exists in the app directory, use it. /// If .venv exists in the AppHost directory (and the app is nearby), use it. /// Otherwise, default to .venv in the app directory. @@ -650,21 +649,14 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// The resolved virtual environment path. private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicationBuilder builder, string appWorkingDirectory, string virtualEnvironmentPath) { - // Priority 1: Check if VIRTUAL_ENV is set in configuration (standard Python convention) - var virtualEnvFromConfig = builder.Configuration["VIRTUAL_ENV"]; - if (!string.IsNullOrEmpty(virtualEnvFromConfig) && Directory.Exists(virtualEnvFromConfig)) - { - return virtualEnvFromConfig; - } - - // Priority 2: Check if the virtual environment exists in the app directory + // Priority 1: Check if the virtual environment exists in the app directory var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appWorkingDirectory); if (Directory.Exists(appDirVenvPath)) { return appDirVenvPath; } - // Priority 3: Check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost + // Priority 2: Check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost // This prevents picking up unrelated .venv directories from test fixtures or other sources var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.AppHostDirectory); diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index f98ccf894bd..ad34c605a79 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -517,37 +517,6 @@ public async Task WithVirtualEnvironment_CanBeChainedWithOtherExtensions() Assert.Equal("test_value", environmentVariables["TEST_VAR"]); } - [Fact] - public void WithVirtualEnvironment_UsesVirtualEnvEnvironmentVariableWhenSet() - { - using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); - using var tempAppDir = new TempDirectory(); - using var tempVirtualEnvDir = new TempDirectory(); - - // Create a .venv directory from VIRTUAL_ENV - var virtualEnvPath = Path.Combine(tempVirtualEnvDir.Path, "custom-venv"); - Directory.CreateDirectory(virtualEnvPath); - - // Also create .venv in the app directory to verify VIRTUAL_ENV takes priority - var appVenvPath = Path.Combine(tempAppDir.Path, ".venv"); - Directory.CreateDirectory(appVenvPath); - - // Set VIRTUAL_ENV in configuration - builder.Configuration["VIRTUAL_ENV"] = virtualEnvPath; - - var scriptName = "main.py"; - var resourceBuilder = builder.AddPythonScript("pythonProject", tempAppDir.Path, scriptName); - - var app = builder.Build(); - var appModel = app.Services.GetRequiredService(); - var executableResources = appModel.GetExecutableResources(); - - var pythonProjectResource = Assert.Single(executableResources); - - // Should use the VIRTUAL_ENV path since it has highest priority - AssertPythonCommandPath(virtualEnvPath, pythonProjectResource.Command); - } - [Fact] public void WithVirtualEnvironment_UsesAppDirectoryWhenVenvExistsThere() { From bb558672939fae4a86204ee6266e93356e78f5a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 03:00:21 +0000 Subject: [PATCH 08/13] Add clarifying comments for "nearby" check in virtual environment lookup Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 7e228778f16..d9d795f8a4e 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -660,7 +660,12 @@ private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicati // This prevents picking up unrelated .venv directories from test fixtures or other sources var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.AppHostDirectory); - // Check if the app directory is under or near the AppHost directory + // Check if the app directory is "nearby" the AppHost directory. + // "Nearby" means the Python app is either: + // - A subdirectory of the AppHost directory (e.g., AppHost/python-app) + // - A sibling of the AppHost directory (e.g., AppHost/../python-app) + // This is determined by checking if the relative path doesn't start with ".." (going up directories) + // and isn't an absolute path (completely unrelated location like /tmp or C:\Temp). var appDirRelativeToAppHost = Path.GetRelativePath(builder.AppHostDirectory, appWorkingDirectory); var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(appDirRelativeToAppHost); From c866034661eb4b8751b8068e55b1180bf005fdf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:46:58 +0000 Subject: [PATCH 09/13] Address PR feedback: simplify logic, use appDirectory, remove redundant checks Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index d9d795f8a4e..b0014006d0d 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -79,7 +79,7 @@ public static IResourceBuilder AddPythonApp( /// By default, the virtual environment is resolved using the following priority: /// /// If .venv exists in the app directory, use it. - /// If .venv exists in the AppHost directory (and the app is nearby), use it. + /// If .venv exists in the AppHost directory, use it. /// Otherwise, default to .venv in the app directory. /// /// Use to specify a different virtual environment path. @@ -355,17 +355,17 @@ private static IResourceBuilder AddPythonAppCore( ArgumentException.ThrowIfNullOrEmpty(entrypoint); ArgumentNullException.ThrowIfNull(virtualEnvironmentPath); - // python will be replaced with the resolved entrypoint based on the virtualEnvironmentPath - var resource = createResource(name, "python", Path.GetFullPath(appDirectory, builder.AppHostDirectory)); - - // When using the default virtual environment path, intelligently look for existing virtual environments + // When using the default virtual environment path, look for existing virtual environments // in multiple locations: app directory first, then AppHost directory as fallback var resolvedVenvPath = virtualEnvironmentPath; - if (virtualEnvironmentPath == DefaultVirtualEnvFolder && !Path.IsPathRooted(virtualEnvironmentPath)) + if (virtualEnvironmentPath == DefaultVirtualEnvFolder) { - resolvedVenvPath = ResolveDefaultVirtualEnvironmentPath(builder, resource.WorkingDirectory, virtualEnvironmentPath); + resolvedVenvPath = ResolveDefaultVirtualEnvironmentPath(builder, appDirectory, virtualEnvironmentPath); } + // python will be replaced with the resolved entrypoint based on the virtualEnvironmentPath + var resource = createResource(name, "python", Path.GetFullPath(appDirectory, builder.AppHostDirectory)); + var resourceBuilder = builder .AddResource(resource) // Order matters, we need to bootstrap the entrypoint before setting the entrypoint @@ -644,33 +644,30 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs) /// Resolves the default virtual environment path by checking multiple candidate locations. /// /// The distributed application builder. - /// The Python app working directory. + /// The Python app directory (relative to AppHost). /// The relative virtual environment path (e.g., ".venv"). /// The resolved virtual environment path. - private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicationBuilder builder, string appWorkingDirectory, string virtualEnvironmentPath) + private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicationBuilder builder, string appDirectory, string virtualEnvironmentPath) { + var appDirectoryFullPath = Path.GetFullPath(appDirectory, builder.AppHostDirectory); + // Priority 1: Check if the virtual environment exists in the app directory - var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appWorkingDirectory); + var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appDirectoryFullPath); if (Directory.Exists(appDirVenvPath)) { return appDirVenvPath; } - // Priority 2: Check the AppHost directory if the Python app is a subdirectory or sibling of the AppHost - // This prevents picking up unrelated .venv directories from test fixtures or other sources + // Priority 2: Check the AppHost directory if the Python app is a child or at the same level + // We only look up to the AppHost's direct parent, not beyond var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.AppHostDirectory); - // Check if the app directory is "nearby" the AppHost directory. - // "Nearby" means the Python app is either: - // - A subdirectory of the AppHost directory (e.g., AppHost/python-app) - // - A sibling of the AppHost directory (e.g., AppHost/../python-app) - // This is determined by checking if the relative path doesn't start with ".." (going up directories) - // and isn't an absolute path (completely unrelated location like /tmp or C:\Temp). - var appDirRelativeToAppHost = Path.GetRelativePath(builder.AppHostDirectory, appWorkingDirectory); - var isAppDirNearAppHost = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal) && - !Path.IsPathRooted(appDirRelativeToAppHost); - - if (isAppDirNearAppHost && Directory.Exists(appHostDirVenvPath)) + // Check if the app directory is within the AppHost's tree (child or same directory) + // The relative path should not start with ".." which would indicate going up beyond the AppHost + var appDirRelativeToAppHost = Path.GetRelativePath(builder.AppHostDirectory, appDirectoryFullPath); + var isAppDirWithinAppHostTree = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal); + + if (isAppDirWithinAppHostTree && Directory.Exists(appHostDirVenvPath)) { return appHostDirVenvPath; } From 7881363983502c38a92aa48dc19b516d35e8d031 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:57:51 +0000 Subject: [PATCH 10/13] Walk up parent directories from Python app, stop at AppHost's parent Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index b0014006d0d..a5501b57a1d 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -651,29 +651,39 @@ private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicati { var appDirectoryFullPath = Path.GetFullPath(appDirectory, builder.AppHostDirectory); - // Priority 1: Check if the virtual environment exists in the app directory - var appDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, appDirectoryFullPath); - if (Directory.Exists(appDirVenvPath)) - { - return appDirVenvPath; - } - - // Priority 2: Check the AppHost directory if the Python app is a child or at the same level - // We only look up to the AppHost's direct parent, not beyond - var appHostDirVenvPath = Path.GetFullPath(virtualEnvironmentPath, builder.AppHostDirectory); + // Walk up from the Python app directory looking for the virtual environment + // Stop at the AppHost's parent directory to avoid picking up unrelated venvs + var appHostParentDirectory = Path.GetDirectoryName(builder.AppHostDirectory); + var currentDirectory = appDirectoryFullPath; - // Check if the app directory is within the AppHost's tree (child or same directory) - // The relative path should not start with ".." which would indicate going up beyond the AppHost - var appDirRelativeToAppHost = Path.GetRelativePath(builder.AppHostDirectory, appDirectoryFullPath); - var isAppDirWithinAppHostTree = !appDirRelativeToAppHost.StartsWith("..", StringComparison.Ordinal); - - if (isAppDirWithinAppHostTree && Directory.Exists(appHostDirVenvPath)) + while (currentDirectory != null) { - return appHostDirVenvPath; + var venvPath = Path.Combine(currentDirectory, virtualEnvironmentPath); + if (Directory.Exists(venvPath)) + { + return venvPath; + } + + // Stop if we've reached the AppHost's parent directory + if (string.Equals(currentDirectory, appHostParentDirectory, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + // Move up to the parent directory + var parentDirectory = Path.GetDirectoryName(currentDirectory); + + // Stop if we can't go up anymore or if we've gone beyond the AppHost's parent + if (parentDirectory == null || parentDirectory == currentDirectory) + { + break; + } + + currentDirectory = parentDirectory; } // Default: Return app directory path (for cases where the venv will be created later) - return appDirVenvPath; + return Path.Combine(appDirectoryFullPath, virtualEnvironmentPath); } /// From 26930abb347c642ee38f0c5be845c1231fb2aeee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:36:26 +0000 Subject: [PATCH 11/13] Use platform-aware path comparison for directory boundary check Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index a5501b57a1d..6f2ab716043 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -665,7 +665,12 @@ private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicati } // Stop if we've reached the AppHost's parent directory - if (string.Equals(currentDirectory, appHostParentDirectory, StringComparison.OrdinalIgnoreCase)) + // Use case-insensitive comparison on Windows, case-sensitive on Unix + var reachedBoundary = OperatingSystem.IsWindows() + ? string.Equals(currentDirectory, appHostParentDirectory, StringComparison.OrdinalIgnoreCase) + : string.Equals(currentDirectory, appHostParentDirectory, StringComparison.Ordinal); + + if (reachedBoundary) { break; } From bf13f277f96c643f2807810c06fb2439e7b4faa7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:10:47 +0000 Subject: [PATCH 12/13] Only walk up directories when app is under AppHost's parent tree Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- .../PythonAppResourceBuilderExtensions.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 6f2ab716043..09d2b4f2cde 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -654,6 +654,27 @@ private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicati // Walk up from the Python app directory looking for the virtual environment // Stop at the AppHost's parent directory to avoid picking up unrelated venvs var appHostParentDirectory = Path.GetDirectoryName(builder.AppHostDirectory); + + // Check if the app directory is under the AppHost's parent directory + // If not, only look in the app directory itself + if (appHostParentDirectory != null) + { + var relativePath = Path.GetRelativePath(appHostParentDirectory, appDirectoryFullPath); + var isUnderAppHostParent = !relativePath.StartsWith("..", StringComparison.Ordinal) && + !Path.IsPathRooted(relativePath); + + if (!isUnderAppHostParent) + { + // App is not under AppHost's parent, only check the app directory + var appDirVenvPath = Path.Combine(appDirectoryFullPath, virtualEnvironmentPath); + if (Directory.Exists(appDirVenvPath)) + { + return appDirVenvPath; + } + return appDirVenvPath; + } + } + var currentDirectory = appDirectoryFullPath; while (currentDirectory != null) From 08eb9abebe8d60fe74e1ca3e9df3f3f3d4adcac0 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 3 Nov 2025 21:55:21 -0600 Subject: [PATCH 13/13] Update src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs --- .../PythonAppResourceBuilderExtensions.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 09d2b4f2cde..3c50c728c77 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -665,13 +665,8 @@ private static string ResolveDefaultVirtualEnvironmentPath(IDistributedApplicati if (!isUnderAppHostParent) { - // App is not under AppHost's parent, only check the app directory - var appDirVenvPath = Path.Combine(appDirectoryFullPath, virtualEnvironmentPath); - if (Directory.Exists(appDirVenvPath)) - { - return appDirVenvPath; - } - return appDirVenvPath; + // App is not under AppHost's parent, only use the app directory + return Path.Combine(appDirectoryFullPath, virtualEnvironmentPath); } }