Visitar URL original
Use uv and derive as much as possible from the environment, if available by filmor · Pull Request #2652 · pythonnet/pythonnet · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Implement venv derivation
  • Loading branch information
filmor committed Oct 25, 2025
commit 3a3abb967ddde637d7f1b5fd62c427c6090d722b
10 changes: 0 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,6 @@ jobs:
activate-environment: true
enable-cache: true

- name: Set Python DLL path and PYTHONHOME (non Windows)
if: ${{ matrix.os.category != 'windows' }}
run: |
echo PYTHONNET_PYDLL=$(uv run find_libpython) >> $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=$(uv run find_libpython)"

- name: Embedding tests
run: dotnet test --runtime any-${{ matrix.os.platform }} --logger "console;verbosity=detailed" src/embed_tests/
env:
Expand Down
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
57 changes: 6 additions & 51 deletions src/runtime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +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 (!string.IsNullOrEmpty(dll))
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 @@ -103,35 +82,11 @@ static void EnsureProgramName()
if (!string.IsNullOrEmpty(PythonEngine.ProgramName))
return;

string fromEnv = Environment.GetEnvironmentVariable("PYTHONNET_PYEXE");
if (!string.IsNullOrEmpty(fromEnv))
if (PythonEnvironment.IsValid)
{
PythonEngine.ProgramName = fromEnv;
PythonEngine.ProgramName = PythonEnvironment.ProgramName!;
return;
}

string venv = Environment.GetEnvironmentVariable("VIRTUAL_ENV");
if (!string.IsNullOrEmpty(venv))
{
if (IsWindows)
{
var path = Path.Combine(venv, "Scripts", "python.exe");
if (System.IO.File.Exists(path))
{
PythonEngine.ProgramName = path;
return;
}
}
else
{
var path = Path.Combine(venv, "bin", "python");
if (System.IO.File.Exists(path))
{
PythonEngine.ProgramName = path;
return;
}
}
}
}

internal static bool HostedInPython;
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;
}
}
Loading