Skip to content

Commit 3c0b895

Browse files
Copilotadamintdavidfowl
authored
Fix AddPythonApp parameter ambiguity using OverloadResolutionPriority (#11892)
* Initial plan * Fix AddPythonApp parameter ambiguity by introducing IEnumerable<string> overload Co-authored-by: adamint <[email protected]> * Update API surface file for AddPythonApp changes Co-authored-by: adamint <[email protected]> * Implement OverloadResolutionPriority pattern with deprecated overloads and WithScriptArgs Co-authored-by: davidfowl <[email protected]> * Suppress obsolete warnings in tests Co-authored-by: davidfowl <[email protected]> * Remove WithScriptArgs method per review feedback - callers can use WithArgs Co-authored-by: davidfowl <[email protected]> * Refactor AddPythonAppCore to not take scriptArgs - use WithArgs instead Co-authored-by: davidfowl <[email protected]> * Implement WithVirtualEnvironment using WithCommand to update venv after resource creation Co-authored-by: davidfowl <[email protected]> * Update src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs * Fix typo in ApplicationBuilder reference * Update src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs * Add CompatibilitySuppressions.xml and refactor Python resource handling - Introduced CompatibilitySuppressions.xml to manage diagnostic suppressions. - Refactored PythonAppResourceBuilderExtensions to use GetExecutable instead of GetRequiredExecutable for better flexibility. - Updated virtual environment handling in WithVirtualEnvironment method. - Removed deprecated PythonProjectResource and PythonProjectResourceBuilderExtensions classes. * Remove deprecated PythonProjectResource tests and update references to PythonAppResource * Update example usage and improve null argument checks in PythonAppResourceBuilderExtensions * Add unit tests for AddPythonApp method to validate virtual environment handling --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: adamint <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: David Fowler <[email protected]>
1 parent 234510b commit 3c0b895

File tree

8 files changed

+255
-674
lines changed

8 files changed

+255
-674
lines changed

src/Aspire.Hosting.Python/Aspire.Hosting.Python.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<Compile Include="$(SharedDir)PathNormalizer.cs" Link="Utils\PathNormalizer.cs" />
12+
<Compile Include="$(SharedDir)OverloadResolutionPriorityAttribute.cs" Link="Utils\OverloadResolutionPriorityAttribute.cs" />
1213
</ItemGroup>
1314

1415
<ItemGroup>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
3+
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
4+
<Suppression>
5+
<DiagnosticId>CP0001</DiagnosticId>
6+
<Target>T:Aspire.Hosting.Python.PythonProjectResource</Target>
7+
<Left>lib/net8.0/Aspire.Hosting.Python.dll</Left>
8+
<Right>lib/net8.0/Aspire.Hosting.Python.dll</Right>
9+
<IsBaselineSuppression>true</IsBaselineSuppression>
10+
</Suppression>
11+
<Suppression>
12+
<DiagnosticId>CP0001</DiagnosticId>
13+
<Target>T:Aspire.Hosting.PythonProjectResourceBuilderExtensions</Target>
14+
<Left>lib/net8.0/Aspire.Hosting.Python.dll</Left>
15+
<Right>lib/net8.0/Aspire.Hosting.Python.dll</Right>
16+
<IsBaselineSuppression>true</IsBaselineSuppression>
17+
</Suppression>
18+
</Suppressions>
Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.ComponentModel;
5+
using System.Runtime.CompilerServices;
46
#pragma warning disable ASPIREEXTENSION001
57
using Aspire.Hosting.ApplicationModel;
68
using Aspire.Hosting.Python;
@@ -14,25 +16,22 @@ namespace Aspire.Hosting;
1416
public static class PythonAppResourceBuilderExtensions
1517
{
1618
/// <summary>
17-
/// Adds a python application with a virtual environment to the application model.
19+
/// Adds a python application to the application model.
1820
/// </summary>
1921
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
2022
/// <param name="name">The name of the resource.</param>
2123
/// <param name="appDirectory">The path to the directory containing the python app files.</param>
2224
/// <param name="scriptPath">The path to the script relative to the app directory to run.</param>
23-
/// <param name="scriptArgs">The arguments for the script.</param>
2425
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
2526
/// <remarks>
2627
/// <para>
27-
/// The virtual environment must be initialized before running the app. By default the virtual environment folder is expected
28-
/// to be named <c>.venv</c> and be located in the app directory. If the virtual environment is located in a different directory
29-
/// this default can be specified by using the <see cref="AddPythonApp(IDistributedApplicationBuilder, string, string, string, string, string[])"/>
30-
/// overload of this method.
28+
/// By default, the virtual environment folder is expected to be named <c>.venv</c> and located in the app directory.
29+
/// Use <see cref="WithVirtualEnvironment(IResourceBuilder{PythonAppResource}, string)"/> to specify a different virtual environment path.
30+
/// Use <c>WithArgs</c> to pass arguments to the script.
3131
/// </para>
3232
/// <para>
33-
/// The virtual environment is setup individually for each app to allow each app to use a different version of
34-
/// Python and dependencies. To setup a virtual environment use the <c>python -m venv .venv</c> command in the app
35-
/// directory. This will create a virtual environment in the <c>.venv</c> directory.
33+
/// The virtual environment must be initialized before running the app. To setup a virtual environment use the
34+
/// <c>python -m venv .venv</c> command in the app directory.
3635
/// </para>
3736
/// <para>
3837
/// To restore dependencies in the virtual environment first activate the environment by executing the activation
@@ -44,23 +43,49 @@ public static class PythonAppResourceBuilderExtensions
4443
/// your Python app.
4544
/// </para>
4645
/// <example>
47-
/// Add a python app or executable to the application model. In this example the python code entry point is located in the <c>PythonApp</c> directory
48-
/// if this path is relative then it is assumed to be relative to the AppHost directory, and the virtual environment path if relative
49-
/// is relative to the app directory. In the example below, if the app host directory is <c>$HOME/repos/MyApp/src/MyApp.AppHost</c> then
50-
/// the AppPath would be <c>$HOME/repos/MyApp/src/MyApp.AppHost/PythonApp</c> and the virtual environment path (defaulted) would
51-
/// be <c>$HOME/repos/MyApp/src/MyApp.AppHost/PythonApp/.venv</c>.
46+
/// Add a python app to the application model:
5247
/// <code lang="csharp">
5348
/// var builder = DistributedApplication.CreateBuilder(args);
5449
///
55-
/// builder.AddPythonApp("python-app", "PythonApp", "main.py");
50+
/// builder.AddPythonApp("python-app", "../python-app", "main.py")
51+
/// .WithArgs("arg1", "arg2");
5652
///
5753
/// builder.Build().Run();
5854
/// </code>
5955
/// </example>
6056
/// </remarks>
57+
[OverloadResolutionPriority(1)]
58+
public static IResourceBuilder<PythonAppResource> AddPythonApp(
59+
this IDistributedApplicationBuilder builder, [ResourceName] string name, string appDirectory, string scriptPath)
60+
=> AddPythonAppCore(builder, name, appDirectory, scriptPath, ".venv");
61+
62+
/// <summary>
63+
/// Adds a python application with a virtual environment to the application model.
64+
/// </summary>
65+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
66+
/// <param name="name">The name of the resource.</param>
67+
/// <param name="appDirectory">The path to the directory containing the python app files.</param>
68+
/// <param name="scriptPath">The path to the script relative to the app directory to run.</param>
69+
/// <param name="scriptArgs">The arguments for the script.</param>
70+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
71+
/// <remarks>
72+
/// <para>
73+
/// This overload is obsolete. Use the overload without parameters and chain with <c>WithArgs</c>:
74+
/// <code lang="csharp">
75+
/// builder.AddPythonApp("name", "dir", "script.py")
76+
/// .WithArgs("arg1", "arg2");
77+
/// </code>
78+
/// </para>
79+
/// </remarks>
80+
[Obsolete("Use AddPythonApp(builder, name, appDirectory, scriptPath) and chain with .WithArgs(...) instead.")]
81+
[EditorBrowsable(EditorBrowsableState.Never)]
6182
public static IResourceBuilder<PythonAppResource> AddPythonApp(
6283
this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath, params string[] scriptArgs)
63-
=> AddPythonApp(builder, name, appDirectory, scriptPath, ".venv", scriptArgs);
84+
{
85+
ThrowIfNullOrContainsIsNullOrEmpty(scriptArgs);
86+
return AddPythonAppCore(builder, name, appDirectory, scriptPath, ".venv")
87+
.WithArgs(scriptArgs);
88+
}
6489

6590
/// <summary>
6691
/// Adds a python application with a virtual environment to the application model.
@@ -74,58 +99,47 @@ public static IResourceBuilder<PythonAppResource> AddPythonApp(
7499
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
75100
/// <remarks>
76101
/// <para>
77-
/// The virtual environment is setup individually for each app to allow each app to use a different version of
78-
/// Python and dependencies. To setup a virtual environment use the <c>python -m venv .venv</c> command in the app
79-
/// directory. This will create a virtual environment in the <c>.venv</c> directory (where <c>.venv</c> is the name of your
80-
/// virtual environment directory).
81-
/// </para>
82-
/// <para>
83-
/// To restore dependencies in the virtual environment first activate the environment by executing the activation
84-
/// script and then use the <c>pip install -r requirements.txt</c> command to restore dependencies.
85-
/// </para>
86-
/// <para>
87-
/// To receive traces, logs, and metrics from the python app in the dashboard, the app must be instrumented with OpenTelemetry.
88-
/// You can instrument your app by adding the <c>opentelemetry-distro</c>, and <c>opentelemetry-exporter-otlp</c> to
89-
/// your Python app.
90-
/// </para>
91-
/// <example>
92-
/// Add a python app or executable to the application model. In this example the python code is located in the <c>PythonApp</c> directory
93-
/// if this path is relative then it is assumed to be relative to the AppHost directory, and the virtual environment path if relative
94-
/// is relative to the app directory. In the example below, if the app host directory is <c>$HOME/repos/MyApp/src/MyApp.AppHost</c> then
95-
/// the AppPath would be <c>$HOME/repos/MyApp/src/MyApp.AppHost/PythonApp</c> and the virtual environment path (defaulted) would
96-
/// be <c>$HOME/repos/MyApp/src/MyApp.AppHost/PythonApp/.venv</c>.
102+
/// This overload is obsolete. Use the overload without parameters and chain with <c>WithVirtualEnvironment</c> and <c>WithArgs</c>:
97103
/// <code lang="csharp">
98-
/// var builder = DistributedApplication.CreateBuilder(args);
99-
///
100-
/// builder.AddPythonApp("python-app", "PythonApp", "main.py");
101-
///
102-
/// builder.Build().Run();
104+
/// builder.AddPythonApp("name", "dir", "script.py")
105+
/// .WithVirtualEnvironment("myenv")
106+
/// .WithArgs("arg1", "arg2");
103107
/// </code>
104-
/// </example>
108+
/// </para>
105109
/// </remarks>
110+
[Obsolete("Use AddPythonApp(builder, name, appDirectory, scriptPath) and chain with .WithVirtualEnvironment(...).WithArgs(...) instead.")]
111+
[EditorBrowsable(EditorBrowsableState.Never)]
106112
public static IResourceBuilder<PythonAppResource> AddPythonApp(
107113
this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath,
108114
string virtualEnvironmentPath, params string[] scriptArgs)
115+
{
116+
ThrowIfNullOrContainsIsNullOrEmpty(scriptArgs);
117+
return AddPythonAppCore(builder, name, appDirectory, scriptPath, virtualEnvironmentPath)
118+
.WithArgs(scriptArgs);
119+
}
120+
121+
private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
122+
IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath,
123+
string virtualEnvironmentPath)
109124
{
110125
ArgumentNullException.ThrowIfNull(builder);
111126
ArgumentException.ThrowIfNullOrEmpty(name);
112-
ArgumentException.ThrowIfNullOrEmpty(appDirectory);
127+
ArgumentNullException.ThrowIfNull(appDirectory);
113128
ArgumentException.ThrowIfNullOrEmpty(scriptPath);
114-
ArgumentException.ThrowIfNullOrEmpty(virtualEnvironmentPath);
115-
ThrowIfNullOrContainsIsNullOrEmpty(scriptArgs);
129+
ArgumentNullException.ThrowIfNull(virtualEnvironmentPath);
116130

117131
appDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, appDirectory));
118132
var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath)
119133
? virtualEnvironmentPath
120134
: Path.Join(appDirectory, virtualEnvironmentPath));
121135

122-
var pythonExecutable = virtualEnvironment.GetRequiredExecutable("python");
136+
var pythonExecutable = virtualEnvironment.GetExecutable("python");
123137

124138
var resource = new PythonAppResource(name, pythonExecutable, appDirectory);
125139

126140
var resourceBuilder = builder.AddResource(resource).WithArgs(context =>
127141
{
128-
AddArguments(scriptPath, scriptArgs, context);
142+
context.Args.Add(scriptPath);
129143
});
130144

131145
resourceBuilder.WithOtlpExporter();
@@ -153,16 +167,6 @@ public static IResourceBuilder<PythonAppResource> AddPythonApp(
153167
return resourceBuilder;
154168
}
155169

156-
private static void AddArguments(string scriptPath, string[] scriptArgs, CommandLineArgsCallbackContext context)
157-
{
158-
context.Args.Add(scriptPath);
159-
160-
foreach (var arg in scriptArgs)
161-
{
162-
context.Args.Add(arg);
163-
}
164-
}
165-
166170
private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs)
167171
{
168172
ArgumentNullException.ThrowIfNull(scriptArgs);
@@ -179,4 +183,25 @@ private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs)
179183
}
180184
}
181185
}
186+
187+
/// <summary>
188+
/// Configures a custom virtual environment path for the Python application.
189+
/// </summary>
190+
/// <param name="builder">The resource builder.</param>
191+
/// <param name="virtualEnvironmentPath">The path to the virtual environment. Can be absolute or relative to the app directory.</param>
192+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
193+
public static IResourceBuilder<PythonAppResource> WithVirtualEnvironment(
194+
this IResourceBuilder<PythonAppResource> builder, string virtualEnvironmentPath)
195+
{
196+
ArgumentNullException.ThrowIfNull(builder);
197+
ArgumentException.ThrowIfNullOrEmpty(virtualEnvironmentPath);
198+
199+
var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath)
200+
? virtualEnvironmentPath
201+
: Path.Join(builder.Resource.WorkingDirectory, virtualEnvironmentPath));
202+
// Update the command to use the new virtual environment
203+
builder.WithCommand(virtualEnvironment.GetExecutable("python"));
204+
205+
return builder;
206+
}
182207
}

src/Aspire.Hosting.Python/PythonProjectResource.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)