Skip to content

Commit 125556f

Browse files
committed
Current state
1 parent f5de0bf commit 125556f

File tree

4 files changed

+140
-74
lines changed

4 files changed

+140
-74
lines changed

.github/workflows/ARM.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
run: python -m pytest --runtime mono
4646

4747
- name: Python Tests (.NET Core)
48-
run: python -m pytest --runtime netcore
48+
run: python -m pytest --runtime coreclr
4949

5050
- name: Python tests run from .NET
5151
run: dotnet test src/python_tests_runner/

.github/workflows/main.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ jobs:
7474
run: pytest --runtime mono
7575

7676
- name: Python Tests (.NET Core)
77-
if: ${{ matrix.platform == 'x64' }}
78-
run: pytest --runtime netcore
77+
if: ${{ matrix.platform == 'x64' || matrix.os == 'windows' }}
78+
run: pytest --runtime coreclr
7979

8080
- name: Python Tests (.NET Framework)
8181
if: ${{ matrix.os == 'windows' }}

pythonnet/__init__.py

+96-20
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,115 @@
11
import sys
2+
from pathlib import Path
3+
from typing import Dict, Optional, Union
24
import clr_loader
35

4-
_RUNTIME = None
5-
_LOADER_ASSEMBLY = None
6-
_FFI = None
7-
_LOADED = False
6+
__all__ = ["set_runtime", "set_default_runtime", "load"]
87

8+
_RUNTIME: Optional[clr_loader.Runtime] = None
9+
_LOADER_ASSEMBLY: Optional[clr_loader.wrappers.Assembly] = None
10+
_LOADED: bool = False
11+
12+
13+
def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None:
14+
"""Set up a clr_loader runtime without loading it
15+
16+
:param runtime: Either an already initialised `clr_loader` runtime, or one
17+
of netfx, coreclr, mono, or default. If a string parameter is given, the
18+
runtime will be created."""
919

10-
def set_runtime(runtime):
1120
global _RUNTIME
1221
if _LOADED:
13-
raise RuntimeError("The runtime {} has already been loaded".format(_RUNTIME))
22+
raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded")
23+
24+
if isinstance(runtime, str):
25+
runtime = _create_runtime_from_spec(runtime, params)
1426

1527
_RUNTIME = runtime
1628

1729

18-
def set_default_runtime() -> None:
19-
if sys.platform == "win32":
20-
set_runtime(clr_loader.get_netfx())
30+
def _get_params_from_env(prefix: str) -> Dict[str, str]:
31+
from os import environ
32+
33+
full_prefix = f"PYTHONNET_{prefix.upper()}"
34+
len_ = len(full_prefix)
35+
36+
env_vars = {
37+
(k[len_:].lower()): v
38+
for k, v in environ.items()
39+
if k.upper().startswith(full_prefix)
40+
}
41+
42+
return env_vars
43+
44+
45+
def _create_runtime_from_spec(
46+
spec: str, params: Optional[Dict[str, str]] = None
47+
) -> clr_loader.Runtime:
48+
if spec == "default":
49+
if sys.platform == "win32":
50+
spec = "netfx"
51+
else:
52+
spec = "mono"
53+
54+
params = params or _get_params_from_env(spec)
55+
56+
if spec == "netfx":
57+
return clr_loader.get_netfx(**params)
58+
elif spec == "mono":
59+
return clr_loader.get_mono(**params)
60+
elif spec == "coreclr":
61+
return clr_loader.get_coreclr(**params)
2162
else:
22-
set_runtime(clr_loader.get_mono())
63+
raise RuntimeError(f"Invalid runtime name: '{spec}'")
2364

2465

25-
def load():
26-
global _FFI, _LOADED, _LOADER_ASSEMBLY
66+
def set_default_runtime() -> None:
67+
"""Set up the default runtime
68+
69+
This will use the environment variable PYTHONNET_RUNTIME to decide the
70+
runtime to use, which may be one of netfx, coreclr or mono. The parameters
71+
of the respective clr_loader.get_<runtime> functions can also be given as
72+
environment variables, named `PYTHONNET_<RUNTIME>_<PARAM_NAME>`. In
73+
particular, to use `PYTHONNET_RUNTIME=coreclr`, the variable
74+
`PYTHONNET_CORECLR_RUNTIME_CONFIG` has to be set to a valid
75+
`.runtimeconfig.json`.
76+
77+
If no environment variable is specified, a globally installed Mono is used
78+
for all environments but Windows, on Windows the legacy .NET Framework is
79+
used.
80+
"""
81+
from os import environ
82+
83+
print("Set default RUNTIME")
84+
raise RuntimeError("Shouldn't be called here")
85+
86+
spec = environ.get("PYTHONNET_RUNTIME", "default")
87+
runtime = _create_runtime_from_spec(spec)
88+
set_runtime(runtime)
89+
90+
91+
def load(
92+
runtime: Union[clr_loader.Runtime, str] = "default", **params: Dict[str, str]
93+
) -> None:
94+
"""Load Python.NET in the specified runtime
95+
96+
The same parameters as for `set_runtime` can be used. By default,
97+
`set_default_runtime` is called if no environment has been set yet and no
98+
parameters are passed."""
99+
global _LOADED, _LOADER_ASSEMBLY
27100

28101
if _LOADED:
29102
return
30103

31-
from os.path import join, dirname
104+
if _RUNTIME is None:
105+
set_runtime(runtime, **params)
32106

33107
if _RUNTIME is None:
34-
# TODO: Warn, in the future the runtime must be set explicitly, either
35-
# as a config/env variable or via set_runtime
36-
set_default_runtime()
108+
raise RuntimeError("No valid runtime selected")
37109

38-
dll_path = join(dirname(__file__), "runtime", "Python.Runtime.dll")
110+
dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll"
39111

40-
_LOADER_ASSEMBLY = _RUNTIME.get_assembly(dll_path)
112+
_LOADER_ASSEMBLY = _RUNTIME.get_assembly(str(dll_path))
41113

42114
func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"]
43115
if func(b"") != 0:
@@ -48,13 +120,17 @@ def load():
48120
atexit.register(unload)
49121

50122

51-
def unload():
52-
global _RUNTIME
123+
def unload() -> None:
124+
"""Explicitly unload a laoded runtime and shut down Python.NET"""
125+
126+
global _RUNTIME, _LOADER_ASSEMBLY
53127
if _LOADER_ASSEMBLY is not None:
54128
func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"]
55129
if func(b"full_shutdown") != 0:
56130
raise RuntimeError("Failed to call Python.NET shutdown")
57131

132+
_LOADER_ASSEMBLY = None
133+
58134
if _RUNTIME is not None:
59135
# TODO: Add explicit `close` to clr_loader
60136
_RUNTIME = None

tests/conftest.py

+41-51
Original file line numberDiff line numberDiff line change
@@ -8,91 +8,83 @@
88
import os
99
import sys
1010
import sysconfig
11+
from pathlib import Path
1112
from subprocess import check_call
1213
from tempfile import mkdtemp
1314
import shutil
1415

1516
import pytest
1617

17-
from pythonnet import set_runtime
18-
1918
# Add path for `Python.Test`
20-
cwd = os.path.dirname(__file__)
21-
fixtures_path = os.path.join(cwd, "fixtures")
22-
sys.path.append(fixtures_path)
19+
cwd = Path(__file__).parent
20+
fixtures_path = cwd / "fixtures"
21+
sys.path.append(str(fixtures_path))
22+
2323

2424
def pytest_addoption(parser):
2525
parser.addoption(
2626
"--runtime",
2727
action="store",
2828
default="default",
29-
help="Must be one of default, netcore, netfx and mono"
29+
help="Must be one of default, coreclr, netfx and mono",
3030
)
3131

32+
3233
collect_ignore = []
3334

35+
3436
def pytest_configure(config):
3537
global bin_path
3638
if "clr" in sys.modules:
3739
# Already loaded (e.g. by the C# test runner), skip build
3840
import clr
41+
3942
clr.AddReference("Python.Test")
4043
return
4144

4245
runtime_opt = config.getoption("runtime")
43-
44-
test_proj_path = os.path.join(cwd, "..", "src", "testing")
45-
46-
if runtime_opt not in ["netcore", "netfx", "mono", "default"]:
46+
if runtime_opt not in ["coreclr", "netfx", "mono", "default"]:
4747
raise RuntimeError(f"Invalid runtime: {runtime_opt}")
4848

49-
bin_path = mkdtemp()
50-
51-
# tmpdir_factory.mktemp(f"pythonnet-{runtime_opt}")
52-
53-
fw = "net6.0" if runtime_opt == "netcore" else "netstandard2.0"
54-
55-
check_call(["dotnet", "publish", "-f", fw, "-o", bin_path, test_proj_path])
56-
57-
sys.path.append(bin_path)
58-
59-
if runtime_opt == "default":
60-
pass
61-
elif runtime_opt == "netfx":
62-
from clr_loader import get_netfx
63-
runtime = get_netfx()
64-
set_runtime(runtime)
65-
elif runtime_opt == "mono":
66-
from clr_loader import get_mono
67-
runtime = get_mono()
68-
set_runtime(runtime)
69-
elif runtime_opt == "netcore":
70-
from clr_loader import get_coreclr
71-
rt_config_path = os.path.join(bin_path, "Python.Test.runtimeconfig.json")
72-
runtime = get_coreclr(rt_config_path)
73-
set_runtime(runtime)
74-
75-
import clr
76-
clr.AddReference("Python.Test")
49+
test_proj_path = cwd.parent / "src/testing"
50+
bin_path = Path(mkdtemp())
7751

78-
soft_mode = False
79-
try:
80-
os.environ['PYTHONNET_SHUTDOWN_MODE'] == 'Soft'
81-
except: pass
52+
fw = "netstandard2.0"
53+
runtime_params = {}
8254

83-
if config.getoption("--runtime") == "netcore" or soft_mode\
84-
:
55+
if runtime_opt == "coreclr":
56+
fw = "net6.0"
57+
runtime_params["runtime_config"] = str(
58+
bin_path / "Python.Test.runtimeconfig.json"
59+
)
8560
collect_ignore.append("domain_tests/test_domain_reload.py")
8661
else:
87-
domain_tests_dir = os.path.join(os.path.dirname(__file__), "domain_tests")
88-
bin_path = os.path.join(domain_tests_dir, "bin")
89-
build_cmd = ["dotnet", "build", domain_tests_dir, "-o", bin_path]
62+
domain_tests_dir = cwd / "domain_tests"
63+
domain_bin_path = domain_tests_dir / "bin"
64+
build_cmd = [
65+
"dotnet",
66+
"build",
67+
str(domain_tests_dir),
68+
"-o",
69+
str(domain_bin_path),
70+
]
9071
is_64bits = sys.maxsize > 2**32
9172
if not is_64bits:
9273
build_cmd += ["/p:Prefer32Bit=True"]
9374
check_call(build_cmd)
9475

76+
check_call(
77+
["dotnet", "publish", "-f", fw, "-o", str(bin_path), str(test_proj_path)]
78+
)
79+
80+
from pythonnet import load
81+
82+
load(runtime_opt, **runtime_params)
83+
84+
import clr
9585

86+
sys.path.append(str(bin_path))
87+
clr.AddReference("Python.Test")
9688

9789

9890
def pytest_unconfigure(config):
@@ -102,18 +94,16 @@ def pytest_unconfigure(config):
10294
except Exception:
10395
pass
10496

97+
10598
def pytest_report_header(config):
10699
"""Generate extra report headers"""
107100
# FIXME: https://github.com/pytest-dev/pytest/issues/2257
108101
is_64bits = sys.maxsize > 2**32
109102
arch = "x64" if is_64bits else "x86"
110103
ucs = ctypes.sizeof(ctypes.c_wchar)
111104
libdir = sysconfig.get_config_var("LIBDIR")
112-
shared = bool(sysconfig.get_config_var("Py_ENABLE_SHARED"))
113105

114-
header = ("Arch: {arch}, UCS: {ucs}, LIBDIR: {libdir}, "
115-
"Py_ENABLE_SHARED: {shared}".format(**locals()))
116-
return header
106+
return f"Arch: {arch}, UCS: {ucs}, LIBDIR: {libdir}"
117107

118108

119109
@pytest.fixture()

0 commit comments

Comments
 (0)