Repository navigation
virtual environment: either crashes or cannot find modules #1478
Description
Environment
- Pythonnet version: 3.0.0-preview2021-06-04
- Python version: 3.9.1 (tags/v3.9.1:1e5d33e, Dec 7 2020, 17:08:21) [MSC v.1927 64 bit (AMD64)]
- Operating System: Windows 10 20H2
- .NET Runtime: 5.0.301
Details
according to #984 .NET Core is supported, which I hope includes .NET 5.0
according to #1389 Python 3.9 is supported in pythonnet 2.5, though I was unable to find 2.5 on nuget, so I went with a 3.0 preview in the hopes that it will work too.
Describe what you were trying to get done.
Embed python in a C# application with a virtual environment (venv) and import a module from it.
I have set up a virtual environment at C:\Users\felk\venv39 and verified that it works and I can use modules that are available in the venv but not globally:
PS C:\Users\felk> python -c 'import chat_downloader; print(True)'
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'chat_downloader'
PS C:\Users\felk> .\venv39\Scripts\Activate.ps1
(venv39) PS C:\Users\felk> python -c 'import chat_downloader; print(True)'
True
According to the wiki this is the code to get virtual environments working:
var pathToVirtualEnv = @"path\to\env";
// be sure not to overwrite your existing "PATH" environmental variable.
var path = Environment.GetEnvironmentVariable("PATH").TrimEnd(';');
path = string.IsNullOrEmpty(path) ? pathToVirtualEnv : path + ";" + pathToVirtualEnv;
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("PATH", pathToVirtualEnv, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("PYTHONHOME", pathToVirtualEnv, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("PYTHONPATH", $"{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib", EnvironmentVariableTarget.Process);
PythonEngine.PythonHome = pathToVirtualEnv;
PythonEngine.PythonPath = Environment.GetEnvironmentVariable("PYTHONPATH", EnvironmentVariableTarget.Process);However it is not clear to me what comes after that, most importantly where do I call PythonEngine.Initialize();, so I attempted to piecemeal something together:
- Calling
PythonEngine.Initialize();after the above snipped causes the application to crash without an exception, which I suspected is a segfault due to threading issues - Not calling it at all crashes the program in the same way upon first usage of any python within C#
- Calling it just before setting
PythonEngine.PythonHomeandPythonEngine.PythonPathseems to work, in a sense that the application does not crash and calls into python from C# are possible.
However, when I try to import a module present in the virtual environment, it is not being found. I can observe that the venv path is part of PythonEngine.PythonPath but not part of sys.path by checking after setting PythonEngine.PythonPath:
dynamic sys = Py.Import("sys");
Console.WriteLine(sys.path);
Console.WriteLine(PythonEngine.PythonPath);this results in
['C:\\Program Files\\Python39\\python39.zip', 'C:\\Program Files\\Python39\\Lib', 'C:\\Program Files\\Python39\\DLLs', 'S:\\projects\\myproject\\bin\\Debug\\net5.0', 'C:\\Users\\felk\\AppData\\Roamin
g\\Python\\Python39\\site-packages', 'C:\\Program Files\\Python39', 'C:\\Program Files\\Python39\\lib\\site-packages', 'C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\5.0.7\\']
C:/Users/felk/venv39/Lib/site-packages;C:/Users/felk/venv39/Lib;
This seems to be unrelated, but I also removed this line from the wiki's example as it seems nonsensical to override the PATH with the venv path if it was just set to the correctly appended one a line above, and since the comment above literally just said be sure not to overwrite your existing "PATH" environmental variable:
Environment.SetEnvironmentVariable("PATH", pathToVirtualEnv, EnvironmentVariableTarget.Process);I also tried appending to PythonEngine.PythonPath instead of replacing it, as described in #1348 but that had no effect either.
Activity
Felk commentedon Jun 26, 2021
Some more random tinkering led me to a solution. The following order of operations works, deviating from the wiki:
PythonEngine.Initialize();Doing things in this order does not crash and also causes the new python path to be visible in
sys.path. My complete example looks something like this:console output:
This seems to work for me, but I'll leave this issue open for now in case anyone wants to adjust the wiki.
bpdavis86 commentedon Aug 12, 2021
I am appending my own troubles with virtual environments to this open issue rather than create a new one.
Environment
Issue
I spent better part of 2 days trying to get virtual environments working with pythonnet. Part of the problem is that there are a number of various methods to create virtual environments and they may end up creating slightly different "flavored" environments. For example, see:
https://stackoverflow.com/questions/41573587/what-is-the-difference-between-venv-pyvenv-pyenv-virtualenv-virtualenvwrappe
This does not even cover conda environments which are totally separate.
In order to better understand how virtual environments can be made to work with pythonnet, you have to dig into the details of how Python sets up sys.path and how this behaves with virtual environments, in particular, PEP 405 "lightweight" environments such as those created with the venv module.
Key Reference Material:
If one reads through these documents, one finds that all things begin with sys.executable. Python walks up the path looking for its system libraries based on this location except in the case where it finds the file "pyvenv.cfg" one level above the executable. This is the extension provided by PEP 405 to enable lightweight virtual environments. In this case, the pyvenv.cfg "home" key tells Python where the base install is.
This already presents a problem, because let's see how sys.executable gets set from a C# program using pythonnet to embed Python. Here is a test program to expose some details of the resulting Python environment.
There are two major points to notice here:
The first issue may be related to this post regarding SetEnvironmentVariable and setenv/getenv. I have not confirmed.
https://stackoverflow.com/questions/4788398/changes-via-setenvironmentvariable-do-not-take-effect-in-library-that-uses-geten
Regardless, it seems that whatever environment settings one makes in code do not end up in the Python environment variables. Therefore, the environment manipulation in the example code is a red herring, except for the PATH variable, which would allow one to set the Python DLL name without a full absolute path if the PATH includes the Python base folder (with pythonXX.dll).
One should instead do such environment manipulations through the Run/Test environment, i.e. Visual Studio project setup for the debugging environment.
Regarding the second issue, one may well ask how this example succeeded if Python does not see the PYTHONHOME variable and the system libraries are not to be found along the path to sys.executable. Examining the Stack Overflow we find:
For sys.path
Finally
Examining my registry shows that HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\3.8\PythonPath contains
So it seems Windows was able to resolve the path issue using the registry and then applies this path to sys.prefix despite it having no resemblance to sys.executable.
What about the rest of the sys.path entries?
C:\Users\myuser\OneDrive\Documents\Python Scriptscomes from PYTHONPATH (already in my user environment)C:\Users\myuser\source\repos\TestPythonnet\TestPythonnet\bin\Debug\netcoreapp3.1comes from rule 10 in the Stack Overflow regarding "the directory which was used (or would have been used) to search dynamically for sys.prefix is added"C:\Users\myuser\AppData\Local\Programs\Python\Python38andC:\Users\myuser\AppData\Local\Programs\Python\Python38\lib\site-packagesare the result of the "site" package run (see https://docs.python.org/3/library/site.html).Finally,
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.18\is added by pythonnet itself in Python.Runtime.Runtime.InitializeSo, now that we've figured out what happened, how do we fix this to work with virtual environments properly, specifically PEP 405 "lightweight" environments? Well, unfortunately, it seems we can't replicate the setup exactly without lots of manual intervention. A PEP 405 environment looks something like this (on my system):
What is not included is pythonXX.dll, which is exactly the file needed by pythonnet to function.
In "normal" operations, I would put
C:\Users\myuser\py38\Scriptsfirst in my path (using the activation script), which would launch Python using the python.exe contained there. Therefore, sys.executable would point toC:\Users\myuser\py38\Scripts\python.exe, Python would "walk back" through the filesystem and find pyvenv.cfg, which containshome = C:\Users\myuser\AppData\Local\Programs\Python\Python38. Therefore, we get this environmentWhy don't we just set PYTHONHOME to the venv folder and be done with it? Unfortunately, this doesn't work because PYTHONHOME needs to point to the root of the Python install with the system libraries, which this lightweight venv does not have. (It seems PYTHONHOME is explicitly incompatible with venv because it is unset/cached in the activate script).
Proposed Solution
So, in order to replicate the venv setup, we need to follow a rather messy manual process.
In the following steps, all environment variables should be set outside of code (i.e. in your shell or Visual Studio project setup) in order to avoid the problem mentioned before where values do not propagate down into Python after being changed in C#.
Get pythonnet to be able to find your python3X.dll. This can be done in a number of ways.
Set your Python home explicitly. This will avoid any shenanigans regarding autodetection of the base environment when sys.executable is not python.exe. Again, this can be done in a couple ways.
Specify the virtual environment path somehow. You could invent an environment variable for this, or supply directly in code.
Here is a full code example. I used as much setup in the external environment as possible (because hardcoding paths into software is not a great way to go.)
Environment variables (from launchSettings.json):
{ "profiles": { "TestPythonnet": { "commandName": "Project", "environmentVariables": { "PYTHONNET_PYVENV": "C:\\Users\\myuser\\py38", "PYTHONNET_PYVER": "3.8", "PYTHONHOME": "C:\\Users\\myuser\\AppData\\Local\\Programs\\Python\\Python38", "PATH": "C:\\Users\\myuser\\AppData\\Local\\Programs\\Python\\Python38; %PATH%" } } } }Program:
Output:
This is pretty close to the environment generated by running python normally through the venv, modulo the extra .NET folders.
Proposed Enhancement Request
Obviously, this is rather ugly and unfortunate. The root cause of the problem is that we cannot control the value of sys.executable so that Python can follow its standard process to load the venv. Fortunately, there is a new config API added to Python 3.8 with PEP 587. This API allows fine-grained control over the startup Python configuration, including sys.executable. Implementing access to this API through pythonnet would allow us to simply set sys.executable to the venv version of python.exe and have everything work from there.
filmor commentedon Aug 12, 2021
Thank you for the detailed report. PRs for this are very welcome!
A few comments:
PYTHONHOMEis (I think) this Windows-specific issue: https://bugs.python.org/issue16633. You can instead usePython.Runtime.PythonHome = somethingto set the value correctly after setting the correct dll location and before runningInitialize. This will use the Python API function Py_SetPythonHome which will work independent of the environment variable.libpython. You can use (and maybe extend?) something like https://github.com/losttech/WhichPython for this task. This is out of scope forpythonnet.lostmsu commentedon Aug 14, 2021
@Meisterrichter offtopic idea: if you want your .NET app run within a virtualenv, can you maybe launch it from Python executable? E.g. do
?
igblip commentedon Nov 9, 2024
I did a hack just to get past this problem while doing development in VSCode in a Debian devcontainer on MacOS. My production environment (AWS EC2 with Amazon Linux) will not need this hack, and I don't recommend using it anywhere except getting past the issue during development.
First, the above post from @bpdavis86 was incredibly helpful and informative.
In my case, I could not get a venv environment working through pythonnet. I needed a way to add site-packages system-wide, but packages were maintained by apt and not pip.
Running
python3 -m siteresulted in:I have no idea why the zip file is in the path and have not investigated. It does not exist.
The USER_SITE value led to the hack. Specifically, I first created a venv, activated it, and installed my required packages. I then created the USER_SITE folder and copied the packages to it. Running my C# app that runs a script via pythonnet worked.
Again - this is just a hack.