Repository navigation
Expand file tree
/
Copy pathPythonVersionDetector.cs
More file actions
117 lines (101 loc) · 4.01 KB
/
Copy pathPythonVersionDetector.cs
File metadata and controls
117 lines (101 loc) · 4.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Aspire.Hosting.Python;
internal static partial class PythonVersionDetector
{
/// <summary>
/// Detects the Python version from .python-version file, pyproject.toml, or virtual environment.
/// </summary>
/// <param name="appDirectory">The directory containing the Python application.</param>
/// <param name="virtualEnvironment">The virtual environment to check as a fallback.</param>
/// <returns>The detected Python version in major.minor format (e.g., "3.13"), or null if not found.</returns>
public static string? DetectVersion(string appDirectory, VirtualEnvironment? virtualEnvironment)
{
// First, try .python-version file (most specific)
var pythonVersionFile = Path.Combine(appDirectory, ".python-version");
if (File.Exists(pythonVersionFile))
{
var version = File.ReadAllText(pythonVersionFile).Trim();
if (!string.IsNullOrWhiteSpace(version))
{
return version;
}
}
// Second, try pyproject.toml
var pyprojectFile = Path.Combine(appDirectory, "pyproject.toml");
if (File.Exists(pyprojectFile))
{
var content = File.ReadAllText(pyprojectFile);
// Look for requires-python = ">=X.Y" or "==X.Y"
var match = RequiresPythonRegex().Match(content);
if (match.Success)
{
return match.Groups[1].Value;
}
}
// Third, try detecting from virtual environment as ultimate fallback
if (virtualEnvironment != null)
{
return DetectVersionFromVirtualEnvironment(virtualEnvironment);
}
return null;
}
/// <summary>
/// Detects the Python version by executing the Python executable from the virtual environment.
/// </summary>
/// <param name="virtualEnvironment">The virtual environment.</param>
/// <returns>The detected Python version in major.minor format, or null if detection fails.</returns>
private static string? DetectVersionFromVirtualEnvironment(VirtualEnvironment virtualEnvironment)
{
var pythonExecutable = virtualEnvironment.GetExecutable("python");
if (!File.Exists(pythonExecutable))
{
return null;
}
try
{
var startInfo = new ProcessStartInfo
{
FileName = pythonExecutable,
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process is null)
{
return null;
}
if (!process.WaitForExit(5000))
{
return null;
}
// Python 2.x outputs to stderr, Python 3.x to stdout
var output = process.StandardOutput.ReadToEnd();
if (string.IsNullOrWhiteSpace(output))
{
output = process.StandardError.ReadToEnd();
}
// Parse "Python X.Y.Z" format
var match = PythonVersionOutputRegex().Match(output);
if (match.Success && match.Groups.Count > 2)
{
return $"{match.Groups[1].Value}.{match.Groups[2].Value}";
}
}
catch
{
// Ignore errors during version detection
return null;
}
return null;
}
[GeneratedRegex(@"requires-python\s*=\s*[""'](?:>=|==)?(\d+\.\d+)", RegexOptions.IgnoreCase)]
private static partial Regex RequiresPythonRegex();
[GeneratedRegex(@"Python\s+(\d+)\.(\d+)", RegexOptions.IgnoreCase)]
private static partial Regex PythonVersionOutputRegex();
}