🌐 AI搜索 & 代理 主页
Skip to content

Commit 3426bfd

Browse files
authored
Use uv and derive as much as possible from the environment, if available (#2652)
* Use uv * Derive program name from venv if possible * Implement venv derivation
1 parent 7d3a6dc commit 3426bfd

File tree

6 files changed

+512
-54
lines changed

6 files changed

+512
-54
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,14 @@ jobs:
6262
dotnet-version: '8.0.x'
6363

6464
- name: Set up Python ${{ matrix.python }}
65-
uses: actions/setup-python@v6
65+
uses: astral-sh/setup-uv@v6
6666
with:
67-
python-version: ${{ matrix.python }}
6867
architecture: ${{ matrix.os.platform }}
69-
70-
- name: Install dependencies
71-
run: |
72-
pip install --upgrade -r requirements.txt
73-
pip install numpy # for tests
74-
75-
- name: Build and Install
76-
run: |
77-
pip install -v .
78-
79-
- name: Set Python DLL path and PYTHONHOME (non Windows)
80-
if: ${{ matrix.os.category != 'windows' }}
81-
run: |
82-
echo PYTHONNET_PYDLL=$(python -m find_libpython) >> $GITHUB_ENV
83-
echo PYTHONHOME=$(python -c 'import sys; print(sys.prefix)') >> $GITHUB_ENV
84-
85-
- name: Set Python DLL path and PYTHONHOME (Windows)
86-
if: ${{ matrix.os.category == 'windows' }}
87-
run: |
88-
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(python -m find_libpython)"
89-
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')"
68+
python-version: ${{ matrix.python }}
69+
activate-environment: true
70+
enable-cache: true
9071

9172
- name: Embedding tests
92-
if: ${{ matrix.python != '3.13' }}
9373
run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/
9474
env:
9575
MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466
@@ -108,7 +88,6 @@ jobs:
10888
run: pytest --runtime netfx
10989

11090
- name: Python tests run from .NET
111-
if: ${{ matrix.python != '3.13' }}
112-
run: dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/
91+
run: uv run dotnet test --runtime any-${{ matrix.os.platform }} src/python_tests_runner/
11392

11493
# TODO: Run mono tests on Windows?

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<TargetFrameworks>netstandard2.0</TargetFrameworks>
44
<Platforms>AnyCPU</Platforms>
5-
<LangVersion>10.0</LangVersion>
5+
<LangVersion>10</LangVersion>
66
<RootNamespace>Python.Runtime</RootNamespace>
77
<AssemblyName>Python.Runtime</AssemblyName>
88
<Nullable>enable</Nullable>

src/runtime/Runtime.cs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using System.Threading;
77
using System.Collections.Generic;
8+
using System.IO;
89
using Python.Runtime.Native;
910
using System.Linq;
1011
using static System.FormattableString;
@@ -18,40 +19,20 @@ namespace Python.Runtime
1819
/// </summary>
1920
public unsafe partial class Runtime
2021
{
22+
internal static PythonEnvironment PythonEnvironment = PythonEnvironment.FromEnv();
23+
2124
public static string? PythonDLL
2225
{
2326
get => _PythonDll;
2427
set
2528
{
2629
if (_isInitialized)
2730
throw new InvalidOperationException("This property must be set before runtime is initialized");
28-
_PythonDll = value;
31+
PythonEnvironment.LibPython = value;
2932
}
3033
}
3134

32-
static string? _PythonDll = GetDefaultDllName();
33-
private static string? GetDefaultDllName()
34-
{
35-
string dll = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");
36-
if (dll is not null) return dll;
37-
38-
string verString = Environment.GetEnvironmentVariable("PYTHONNET_PYVER");
39-
if (!Version.TryParse(verString, out var version)) return null;
40-
41-
return GetDefaultDllName(version);
42-
}
43-
44-
private static string GetDefaultDllName(Version version)
45-
{
46-
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
47-
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
48-
? Invariant($"{version.Major}{version.Minor}")
49-
: Invariant($"{version.Major}.{version.Minor}");
50-
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
51-
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
52-
: ".so";
53-
return prefix + "python" + suffix + ext;
54-
}
35+
static string? _PythonDll => PythonEnvironment.LibPython;
5536

5637
private static bool _isInitialized = false;
5738
internal static bool IsInitialized => _isInitialized;
@@ -96,6 +77,18 @@ internal static int GetRun()
9677
return runNumber;
9778
}
9879

80+
static void EnsureProgramName()
81+
{
82+
if (!string.IsNullOrEmpty(PythonEngine.ProgramName))
83+
return;
84+
85+
if (PythonEnvironment.IsValid)
86+
{
87+
PythonEngine.ProgramName = PythonEnvironment.ProgramName!;
88+
return;
89+
}
90+
}
91+
9992
internal static bool HostedInPython;
10093
internal static bool ProcessIsTerminating;
10194

@@ -117,6 +110,8 @@ internal static void Initialize(bool initSigs = false)
117110
);
118111
if (!interpreterAlreadyInitialized)
119112
{
113+
EnsureProgramName();
114+
120115
Py_InitializeEx(initSigs ? 1 : 0);
121116

122117
NewRun();
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System;
2+
using System.IO;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using static System.FormattableString;
7+
8+
namespace Python.Runtime;
9+
10+
11+
internal class PythonEnvironment
12+
{
13+
readonly static string PYDLL_ENV_VAR = "PYTHONNET_PYDLL";
14+
readonly static string PYEXE_ENV_VAR = "PYTHONNET_PYEXE";
15+
readonly static string PYNET_VENV_ENV_VAR = "PYTHONNET_VENV";
16+
readonly static string VENV_ENV_VAR = "VIRTUAL_ENV";
17+
18+
public string? VenvPath { get; private set; }
19+
public string? Home { get; private set; }
20+
public Version? Version { get; private set; }
21+
public string? ProgramName { get; set; }
22+
public string? LibPython { get; set; }
23+
24+
public bool IsValid =>
25+
!string.IsNullOrEmpty(ProgramName) && !string.IsNullOrEmpty(LibPython);
26+
27+
28+
// TODO: Move the lib-guessing step to separate function, use together with
29+
// PYTHONNET_PYEXE or a path lookup as last resort
30+
31+
// Initialize PythonEnvironment instance from environment variables.
32+
//
33+
// If PYTHONNET_PYEXE and PYTHONNET_PYDLL are set, these always have precedence.
34+
// If PYTHONNET_VENV or VIRTUAL_ENV is set, we interpret the environment as a venv
35+
// and set the ProgramName/LibPython accordingly. PYTHONNET_VENV takes precedence.
36+
public static PythonEnvironment FromEnv()
37+
{
38+
var pydll = Environment.GetEnvironmentVariable(PYDLL_ENV_VAR);
39+
var pydllSet = !string.IsNullOrEmpty(pydll);
40+
var pyexe = Environment.GetEnvironmentVariable(PYEXE_ENV_VAR);
41+
var pyexeSet = !string.IsNullOrEmpty(pyexe);
42+
var pynetVenv = Environment.GetEnvironmentVariable(PYNET_VENV_ENV_VAR);
43+
var pynetVenvSet = !string.IsNullOrEmpty(pynetVenv);
44+
var venv = Environment.GetEnvironmentVariable(VENV_ENV_VAR);
45+
var venvSet = !string.IsNullOrEmpty(venv);
46+
47+
PythonEnvironment? res = new();
48+
49+
if (pynetVenvSet)
50+
res = FromVenv(pynetVenv) ?? res;
51+
else if (venvSet)
52+
res = FromVenv(venv) ?? res;
53+
54+
if (pyexeSet)
55+
res.ProgramName = pyexe;
56+
57+
if (pydllSet)
58+
res.LibPython = pydll;
59+
60+
return res;
61+
}
62+
63+
public static PythonEnvironment? FromVenv(string path)
64+
{
65+
var env = new PythonEnvironment
66+
{
67+
VenvPath = path
68+
};
69+
70+
string venvCfg = Path.Combine(path, "pyvenv.cfg");
71+
72+
if (!File.Exists(venvCfg))
73+
return null;
74+
75+
var settings = TryParse(venvCfg);
76+
77+
if (!settings.ContainsKey("home"))
78+
return null;
79+
80+
env.Home = settings["home"];
81+
var pname = ProgramNameFromPath(path);
82+
if (File.Exists(pname))
83+
env.ProgramName = pname;
84+
85+
if (settings.TryGetValue("version", out string versionStr))
86+
{
87+
_ = Version.TryParse(versionStr, out Version versionObj);
88+
env.Version = versionObj;
89+
}
90+
else if (settings.TryGetValue("version_info", out versionStr))
91+
{
92+
_ = Version.TryParse(versionStr, out Version versionObj);
93+
env.Version = versionObj;
94+
}
95+
96+
env.LibPython = FindLibPython(env.Home, env.Version);
97+
98+
return env;
99+
}
100+
101+
private static Dictionary<string, string> TryParse(string venvCfg)
102+
{
103+
var settings = new Dictionary<string, string>();
104+
105+
string[] lines = File.ReadAllLines(venvCfg);
106+
107+
// The actually used format is really primitive: "<key> = <value>"
108+
foreach (string line in lines)
109+
{
110+
var split = line.Split(new[] { '=' }, 2);
111+
112+
if (split.Length != 2)
113+
continue;
114+
115+
settings[split[0].Trim()] = split[1].Trim();
116+
}
117+
118+
return settings;
119+
}
120+
121+
private static string? FindLibPython(string home, Version? maybeVersion)
122+
{
123+
// TODO: Check whether there is a .dll/.so/.dylib next to the executable
124+
125+
if (maybeVersion is Version version)
126+
{
127+
return FindLibPythonInHome(home, version);
128+
}
129+
130+
return null;
131+
}
132+
133+
private static string? FindLibPythonInHome(string home, Version version)
134+
{
135+
var libPythonName = GetDefaultDllName(version);
136+
137+
List<string> pathsToCheck = new();
138+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
139+
{
140+
var arch = RuntimeInformation.ProcessArchitecture;
141+
if (arch == Architecture.X64 || arch == Architecture.Arm64)
142+
{
143+
// multilib systems
144+
pathsToCheck.Add("../lib64");
145+
}
146+
pathsToCheck.Add("../lib");
147+
}
148+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
149+
{
150+
pathsToCheck.Add(".");
151+
}
152+
else
153+
{
154+
pathsToCheck.Add("../lib");
155+
}
156+
157+
return pathsToCheck
158+
.Select(path => Path.Combine(home, path, libPythonName))
159+
.FirstOrDefault(File.Exists);
160+
}
161+
162+
private static string ProgramNameFromPath(string path)
163+
{
164+
if (Runtime.IsWindows)
165+
{
166+
return Path.Combine(path, "Scripts", "python.exe");
167+
}
168+
else
169+
{
170+
return Path.Combine(path, "bin", "python");
171+
}
172+
}
173+
174+
internal static string GetDefaultDllName(Version version)
175+
{
176+
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
177+
178+
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
179+
? Invariant($"{version.Major}{version.Minor}")
180+
: Invariant($"{version.Major}.{version.Minor}");
181+
182+
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
183+
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
184+
: ".so";
185+
186+
return prefix + "python" + suffix + ext;
187+
}
188+
}

tools/geninterop/geninterop.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
3-
1+
#!/usr/bin/env uv run
2+
# /// script
3+
# dependencies = ["pycparser"]
4+
# ///
45
"""
56
TypeOffset is a C# class that mirrors the in-memory layout of heap
67
allocated Python objects.

0 commit comments

Comments
 (0)