🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 5 additions & 26 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,14 @@ jobs:
dotnet-version: '8.0.x'

- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v6
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.os.platform }}

- name: Install dependencies
run: |
pip install --upgrade -r requirements.txt
pip install numpy # for tests

- name: Build and Install
run: |
pip install -v .

- name: Set Python DLL path and PYTHONHOME (non Windows)
if: ${{ matrix.os.category != 'windows' }}
run: |
echo PYTHONNET_PYDLL=$(python -m find_libpython) >> $GITHUB_ENV
echo PYTHONHOME=$(python -c 'import sys; print(sys.prefix)') >> $GITHUB_ENV

- name: Set Python DLL path and PYTHONHOME (Windows)
if: ${{ matrix.os.category == 'windows' }}
run: |
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONNET_PYDLL=$(python -m find_libpython)"
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -InputObject "PYTHONHOME=$(python -c 'import sys; print(sys.prefix)')"
python-version: ${{ matrix.python }}
activate-environment: true
enable-cache: true

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

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

# TODO: Run mono tests on Windows?
2 changes: 1 addition & 1 deletion src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Platforms>AnyCPU</Platforms>
<LangVersion>10.0</LangVersion>
<LangVersion>10</LangVersion>
<RootNamespace>Python.Runtime</RootNamespace>
<AssemblyName>Python.Runtime</AssemblyName>
<Nullable>enable</Nullable>
Expand Down
43 changes: 19 additions & 24 deletions src/runtime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;
using System.Threading;
using System.Collections.Generic;
using System.IO;
using Python.Runtime.Native;
using System.Linq;
using static System.FormattableString;
Expand All @@ -18,40 +19,20 @@ namespace Python.Runtime
/// </summary>
public unsafe partial class Runtime
{
internal static PythonEnvironment PythonEnvironment = PythonEnvironment.FromEnv();

public static string? PythonDLL
{
get => _PythonDll;
set
{
if (_isInitialized)
throw new InvalidOperationException("This property must be set before runtime is initialized");
_PythonDll = value;
PythonEnvironment.LibPython = value;
}
}

static string? _PythonDll = GetDefaultDllName();
private static string? GetDefaultDllName()
{
string dll = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");
if (dll is not null) return dll;

string verString = Environment.GetEnvironmentVariable("PYTHONNET_PYVER");
if (!Version.TryParse(verString, out var version)) return null;

return GetDefaultDllName(version);
}

private static string GetDefaultDllName(Version version)
{
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Invariant($"{version.Major}{version.Minor}")
: Invariant($"{version.Major}.{version.Minor}");
string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
: ".so";
return prefix + "python" + suffix + ext;
}
static string? _PythonDll => PythonEnvironment.LibPython;

private static bool _isInitialized = false;
internal static bool IsInitialized => _isInitialized;
Expand Down Expand Up @@ -96,6 +77,18 @@ internal static int GetRun()
return runNumber;
}

static void EnsureProgramName()
{
if (!string.IsNullOrEmpty(PythonEngine.ProgramName))
return;

if (PythonEnvironment.IsValid)
{
PythonEngine.ProgramName = PythonEnvironment.ProgramName!;
return;
}
}

internal static bool HostedInPython;
internal static bool ProcessIsTerminating;

Expand All @@ -117,6 +110,8 @@ internal static void Initialize(bool initSigs = false)
);
if (!interpreterAlreadyInitialized)
{
EnsureProgramName();

Py_InitializeEx(initSigs ? 1 : 0);

NewRun();
Expand Down
188 changes: 188 additions & 0 deletions src/runtime/Util/PythonEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using static System.FormattableString;

namespace Python.Runtime;


internal class PythonEnvironment
{
readonly static string PYDLL_ENV_VAR = "PYTHONNET_PYDLL";
readonly static string PYEXE_ENV_VAR = "PYTHONNET_PYEXE";
readonly static string PYNET_VENV_ENV_VAR = "PYTHONNET_VENV";
readonly static string VENV_ENV_VAR = "VIRTUAL_ENV";

public string? VenvPath { get; private set; }
public string? Home { get; private set; }
public Version? Version { get; private set; }
public string? ProgramName { get; set; }
public string? LibPython { get; set; }

public bool IsValid =>
!string.IsNullOrEmpty(ProgramName) && !string.IsNullOrEmpty(LibPython);


// TODO: Move the lib-guessing step to separate function, use together with
// PYTHONNET_PYEXE or a path lookup as last resort

// Initialize PythonEnvironment instance from environment variables.
//
// If PYTHONNET_PYEXE and PYTHONNET_PYDLL are set, these always have precedence.
// If PYTHONNET_VENV or VIRTUAL_ENV is set, we interpret the environment as a venv
// and set the ProgramName/LibPython accordingly. PYTHONNET_VENV takes precedence.
public static PythonEnvironment FromEnv()
{
var pydll = Environment.GetEnvironmentVariable(PYDLL_ENV_VAR);
var pydllSet = !string.IsNullOrEmpty(pydll);
var pyexe = Environment.GetEnvironmentVariable(PYEXE_ENV_VAR);
var pyexeSet = !string.IsNullOrEmpty(pyexe);
var pynetVenv = Environment.GetEnvironmentVariable(PYNET_VENV_ENV_VAR);
var pynetVenvSet = !string.IsNullOrEmpty(pynetVenv);
var venv = Environment.GetEnvironmentVariable(VENV_ENV_VAR);
var venvSet = !string.IsNullOrEmpty(venv);

PythonEnvironment? res = new();

if (pynetVenvSet)
res = FromVenv(pynetVenv) ?? res;
else if (venvSet)
res = FromVenv(venv) ?? res;

if (pyexeSet)
res.ProgramName = pyexe;

if (pydllSet)
res.LibPython = pydll;

return res;
}

public static PythonEnvironment? FromVenv(string path)
{
var env = new PythonEnvironment
{
VenvPath = path
};

string venvCfg = Path.Combine(path, "pyvenv.cfg");

if (!File.Exists(venvCfg))
return null;

var settings = TryParse(venvCfg);

if (!settings.ContainsKey("home"))
return null;

env.Home = settings["home"];
var pname = ProgramNameFromPath(path);
if (File.Exists(pname))
env.ProgramName = pname;

if (settings.TryGetValue("version", out string versionStr))
{
_ = Version.TryParse(versionStr, out Version versionObj);
env.Version = versionObj;
}
else if (settings.TryGetValue("version_info", out versionStr))
{
_ = Version.TryParse(versionStr, out Version versionObj);
env.Version = versionObj;
}

env.LibPython = FindLibPython(env.Home, env.Version);

return env;
}

private static Dictionary<string, string> TryParse(string venvCfg)
{
var settings = new Dictionary<string, string>();

string[] lines = File.ReadAllLines(venvCfg);

// The actually used format is really primitive: "<key> = <value>"
foreach (string line in lines)
{
var split = line.Split(new[] { '=' }, 2);

if (split.Length != 2)
continue;

settings[split[0].Trim()] = split[1].Trim();
}

return settings;
}

private static string? FindLibPython(string home, Version? maybeVersion)
{
// TODO: Check whether there is a .dll/.so/.dylib next to the executable

if (maybeVersion is Version version)
{
return FindLibPythonInHome(home, version);
}

return null;
}

private static string? FindLibPythonInHome(string home, Version version)
{
var libPythonName = GetDefaultDllName(version);

List<string> pathsToCheck = new();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var arch = RuntimeInformation.ProcessArchitecture;
if (arch == Architecture.X64 || arch == Architecture.Arm64)
{
// multilib systems
pathsToCheck.Add("../lib64");
}
pathsToCheck.Add("../lib");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
pathsToCheck.Add(".");
}
else
{
pathsToCheck.Add("../lib");
}

return pathsToCheck
.Select(path => Path.Combine(home, path, libPythonName))
.FirstOrDefault(File.Exists);
}

private static string ProgramNameFromPath(string path)
{
if (Runtime.IsWindows)
{
return Path.Combine(path, "Scripts", "python.exe");
}
else
{
return Path.Combine(path, "bin", "python");
}
}

internal static string GetDefaultDllName(Version version)
{
string prefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";

string suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Invariant($"{version.Major}{version.Minor}")
: Invariant($"{version.Major}.{version.Minor}");

string ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib"
: ".so";

return prefix + "python" + suffix + ext;
}
}
7 changes: 4 additions & 3 deletions tools/geninterop/geninterop.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#!/usr/bin/env uv run
# /// script
# dependencies = ["pycparser"]
# ///
"""
TypeOffset is a C# class that mirrors the in-memory layout of heap
allocated Python objects.
Expand Down
Loading
Loading