Allow configuring .NET runtimes via environment variables
This commit is contained in:
Родитель
f5de0bf9eb
Коммит
7da78892d7
|
@ -45,7 +45,7 @@ jobs:
|
|||
run: python -m pytest --runtime mono
|
||||
|
||||
- name: Python Tests (.NET Core)
|
||||
run: python -m pytest --runtime netcore
|
||||
run: python -m pytest --runtime coreclr
|
||||
|
||||
- name: Python tests run from .NET
|
||||
run: dotnet test src/python_tests_runner/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: Main (x64)
|
||||
name: Main
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -73,9 +73,10 @@ jobs:
|
|||
if: ${{ matrix.os != 'windows' }}
|
||||
run: pytest --runtime mono
|
||||
|
||||
# TODO: Run these tests on Windows x86
|
||||
- name: Python Tests (.NET Core)
|
||||
if: ${{ matrix.platform == 'x64' }}
|
||||
run: pytest --runtime netcore
|
||||
run: pytest --runtime coreclr
|
||||
|
||||
- name: Python Tests (.NET Framework)
|
||||
if: ${{ matrix.os == 'windows' }}
|
||||
|
|
|
@ -29,6 +29,7 @@ and other `PyObject` derived types when called from Python.
|
|||
- .NET classes, that have `__call__` method are callable from Python
|
||||
- `PyIterable` type, that wraps any iterable object in Python
|
||||
- `PythonEngine` properties for supported Python versions: `MinSupportedVersion`, `MaxSupportedVersion`, and `IsSupportedVersion`
|
||||
- The runtime that is loaded on `import clr` can now be configured via environment variables
|
||||
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -1,43 +1,115 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Union
|
||||
import clr_loader
|
||||
|
||||
_RUNTIME = None
|
||||
_LOADER_ASSEMBLY = None
|
||||
_FFI = None
|
||||
_LOADED = False
|
||||
__all__ = ["set_runtime", "set_default_runtime", "load"]
|
||||
|
||||
_RUNTIME: Optional[clr_loader.Runtime] = None
|
||||
_LOADER_ASSEMBLY: Optional[clr_loader.wrappers.Assembly] = None
|
||||
_LOADED: bool = False
|
||||
|
||||
|
||||
def set_runtime(runtime):
|
||||
def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None:
|
||||
"""Set up a clr_loader runtime without loading it
|
||||
|
||||
:param runtime: Either an already initialised `clr_loader` runtime, or one
|
||||
of netfx, coreclr, mono, or default. If a string parameter is given, the
|
||||
runtime will be created."""
|
||||
|
||||
global _RUNTIME
|
||||
if _LOADED:
|
||||
raise RuntimeError("The runtime {} has already been loaded".format(_RUNTIME))
|
||||
raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded")
|
||||
|
||||
if isinstance(runtime, str):
|
||||
runtime = _create_runtime_from_spec(runtime, params)
|
||||
|
||||
_RUNTIME = runtime
|
||||
|
||||
|
||||
def set_default_runtime() -> None:
|
||||
if sys.platform == "win32":
|
||||
set_runtime(clr_loader.get_netfx())
|
||||
def _get_params_from_env(prefix: str) -> Dict[str, str]:
|
||||
from os import environ
|
||||
|
||||
full_prefix = f"PYTHONNET_{prefix.upper()}"
|
||||
len_ = len(full_prefix)
|
||||
|
||||
env_vars = {
|
||||
(k[len_:].lower()): v
|
||||
for k, v in environ.items()
|
||||
if k.upper().startswith(full_prefix)
|
||||
}
|
||||
|
||||
return env_vars
|
||||
|
||||
|
||||
def _create_runtime_from_spec(
|
||||
spec: str, params: Optional[Dict[str, str]] = None
|
||||
) -> clr_loader.Runtime:
|
||||
if spec == "default":
|
||||
if sys.platform == "win32":
|
||||
spec = "netfx"
|
||||
else:
|
||||
spec = "mono"
|
||||
|
||||
params = params or _get_params_from_env(spec)
|
||||
|
||||
if spec == "netfx":
|
||||
return clr_loader.get_netfx(**params)
|
||||
elif spec == "mono":
|
||||
return clr_loader.get_mono(**params)
|
||||
elif spec == "coreclr":
|
||||
return clr_loader.get_coreclr(**params)
|
||||
else:
|
||||
set_runtime(clr_loader.get_mono())
|
||||
raise RuntimeError(f"Invalid runtime name: '{spec}'")
|
||||
|
||||
|
||||
def load():
|
||||
global _FFI, _LOADED, _LOADER_ASSEMBLY
|
||||
def set_default_runtime() -> None:
|
||||
"""Set up the default runtime
|
||||
|
||||
This will use the environment variable PYTHONNET_RUNTIME to decide the
|
||||
runtime to use, which may be one of netfx, coreclr or mono. The parameters
|
||||
of the respective clr_loader.get_<runtime> functions can also be given as
|
||||
environment variables, named `PYTHONNET_<RUNTIME>_<PARAM_NAME>`. In
|
||||
particular, to use `PYTHONNET_RUNTIME=coreclr`, the variable
|
||||
`PYTHONNET_CORECLR_RUNTIME_CONFIG` has to be set to a valid
|
||||
`.runtimeconfig.json`.
|
||||
|
||||
If no environment variable is specified, a globally installed Mono is used
|
||||
for all environments but Windows, on Windows the legacy .NET Framework is
|
||||
used.
|
||||
"""
|
||||
from os import environ
|
||||
|
||||
print("Set default RUNTIME")
|
||||
raise RuntimeError("Shouldn't be called here")
|
||||
|
||||
spec = environ.get("PYTHONNET_RUNTIME", "default")
|
||||
runtime = _create_runtime_from_spec(spec)
|
||||
set_runtime(runtime)
|
||||
|
||||
|
||||
def load(
|
||||
runtime: Union[clr_loader.Runtime, str] = "default", **params: Dict[str, str]
|
||||
) -> None:
|
||||
"""Load Python.NET in the specified runtime
|
||||
|
||||
The same parameters as for `set_runtime` can be used. By default,
|
||||
`set_default_runtime` is called if no environment has been set yet and no
|
||||
parameters are passed."""
|
||||
global _LOADED, _LOADER_ASSEMBLY
|
||||
|
||||
if _LOADED:
|
||||
return
|
||||
|
||||
from os.path import join, dirname
|
||||
if _RUNTIME is None:
|
||||
set_runtime(runtime, **params)
|
||||
|
||||
if _RUNTIME is None:
|
||||
# TODO: Warn, in the future the runtime must be set explicitly, either
|
||||
# as a config/env variable or via set_runtime
|
||||
set_default_runtime()
|
||||
raise RuntimeError("No valid runtime selected")
|
||||
|
||||
dll_path = join(dirname(__file__), "runtime", "Python.Runtime.dll")
|
||||
dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll"
|
||||
|
||||
_LOADER_ASSEMBLY = _RUNTIME.get_assembly(dll_path)
|
||||
_LOADER_ASSEMBLY = _RUNTIME.get_assembly(str(dll_path))
|
||||
|
||||
func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"]
|
||||
if func(b"") != 0:
|
||||
|
@ -48,13 +120,17 @@ def load():
|
|||
atexit.register(unload)
|
||||
|
||||
|
||||
def unload():
|
||||
global _RUNTIME
|
||||
def unload() -> None:
|
||||
"""Explicitly unload a laoded runtime and shut down Python.NET"""
|
||||
|
||||
global _RUNTIME, _LOADER_ASSEMBLY
|
||||
if _LOADER_ASSEMBLY is not None:
|
||||
func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"]
|
||||
if func(b"full_shutdown") != 0:
|
||||
raise RuntimeError("Failed to call Python.NET shutdown")
|
||||
|
||||
_LOADER_ASSEMBLY = None
|
||||
|
||||
if _RUNTIME is not None:
|
||||
# TODO: Add explicit `close` to clr_loader
|
||||
_RUNTIME = None
|
||||
|
|
|
@ -8,91 +8,83 @@ import ctypes
|
|||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
from subprocess import check_call
|
||||
from tempfile import mkdtemp
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from pythonnet import set_runtime
|
||||
|
||||
# Add path for `Python.Test`
|
||||
cwd = os.path.dirname(__file__)
|
||||
fixtures_path = os.path.join(cwd, "fixtures")
|
||||
sys.path.append(fixtures_path)
|
||||
cwd = Path(__file__).parent
|
||||
fixtures_path = cwd / "fixtures"
|
||||
sys.path.append(str(fixtures_path))
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--runtime",
|
||||
action="store",
|
||||
default="default",
|
||||
help="Must be one of default, netcore, netfx and mono"
|
||||
help="Must be one of default, coreclr, netfx and mono",
|
||||
)
|
||||
|
||||
|
||||
collect_ignore = []
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
global bin_path
|
||||
if "clr" in sys.modules:
|
||||
# Already loaded (e.g. by the C# test runner), skip build
|
||||
import clr
|
||||
|
||||
clr.AddReference("Python.Test")
|
||||
return
|
||||
|
||||
runtime_opt = config.getoption("runtime")
|
||||
|
||||
test_proj_path = os.path.join(cwd, "..", "src", "testing")
|
||||
|
||||
if runtime_opt not in ["netcore", "netfx", "mono", "default"]:
|
||||
if runtime_opt not in ["coreclr", "netfx", "mono", "default"]:
|
||||
raise RuntimeError(f"Invalid runtime: {runtime_opt}")
|
||||
|
||||
bin_path = mkdtemp()
|
||||
test_proj_path = cwd.parent / "src" / "testing"
|
||||
bin_path = Path(mkdtemp())
|
||||
|
||||
# tmpdir_factory.mktemp(f"pythonnet-{runtime_opt}")
|
||||
fw = "netstandard2.0"
|
||||
runtime_params = {}
|
||||
|
||||
fw = "net6.0" if runtime_opt == "netcore" else "netstandard2.0"
|
||||
|
||||
check_call(["dotnet", "publish", "-f", fw, "-o", bin_path, test_proj_path])
|
||||
|
||||
sys.path.append(bin_path)
|
||||
|
||||
if runtime_opt == "default":
|
||||
pass
|
||||
elif runtime_opt == "netfx":
|
||||
from clr_loader import get_netfx
|
||||
runtime = get_netfx()
|
||||
set_runtime(runtime)
|
||||
elif runtime_opt == "mono":
|
||||
from clr_loader import get_mono
|
||||
runtime = get_mono()
|
||||
set_runtime(runtime)
|
||||
elif runtime_opt == "netcore":
|
||||
from clr_loader import get_coreclr
|
||||
rt_config_path = os.path.join(bin_path, "Python.Test.runtimeconfig.json")
|
||||
runtime = get_coreclr(rt_config_path)
|
||||
set_runtime(runtime)
|
||||
|
||||
import clr
|
||||
clr.AddReference("Python.Test")
|
||||
|
||||
soft_mode = False
|
||||
try:
|
||||
os.environ['PYTHONNET_SHUTDOWN_MODE'] == 'Soft'
|
||||
except: pass
|
||||
|
||||
if config.getoption("--runtime") == "netcore" or soft_mode\
|
||||
:
|
||||
if runtime_opt == "coreclr":
|
||||
fw = "net6.0"
|
||||
runtime_params["runtime_config"] = str(
|
||||
bin_path / "Python.Test.runtimeconfig.json"
|
||||
)
|
||||
collect_ignore.append("domain_tests/test_domain_reload.py")
|
||||
else:
|
||||
domain_tests_dir = os.path.join(os.path.dirname(__file__), "domain_tests")
|
||||
bin_path = os.path.join(domain_tests_dir, "bin")
|
||||
build_cmd = ["dotnet", "build", domain_tests_dir, "-o", bin_path]
|
||||
domain_tests_dir = cwd / "domain_tests"
|
||||
domain_bin_path = domain_tests_dir / "bin"
|
||||
build_cmd = [
|
||||
"dotnet",
|
||||
"build",
|
||||
str(domain_tests_dir),
|
||||
"-o",
|
||||
str(domain_bin_path),
|
||||
]
|
||||
is_64bits = sys.maxsize > 2**32
|
||||
if not is_64bits:
|
||||
build_cmd += ["/p:Prefer32Bit=True"]
|
||||
check_call(build_cmd)
|
||||
|
||||
check_call(
|
||||
["dotnet", "publish", "-f", fw, "-o", str(bin_path), str(test_proj_path)]
|
||||
)
|
||||
|
||||
from pythonnet import load
|
||||
|
||||
load(runtime_opt, **runtime_params)
|
||||
|
||||
import clr
|
||||
|
||||
sys.path.append(str(bin_path))
|
||||
clr.AddReference("Python.Test")
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
|
@ -102,6 +94,7 @@ def pytest_unconfigure(config):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
"""Generate extra report headers"""
|
||||
# FIXME: https://github.com/pytest-dev/pytest/issues/2257
|
||||
|
@ -109,11 +102,8 @@ def pytest_report_header(config):
|
|||
arch = "x64" if is_64bits else "x86"
|
||||
ucs = ctypes.sizeof(ctypes.c_wchar)
|
||||
libdir = sysconfig.get_config_var("LIBDIR")
|
||||
shared = bool(sysconfig.get_config_var("Py_ENABLE_SHARED"))
|
||||
|
||||
header = ("Arch: {arch}, UCS: {ucs}, LIBDIR: {libdir}, "
|
||||
"Py_ENABLE_SHARED: {shared}".format(**locals()))
|
||||
return header
|
||||
return f"Arch: {arch}, UCS: {ucs}, LIBDIR: {libdir}"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
Загрузка…
Ссылка в новой задаче