From 1e9984cfc0e0dc76f954fd9980a9342d3a7f0d7a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 06:56:12 +0000
Subject: [PATCH 1/9] Initial plan
From d5695d5513a2cdad8683a55840e060a1f135fb71 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 07:10:46 +0000
Subject: [PATCH 2/9] Add Poetry package manager support with tests
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PoetryEnvironmentAnnotation.cs | 18 +
.../PoetryInstallationManager.cs | 46 +++
.../PythonAppResourceBuilderExtensions.cs | 309 +++++++++++++++++-
.../AddPythonAppTests.cs | 194 +++++++++++
4 files changed, 555 insertions(+), 12 deletions(-)
create mode 100644 src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
create mode 100644 src/Aspire.Hosting.Python/PoetryInstallationManager.cs
diff --git a/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs b/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
new file mode 100644
index 00000000000..e689c62952b
--- /dev/null
+++ b/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.Python;
+
+///
+/// Represents environment variables to be set for Poetry operations.
+///
+/// The environment variables to set for Poetry.
+internal sealed class PoetryEnvironmentAnnotation((string key, string value)[] environmentVariables) : IResourceAnnotation
+{
+ ///
+ /// Gets the environment variables to be set for Poetry operations.
+ ///
+ public (string key, string value)[] EnvironmentVariables { get; } = environmentVariables;
+}
diff --git a/src/Aspire.Hosting.Python/PoetryInstallationManager.cs b/src/Aspire.Hosting.Python/PoetryInstallationManager.cs
new file mode 100644
index 00000000000..e96bccf0bcb
--- /dev/null
+++ b/src/Aspire.Hosting.Python/PoetryInstallationManager.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Utils;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.Python;
+
+///
+/// Validates that the poetry command is available on the system.
+///
+#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+internal sealed class PoetryInstallationManager : RequiredCommandValidator
+{
+ private string? _resolvedCommandPath;
+
+ public PoetryInstallationManager(
+ IInteractionService interactionService,
+ ILogger logger)
+ : base(interactionService, logger)
+ {
+ }
+
+ ///
+ /// Ensures poetry is installed/available. This method is safe for concurrent callers;
+ /// only one validation will run at a time.
+ ///
+ /// Whether to throw an exception if poetry is not found. Default is true.
+ /// Cancellation token.
+ public Task EnsureInstalledAsync(bool throwOnFailure = true, CancellationToken cancellationToken = default)
+ {
+ SetThrowOnFailure(throwOnFailure);
+ return RunAsync(cancellationToken);
+ }
+
+ protected override string GetCommandPath() => "poetry";
+
+ protected override Task OnValidatedAsync(string resolvedCommandPath, CancellationToken cancellationToken)
+ {
+ _resolvedCommandPath = resolvedCommandPath;
+ return Task.CompletedTask;
+ }
+
+ protected override string? GetHelpLink() => "https://python-poetry.org/docs/#installation";
+}
+#pragma warning restore ASPIREINTERACTION001
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index a0ff10b660a..1234f109ad8 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -478,14 +478,20 @@ private static IResourceBuilder AddPythonAppCore(
var entrypointType = entrypointAnnotation.Type;
var entrypoint = entrypointAnnotation.Entrypoint;
- // Check if using UV by looking at the package manager annotation
+ // Check which package manager is being used
var isUsingUv = context.Resource.TryGetLastAnnotation(out var pkgMgr) &&
pkgMgr.ExecutableName == "uv";
+ var isUsingPoetry = context.Resource.TryGetLastAnnotation(out pkgMgr) &&
+ pkgMgr.ExecutableName == "poetry";
if (isUsingUv)
{
GenerateUvDockerfile(context, resource, pythonVersion, entrypointType, entrypoint);
}
+ else if (isUsingPoetry)
+ {
+ GeneratePoetryDockerfile(context, resource, pythonVersion, entrypointType, entrypoint);
+ }
else
{
GenerateFallbackDockerfile(context, resource, pythonVersion, entrypointType, entrypoint);
@@ -646,6 +652,111 @@ private static void GenerateUvDockerfile(DockerfileBuilderCallbackContext contex
}
}
+ private static void GeneratePoetryDockerfile(DockerfileBuilderCallbackContext context, PythonAppResource resource,
+ string pythonVersion, EntrypointType entrypointType, string entrypoint)
+ {
+ // Check if poetry.lock exists in the working directory
+ var poetryLockPath = Path.Combine(resource.WorkingDirectory, "poetry.lock");
+ var hasPoetryLock = File.Exists(poetryLockPath);
+
+ // Get custom base images from annotation, if present
+ context.Resource.TryGetLastAnnotation(out var baseImageAnnotation);
+ var buildImage = baseImageAnnotation?.BuildImage ?? $"python:{pythonVersion}-slim-bookworm";
+ var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"python:{pythonVersion}-slim-bookworm";
+
+ var builderStage = context.Builder
+ .From(buildImage, "builder")
+ .EmptyLine()
+ .Comment("Install Poetry")
+ .Run("pip install --no-cache-dir poetry")
+ .EmptyLine()
+ .Comment("Configure Poetry to create virtual environments in the project directory")
+ .Env("POETRY_VIRTUALENVS_IN_PROJECT", "true")
+ .Env("POETRY_NO_INTERACTION", "1")
+ .EmptyLine()
+ .WorkDir("/app")
+ .EmptyLine();
+
+ if (hasPoetryLock)
+ {
+ // If poetry.lock exists, use locked mode for reproducible builds
+ builderStage
+ .Comment("Install dependencies first for better layer caching")
+ .Comment("Copy only pyproject.toml and poetry.lock for dependency installation")
+ .Copy("pyproject.toml", "/app/")
+ .Copy("poetry.lock", "/app/")
+ .EmptyLine()
+ .Comment("Install dependencies (no dev dependencies)")
+ .Run("poetry install --no-interaction --no-root --only main")
+ .EmptyLine()
+ .Comment("Copy the rest of the application source")
+ .Copy(".", "/app")
+ .EmptyLine()
+ .Comment("Install the project itself")
+ .Run("poetry install --no-interaction --only-root");
+ }
+ else
+ {
+ // If poetry.lock doesn't exist, copy pyproject.toml and generate lock file
+ builderStage
+ .Comment("Copy pyproject.toml to install dependencies")
+ .Copy("pyproject.toml", "/app/")
+ .EmptyLine()
+ .Comment("Install dependencies and generate lock file")
+ .Run("poetry install --no-interaction --no-root --only main")
+ .EmptyLine()
+ .Comment("Copy the rest of the application source")
+ .Copy(".", "/app")
+ .EmptyLine()
+ .Comment("Install the project itself")
+ .Run("poetry install --no-interaction --only-root");
+ }
+
+ var logger = context.Services.GetService>();
+ context.Builder.AddContainerFilesStages(context.Resource, logger);
+
+ var runtimeBuilder = context.Builder
+ .From(runtimeImage, "app")
+ .EmptyLine()
+ .AddContainerFiles(context.Resource, "/app", logger)
+ .Comment("------------------------------")
+ .Comment("🚀 Runtime stage")
+ .Comment("------------------------------")
+ .Comment("Create non-root user for security")
+ .Run("groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser")
+ .EmptyLine()
+ .Comment("Copy the application and virtual environment from builder")
+ .CopyFrom(builderStage.StageName!, "/app", "/app", "appuser:appuser")
+ .EmptyLine()
+ .Comment("Add virtual environment to PATH and set VIRTUAL_ENV")
+ .Env("PATH", "/app/.venv/bin:${PATH}")
+ .Env("VIRTUAL_ENV", "/app/.venv")
+ .Env("PYTHONDONTWRITEBYTECODE", "1")
+ .Env("PYTHONUNBUFFERED", "1")
+ .EmptyLine()
+ .Comment("Use the non-root user to run the application")
+ .User("appuser")
+ .EmptyLine()
+ .Comment("Set working directory")
+ .WorkDir("/app")
+ .EmptyLine()
+ .Comment("Run the application");
+
+ // Set the appropriate entrypoint and command based on entrypoint type
+ switch (entrypointType)
+ {
+ case EntrypointType.Script:
+ runtimeBuilder.Entrypoint(["python", entrypoint]);
+ break;
+ case EntrypointType.Module:
+ runtimeBuilder.Entrypoint(["python", "-m", entrypoint]);
+ break;
+ case EntrypointType.Executable:
+ runtimeBuilder.Entrypoint([entrypoint]);
+ break;
+ }
+ }
+
private static void GenerateFallbackDockerfile(DockerfileBuilderCallbackContext context, PythonAppResource resource,
string pythonVersion, EntrypointType entrypointType, string entrypoint)
{
@@ -1276,6 +1387,118 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
return builder;
}
+ ///
+ /// Configures the Python resource to use Poetry as the package manager.
+ ///
+ /// The type of the Python application resource, must derive from .
+ /// The resource builder.
+ /// When true (default), automatically runs poetry install before the application starts. When false, only sets the package manager annotation without creating an installer resource.
+ /// Additional arguments appended to the poetry install command (after --no-interaction).
+ /// Extra environment variables applied to the Poetry restore step. These can override defaults like POETRY_VIRTUALENVS_IN_PROJECT.
+ /// A reference to the for method chaining.
+ ///
+ ///
+ /// This method creates a child resource that runs poetry install --no-interaction in the working directory of the Python application.
+ /// The Python application will wait for this resource to complete successfully before starting.
+ ///
+ ///
+ /// Poetry (https://python-poetry.org/) is a Python package and dependency manager that handles virtual environment creation.
+ /// By default, Aspire configures Poetry to create the virtual environment in the project directory (.venv) by setting
+ /// POETRY_VIRTUALENVS_IN_PROJECT=true, unless an explicit virtual environment path is configured via
+ /// or overridden via the parameter.
+ ///
+ ///
+ /// When using to specify an explicit path, Aspire will set POETRY_VIRTUALENVS_PATH
+ /// to direct Poetry to use that location.
+ ///
+ ///
+ /// Calling this method will replace any previously configured package manager (such as pip or uv).
+ ///
+ ///
+ ///
+ /// Add a Python app with automatic Poetry package installation:
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var python = builder.AddPythonApp("api", "../python-api", "main.py")
+ /// .WithPoetry() // Automatically runs 'poetry install --no-interaction'
+ /// .WithHttpEndpoint(port: 5000);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ /// Add a Python app with custom Poetry install arguments:
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var python = builder.AddPythonApp("api", "../python-api", "main.py")
+ /// .WithPoetry(installArgs: ["--no-root", "--sync"])
+ /// .WithHttpEndpoint(port: 5000);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ /// Disable Poetry's in-project virtual environment:
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var python = builder.AddPythonApp("api", "../python-api", "main.py")
+ /// .WithPoetry(env: [("POETRY_VIRTUALENVS_IN_PROJECT", "false")])
+ /// .WithHttpEndpoint(port: 5000);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ /// Thrown when is null.
+ public static IResourceBuilder WithPoetry(
+ this IResourceBuilder builder,
+ bool install = true,
+ string[]? installArgs = null,
+ (string key, string value)[]? env = null)
+ where T : PythonAppResource
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ // Register Poetry validation service
+ builder.ApplicationBuilder.Services.TryAddSingleton();
+
+ // Ensure virtual environment exists - create default .venv if not configured
+ if (!builder.Resource.TryGetLastAnnotation(out var pythonEnv) ||
+ pythonEnv.VirtualEnvironment is null)
+ {
+ // Create default virtual environment if none exists
+ builder.WithVirtualEnvironment(".venv");
+ }
+
+ // Build base install command: poetry install --no-interaction
+ var baseInstallArgs = new List { "install", "--no-interaction" };
+
+ // Append user-provided install args
+ if (installArgs != null)
+ {
+ baseInstallArgs.AddRange(installArgs);
+ }
+
+ // Store Poetry environment variables in an annotation for later use
+ if (env != null && env.Length > 0)
+ {
+ builder.WithAnnotation(new PoetryEnvironmentAnnotation(env), ResourceAnnotationMutationBehavior.Replace);
+ }
+
+ builder
+ .WithAnnotation(new PythonPackageManagerAnnotation("poetry"), ResourceAnnotationMutationBehavior.Replace)
+ .WithAnnotation(new PythonInstallCommandAnnotation([.. baseInstallArgs]), ResourceAnnotationMutationBehavior.Replace);
+
+ AddInstaller(builder, install);
+
+ // Poetry handles venv creation, so remove any existing venv creator
+ RemoveVenvCreator(builder);
+
+ return builder;
+ }
+
private static bool IsPythonCommandAvailable(string command)
{
var pathVariable = Environment.GetEnvironmentVariable("PATH");
@@ -1349,16 +1572,24 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w
.WithParentRelationship(builder.Resource)
.ExcludeFromManifest();
- // Add validation for the installer command (uv or python)
+ // Add validation for the installer command (uv, poetry, or python)
installerBuilder.OnBeforeResourceStarted(static async (installerResource, e, ct) =>
{
// Check which command this installer is using (set by BeforeStartEvent)
- if (installerResource.TryGetLastAnnotation(out var executable) &&
- executable.Command == "uv")
+ if (installerResource.TryGetLastAnnotation(out var executable))
{
- // Validate that uv is installed - don't throw so the app fails as it normally would
- var uvInstallationManager = e.Services.GetRequiredService();
- await uvInstallationManager.EnsureInstalledAsync(throwOnFailure: false, ct).ConfigureAwait(false);
+ if (executable.Command == "uv")
+ {
+ // Validate that uv is installed - don't throw so the app fails as it normally would
+ var uvInstallationManager = e.Services.GetRequiredService();
+ await uvInstallationManager.EnsureInstalledAsync(throwOnFailure: false, ct).ConfigureAwait(false);
+ }
+ else if (executable.Command == "poetry")
+ {
+ // Validate that poetry is installed - don't throw so the app fails as it normally would
+ var poetryInstallationManager = e.Services.GetRequiredService();
+ await poetryInstallationManager.EnsureInstalledAsync(throwOnFailure: false, ct).ConfigureAwait(false);
+ }
}
// For other package managers (pip, etc.), Python validation happens via PythonVenvCreatorResource
});
@@ -1380,6 +1611,60 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w
.WithWorkingDirectory(builder.Resource.WorkingDirectory)
.WithArgs(installCommand.Args);
+ // Add Poetry-specific environment variables
+ if (packageManager.ExecutableName == "poetry")
+ {
+ installerBuilder.WithEnvironment(ctx =>
+ {
+ // Determine default Poetry environment variables based on virtual environment configuration
+ if (builder.Resource.TryGetLastAnnotation(out var pythonEnv) &&
+ pythonEnv.VirtualEnvironment != null)
+ {
+ var venvPath = pythonEnv.VirtualEnvironment.VirtualEnvironmentPath;
+ var isAbsolutePath = Path.IsPathRooted(venvPath);
+ var resolvedPath = isAbsolutePath
+ ? venvPath
+ : Path.GetFullPath(venvPath, builder.Resource.WorkingDirectory);
+
+ // Check if the venv path is within the project directory
+ var projectVenvPath = Path.GetFullPath(".venv", builder.Resource.WorkingDirectory);
+ var isInProject = string.Equals(
+ resolvedPath,
+ projectVenvPath,
+ OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+
+ if (isInProject)
+ {
+ // Use in-project venv
+ ctx.EnvironmentVariables["POETRY_VIRTUALENVS_IN_PROJECT"] = "true";
+ }
+ else
+ {
+ // Use external venv path
+ var venvParentPath = Path.GetDirectoryName(resolvedPath);
+ if (!string.IsNullOrEmpty(venvParentPath))
+ {
+ ctx.EnvironmentVariables["POETRY_VIRTUALENVS_PATH"] = venvParentPath;
+ }
+ }
+ }
+ else
+ {
+ // Default to in-project venv if no explicit path configured
+ ctx.EnvironmentVariables["POETRY_VIRTUALENVS_IN_PROJECT"] = "true";
+ }
+
+ // Apply user-specified environment variable overrides
+ if (builder.Resource.TryGetLastAnnotation(out var poetryEnv))
+ {
+ foreach (var (key, value) in poetryEnv.EnvironmentVariables)
+ {
+ ctx.EnvironmentVariables[key] = value;
+ }
+ }
+ });
+ }
+
return Task.CompletedTask;
});
@@ -1506,13 +1791,13 @@ private static void SetupDependencies(IDistributedApplicationBuilder builder, Py
private static bool ShouldCreateVenv(IResourceBuilder builder) where T : PythonAppResource
{
- // Check if we're using uv (which handles venv creation itself)
- var isUsingUv = builder.Resource.TryGetLastAnnotation(out var pkgMgr) &&
- pkgMgr.ExecutableName == "uv";
+ // Check if we're using uv or poetry (which handle venv creation themselves)
+ var isUsingPackageManagerWithVenv = builder.Resource.TryGetLastAnnotation(out var pkgMgr) &&
+ (pkgMgr.ExecutableName == "uv" || pkgMgr.ExecutableName == "poetry");
- if (isUsingUv)
+ if (isUsingPackageManagerWithVenv)
{
- // UV handles venv creation, we don't need to create it
+ // UV and Poetry handle venv creation, we don't need to create it
return false;
}
diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
index 8a1fea29671..851c0eb24e4 100644
--- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
+++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
@@ -2287,6 +2287,200 @@ public void MethodOrdering_WithUv_ThenWithPip_ReplacesPackageManager_And_Enables
Assert.Single(appModel.Resources.OfType());
}
+ [Fact]
+ public void WithPoetry_CreatesPoetryInstallerResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPoetry();
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ // Verify the installer resource exists
+ var installerResource = appModel.Resources.OfType().Single();
+ Assert.Equal("pythonProject-installer", installerResource.Name);
+
+ var expectedProjectDirectory = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, tempDir.Path));
+ Assert.Equal(expectedProjectDirectory, installerResource.WorkingDirectory);
+
+ // Verify the package manager annotation
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var packageManager));
+ Assert.Equal("poetry", packageManager.ExecutableName);
+
+ // Verify the install command annotation
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var installAnnotation));
+ Assert.Equal(2, installAnnotation.Args.Length);
+ Assert.Equal("install", installAnnotation.Args[0]);
+ Assert.Equal("--no-interaction", installAnnotation.Args[1]);
+ }
+
+ [Fact]
+ public void WithPoetry_WithCustomInstallArgs_AppendsArgs()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPoetry(installArgs: ["--no-root", "--sync"]);
+
+ var app = builder.Build();
+
+ // Verify the install command annotation has custom args
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var installAnnotation));
+ Assert.Equal(4, installAnnotation.Args.Length);
+ Assert.Equal("install", installAnnotation.Args[0]);
+ Assert.Equal("--no-interaction", installAnnotation.Args[1]);
+ Assert.Equal("--no-root", installAnnotation.Args[2]);
+ Assert.Equal("--sync", installAnnotation.Args[3]);
+ }
+
+ [Fact]
+ public void WithPoetry_WithEnvironmentVariables_StoresAnnotation()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPoetry(env: [("POETRY_VIRTUALENVS_IN_PROJECT", "false"), ("POETRY_HTTP_TIMEOUT", "60")]);
+
+ var app = builder.Build();
+
+ // Verify the Poetry environment annotation exists with the custom environment variables
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var poetryEnv));
+ Assert.Equal(2, poetryEnv.EnvironmentVariables.Length);
+ Assert.Contains(poetryEnv.EnvironmentVariables, e => e.key == "POETRY_VIRTUALENVS_IN_PROJECT" && e.value == "false");
+ Assert.Contains(poetryEnv.EnvironmentVariables, e => e.key == "POETRY_HTTP_TIMEOUT" && e.value == "60");
+ }
+
+ [Fact]
+ public void WithPoetry_InstallFalse_DoesNotCreateInstaller()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPoetry(install: false);
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ // Verify the installer resource does not exist
+ var installerResource = appModel.Resources.OfType().SingleOrDefault();
+ Assert.Null(installerResource);
+
+ // Verify the package manager annotation still exists
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var packageManager));
+ Assert.Equal("poetry", packageManager.ExecutableName);
+ }
+
+ [Fact]
+ public void WithPoetry_AfterWithUv_ReplacesPackageManager()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ // Call WithUv then WithPoetry - WithPoetry should replace WithUv
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithUv()
+ .WithPoetry();
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ // Verify that only one installer resource was created
+ var installerResource = appModel.Resources.OfType().Single();
+ Assert.Equal("pythonProject-installer", installerResource.Name);
+
+ // Verify that poetry is the active package manager (not uv)
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var packageManager));
+ Assert.Equal("poetry", packageManager.ExecutableName);
+
+ // Verify the install command is for poetry (not uv sync)
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var installAnnotation));
+ Assert.Equal("install", installAnnotation.Args[0]);
+ Assert.Equal("--no-interaction", installAnnotation.Args[1]);
+ }
+
+ [Fact]
+ public void WithPoetry_AfterWithPip_ReplacesPackageManager()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ // Call WithPip then WithPoetry - WithPoetry should replace WithPip
+ var pythonApp = builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPip()
+ .WithPoetry();
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ // Verify that only one installer resource was created
+ var installerResource = appModel.Resources.OfType().Single();
+ Assert.Equal("pythonProject-installer", installerResource.Name);
+
+ // Verify that poetry is the active package manager (not pip)
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var packageManager));
+ Assert.Equal("poetry", packageManager.ExecutableName);
+
+ // Verify the install command is for poetry (not pip install)
+ Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var installAnnotation));
+ Assert.Equal("install", installAnnotation.Args[0]);
+ Assert.Equal("--no-interaction", installAnnotation.Args[1]);
+ }
+
+ [Fact]
+ public void WithPoetry_ThrowsOnNullBuilder()
+ {
+ IResourceBuilder builder = null!;
+
+ var exception = Assert.Throws(() =>
+ builder.WithPoetry());
+
+ Assert.Equal("builder", exception.ParamName);
+ }
+
+ [Fact]
+ public async Task WithPoetry_AddsWaitForCompletionRelationship()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper);
+ using var tempDir = new TempDirectory();
+
+ var scriptName = "main.py";
+
+ builder.AddPythonApp("pythonProject", tempDir.Path, scriptName)
+ .WithPoetry();
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ // Manually trigger BeforeStartEvent to wire up wait dependencies
+ await PublishBeforeStartEventAsync(app);
+
+ var pythonAppResource = appModel.Resources.OfType().Single();
+ var installerResource = appModel.Resources.OfType().Single();
+
+ var waitAnnotations = pythonAppResource.Annotations.OfType();
+ var waitForCompletionAnnotation = Assert.Single(waitAnnotations);
+ Assert.Equal(installerResource, waitForCompletionAnnotation.Resource);
+ Assert.Equal(WaitType.WaitForCompletion, waitForCompletionAnnotation.WaitType);
+ }
+
///
/// Helper method to manually trigger BeforeStartEvent for tests.
/// This is needed because BeforeStartEvent is normally triggered during StartAsync(),
From 4fc46a70eb24ab5e788ea8a7436d6596804e6b19 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 07:16:12 +0000
Subject: [PATCH 3/9] Update public API surface for WithPoetry method
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs b/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs
index 99aaa9a24da..030e6bf5266 100644
--- a/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs
+++ b/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs
@@ -13,6 +13,8 @@ public static partial class PythonAppResourceBuilderExtensions
public static ApplicationModel.IResourceBuilder AddPythonApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath, string virtualEnvironmentPath, params string[] scriptArgs) { throw null; }
public static ApplicationModel.IResourceBuilder AddPythonApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath, params string[] scriptArgs) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder WithPoetry(this ApplicationModel.IResourceBuilder builder, bool install = true, string[]? installArgs = null, (string key, string value)[]? env = null) where T : Python.PythonAppResource { throw null; }
}
[System.Obsolete("PythonProjectResource is deprecated. Please use PythonAppResource instead.")]
From bdcf493ebe56efa7f1e97ce9efea97581eeeb14c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 07:23:07 +0000
Subject: [PATCH 4/9] Address code review feedback - optimize package manager
checks
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PythonAppResourceBuilderExtensions.cs | 30 +++++++++----------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index 1234f109ad8..5a33d1fe8e4 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -479,16 +479,17 @@ private static IResourceBuilder AddPythonAppCore(
var entrypoint = entrypointAnnotation.Entrypoint;
// Check which package manager is being used
- var isUsingUv = context.Resource.TryGetLastAnnotation(out var pkgMgr) &&
- pkgMgr.ExecutableName == "uv";
- var isUsingPoetry = context.Resource.TryGetLastAnnotation(out pkgMgr) &&
- pkgMgr.ExecutableName == "poetry";
+ string? packageManagerName = null;
+ if (context.Resource.TryGetLastAnnotation(out var pkgMgr))
+ {
+ packageManagerName = pkgMgr.ExecutableName;
+ }
- if (isUsingUv)
+ if (packageManagerName == "uv")
{
GenerateUvDockerfile(context, resource, pythonVersion, entrypointType, entrypoint);
}
- else if (isUsingPoetry)
+ else if (packageManagerName == "poetry")
{
GeneratePoetryDockerfile(context, resource, pythonVersion, entrypointType, entrypoint);
}
@@ -1621,8 +1622,7 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w
pythonEnv.VirtualEnvironment != null)
{
var venvPath = pythonEnv.VirtualEnvironment.VirtualEnvironmentPath;
- var isAbsolutePath = Path.IsPathRooted(venvPath);
- var resolvedPath = isAbsolutePath
+ var resolvedPath = Path.IsPathRooted(venvPath)
? venvPath
: Path.GetFullPath(venvPath, builder.Resource.WorkingDirectory);
@@ -1791,14 +1791,14 @@ private static void SetupDependencies(IDistributedApplicationBuilder builder, Py
private static bool ShouldCreateVenv(IResourceBuilder builder) where T : PythonAppResource
{
- // Check if we're using uv or poetry (which handle venv creation themselves)
- var isUsingPackageManagerWithVenv = builder.Resource.TryGetLastAnnotation(out var pkgMgr) &&
- (pkgMgr.ExecutableName == "uv" || pkgMgr.ExecutableName == "poetry");
-
- if (isUsingPackageManagerWithVenv)
+ // Check if we're using a package manager that handles venv creation itself (uv, poetry)
+ if (builder.Resource.TryGetLastAnnotation(out var pkgMgr))
{
- // UV and Poetry handle venv creation, we don't need to create it
- return false;
+ // Package managers that handle their own venv creation
+ if (pkgMgr.ExecutableName is "uv" or "poetry")
+ {
+ return false;
+ }
}
// Get the virtual environment path
From 28782b4b6edee13c4646efd6ffc605e4f6ab9eae Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 08:01:10 +0000
Subject: [PATCH 5/9] Use dictionary instead of array for
PoetryEnvironmentAnnotation
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PoetryEnvironmentAnnotation.cs | 4 ++--
.../PythonAppResourceBuilderExtensions.cs | 11 ++++++++---
.../Aspire.Hosting.Python.Tests/AddPythonAppTests.cs | 6 +++---
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs b/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
index e689c62952b..b27a478b95b 100644
--- a/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
+++ b/src/Aspire.Hosting.Python/PoetryEnvironmentAnnotation.cs
@@ -9,10 +9,10 @@ namespace Aspire.Hosting.Python;
/// Represents environment variables to be set for Poetry operations.
///
/// The environment variables to set for Poetry.
-internal sealed class PoetryEnvironmentAnnotation((string key, string value)[] environmentVariables) : IResourceAnnotation
+internal sealed class PoetryEnvironmentAnnotation(IReadOnlyDictionary environmentVariables) : IResourceAnnotation
{
///
/// Gets the environment variables to be set for Poetry operations.
///
- public (string key, string value)[] EnvironmentVariables { get; } = environmentVariables;
+ public IReadOnlyDictionary EnvironmentVariables { get; } = environmentVariables;
}
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index 5a33d1fe8e4..35ff2508ea6 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -1485,7 +1485,12 @@ public static IResourceBuilder WithPoetry(
// Store Poetry environment variables in an annotation for later use
if (env != null && env.Length > 0)
{
- builder.WithAnnotation(new PoetryEnvironmentAnnotation(env), ResourceAnnotationMutationBehavior.Replace);
+ var envDict = new Dictionary(env.Length);
+ foreach (var (key, value) in env)
+ {
+ envDict[key] = value;
+ }
+ builder.WithAnnotation(new PoetryEnvironmentAnnotation(envDict), ResourceAnnotationMutationBehavior.Replace);
}
builder
@@ -1657,9 +1662,9 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w
// Apply user-specified environment variable overrides
if (builder.Resource.TryGetLastAnnotation(out var poetryEnv))
{
- foreach (var (key, value) in poetryEnv.EnvironmentVariables)
+ foreach (var kvp in poetryEnv.EnvironmentVariables)
{
- ctx.EnvironmentVariables[key] = value;
+ ctx.EnvironmentVariables[kvp.Key] = kvp.Value;
}
}
});
diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
index 851c0eb24e4..b362cd4c9c8 100644
--- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
+++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
@@ -2356,9 +2356,9 @@ public void WithPoetry_WithEnvironmentVariables_StoresAnnotation()
// Verify the Poetry environment annotation exists with the custom environment variables
Assert.True(pythonApp.Resource.TryGetLastAnnotation(out var poetryEnv));
- Assert.Equal(2, poetryEnv.EnvironmentVariables.Length);
- Assert.Contains(poetryEnv.EnvironmentVariables, e => e.key == "POETRY_VIRTUALENVS_IN_PROJECT" && e.value == "false");
- Assert.Contains(poetryEnv.EnvironmentVariables, e => e.key == "POETRY_HTTP_TIMEOUT" && e.value == "60");
+ Assert.Equal(2, poetryEnv.EnvironmentVariables.Count);
+ Assert.Equal("false", poetryEnv.EnvironmentVariables["POETRY_VIRTUALENVS_IN_PROJECT"]);
+ Assert.Equal("60", poetryEnv.EnvironmentVariables["POETRY_HTTP_TIMEOUT"]);
}
[Fact]
From 0e6d1009b9b800ec4da88e236362ddf719f23881 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 22 Nov 2025 17:01:45 +0000
Subject: [PATCH 6/9] Check IsReadOnly before adding installation managers to
service collection
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PythonAppResourceBuilderExtensions.cs | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index 35ff2508ea6..b3523073971 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -366,7 +366,10 @@ private static IResourceBuilder AddPythonAppCore(
ArgumentNullException.ThrowIfNull(virtualEnvironmentPath);
// Register Python environment validation services (once per builder)
- builder.Services.TryAddSingleton();
+ if (!builder.Services.IsReadOnly)
+ {
+ builder.Services.TryAddSingleton();
+ }
// 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;
@@ -1371,7 +1374,10 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
ArgumentNullException.ThrowIfNull(builder);
// Register UV validation service
- builder.ApplicationBuilder.Services.TryAddSingleton();
+ if (!builder.ApplicationBuilder.Services.IsReadOnly)
+ {
+ builder.ApplicationBuilder.Services.TryAddSingleton();
+ }
// Default args: sync only (uv will auto-detect Python and dependencies from pyproject.toml)
args ??= ["sync"];
@@ -1463,7 +1469,10 @@ public static IResourceBuilder WithPoetry(
ArgumentNullException.ThrowIfNull(builder);
// Register Poetry validation service
- builder.ApplicationBuilder.Services.TryAddSingleton();
+ if (!builder.ApplicationBuilder.Services.IsReadOnly)
+ {
+ builder.ApplicationBuilder.Services.TryAddSingleton();
+ }
// Ensure virtual environment exists - create default .venv if not configured
if (!builder.Resource.TryGetLastAnnotation(out var pythonEnv) ||
From f98188388221980035413a29d9cd32e966451893 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 22 Nov 2025 18:29:59 +0000
Subject: [PATCH 7/9] Don't set POETRY_VIRTUALENVS_IN_PROJECT or
POETRY_VIRTUALENVS_PATH by default
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PythonAppResourceBuilderExtensions.cs | 60 ++++---------------
1 file changed, 10 insertions(+), 50 deletions(-)
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index b3523073971..a43a7aba9ae 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -674,8 +674,7 @@ private static void GeneratePoetryDockerfile(DockerfileBuilderCallbackContext co
.Comment("Install Poetry")
.Run("pip install --no-cache-dir poetry")
.EmptyLine()
- .Comment("Configure Poetry to create virtual environments in the project directory")
- .Env("POETRY_VIRTUALENVS_IN_PROJECT", "true")
+ .Comment("Configure Poetry")
.Env("POETRY_NO_INTERACTION", "1")
.EmptyLine()
.WorkDir("/app")
@@ -1401,7 +1400,7 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
/// The resource builder.
/// When true (default), automatically runs poetry install before the application starts. When false, only sets the package manager annotation without creating an installer resource.
/// Additional arguments appended to the poetry install command (after --no-interaction).
- /// Extra environment variables applied to the Poetry restore step. These can override defaults like POETRY_VIRTUALENVS_IN_PROJECT.
+ /// Extra environment variables applied to the Poetry restore step. These can be used to configure Poetry behavior such as POETRY_VIRTUALENVS_IN_PROJECT or POETRY_VIRTUALENVS_PATH.
/// A reference to the for method chaining.
///
///
@@ -1410,13 +1409,9 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
///
///
/// Poetry (https://python-poetry.org/) is a Python package and dependency manager that handles virtual environment creation.
- /// By default, Aspire configures Poetry to create the virtual environment in the project directory (.venv) by setting
- /// POETRY_VIRTUALENVS_IN_PROJECT=true, unless an explicit virtual environment path is configured via
- /// or overridden via the parameter.
- ///
- ///
- /// When using to specify an explicit path, Aspire will set POETRY_VIRTUALENVS_PATH
- /// to direct Poetry to use that location.
+ /// Aspire does not set Poetry environment variables by default, allowing Poetry to use its own default configuration.
+ /// You can customize Poetry's behavior by passing environment variables via the parameter, such as
+ /// setting POETRY_VIRTUALENVS_IN_PROJECT=true to create the virtual environment in the project directory.
///
///
/// Calling this method will replace any previously configured package manager (such as pip or uv).
@@ -1447,12 +1442,12 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
///
///
///
- /// Disable Poetry's in-project virtual environment:
+ /// Configure Poetry's virtual environment location:
///
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var python = builder.AddPythonApp("api", "../python-api", "main.py")
- /// .WithPoetry(env: [("POETRY_VIRTUALENVS_IN_PROJECT", "false")])
+ /// .WithPoetry(env: [("POETRY_VIRTUALENVS_IN_PROJECT", "true")])
/// .WithHttpEndpoint(port: 5000);
///
/// builder.Build().Run();
@@ -1631,44 +1626,9 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w
{
installerBuilder.WithEnvironment(ctx =>
{
- // Determine default Poetry environment variables based on virtual environment configuration
- if (builder.Resource.TryGetLastAnnotation(out var pythonEnv) &&
- pythonEnv.VirtualEnvironment != null)
- {
- var venvPath = pythonEnv.VirtualEnvironment.VirtualEnvironmentPath;
- var resolvedPath = Path.IsPathRooted(venvPath)
- ? venvPath
- : Path.GetFullPath(venvPath, builder.Resource.WorkingDirectory);
-
- // Check if the venv path is within the project directory
- var projectVenvPath = Path.GetFullPath(".venv", builder.Resource.WorkingDirectory);
- var isInProject = string.Equals(
- resolvedPath,
- projectVenvPath,
- OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
-
- if (isInProject)
- {
- // Use in-project venv
- ctx.EnvironmentVariables["POETRY_VIRTUALENVS_IN_PROJECT"] = "true";
- }
- else
- {
- // Use external venv path
- var venvParentPath = Path.GetDirectoryName(resolvedPath);
- if (!string.IsNullOrEmpty(venvParentPath))
- {
- ctx.EnvironmentVariables["POETRY_VIRTUALENVS_PATH"] = venvParentPath;
- }
- }
- }
- else
- {
- // Default to in-project venv if no explicit path configured
- ctx.EnvironmentVariables["POETRY_VIRTUALENVS_IN_PROJECT"] = "true";
- }
-
- // Apply user-specified environment variable overrides
+ // Apply user-specified environment variable overrides only
+ // Do not set POETRY_VIRTUALENVS_IN_PROJECT or POETRY_VIRTUALENVS_PATH by default
+ // Let Poetry use its own defaults
if (builder.Resource.TryGetLastAnnotation(out var poetryEnv))
{
foreach (var kvp in poetryEnv.EnvironmentVariables)
From fd7a110218abcd2c3918c3840cc6d89a672fab5f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 23 Nov 2025 04:19:36 +0000
Subject: [PATCH 8/9] Set POETRY_VIRTUALENVS_IN_PROJECT in container to create
venv locally
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PythonAppResourceBuilderExtensions.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index a43a7aba9ae..25a4aada169 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -674,7 +674,8 @@ private static void GeneratePoetryDockerfile(DockerfileBuilderCallbackContext co
.Comment("Install Poetry")
.Run("pip install --no-cache-dir poetry")
.EmptyLine()
- .Comment("Configure Poetry")
+ .Comment("Configure Poetry to create virtual environments in the project directory")
+ .Env("POETRY_VIRTUALENVS_IN_PROJECT", "true")
.Env("POETRY_NO_INTERACTION", "1")
.EmptyLine()
.WorkDir("/app")
From e648bc641079cd5d191563e78676d69f4b0bc12e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 23 Nov 2025 06:37:37 +0000
Subject: [PATCH 9/9] Register all installation managers upfront in
AddPythonAppCore
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
---
.../PythonAppResourceBuilderExtensions.cs | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
index 25a4aada169..c87138cb2fa 100644
--- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
@@ -369,6 +369,8 @@ private static IResourceBuilder AddPythonAppCore(
if (!builder.Services.IsReadOnly)
{
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
}
// When using the default virtual environment path, look for existing virtual environments
// in multiple locations: app directory first, then AppHost directory as fallback
@@ -1373,12 +1375,6 @@ public static IResourceBuilder WithUv(this IResourceBuilder builder, bo
{
ArgumentNullException.ThrowIfNull(builder);
- // Register UV validation service
- if (!builder.ApplicationBuilder.Services.IsReadOnly)
- {
- builder.ApplicationBuilder.Services.TryAddSingleton();
- }
-
// Default args: sync only (uv will auto-detect Python and dependencies from pyproject.toml)
args ??= ["sync"];
@@ -1464,12 +1460,6 @@ public static IResourceBuilder WithPoetry(
{
ArgumentNullException.ThrowIfNull(builder);
- // Register Poetry validation service
- if (!builder.ApplicationBuilder.Services.IsReadOnly)
- {
- builder.ApplicationBuilder.Services.TryAddSingleton();
- }
-
// Ensure virtual environment exists - create default .venv if not configured
if (!builder.Resource.TryGetLastAnnotation(out var pythonEnv) ||
pythonEnv.VirtualEnvironment is null)