|
| 1 | +"""Python.NET runtime loading and configuration""" |
| 2 | + |
1 | 3 | import sys |
| 4 | +from pathlib import Path |
| 5 | +from typing import Dict, Optional, Union, Any |
2 | 6 | import clr_loader |
3 | 7 |
|
4 | | -_RUNTIME = None |
5 | | -_LOADER_ASSEMBLY = None |
6 | | -_FFI = None |
7 | | -_LOADED = False |
| 8 | +__all__ = ["set_runtime", "set_runtime_from_env", "load", "unload", "get_runtime_info"] |
| 9 | + |
| 10 | +_RUNTIME: Optional[clr_loader.Runtime] = None |
| 11 | +_LOADER_ASSEMBLY: Optional[clr_loader.Assembly] = None |
| 12 | +_LOADED: bool = False |
| 13 | + |
| 14 | + |
| 15 | +def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None: |
| 16 | + """Set up a clr_loader runtime without loading it |
8 | 17 |
|
| 18 | + :param runtime: |
| 19 | + Either an already initialised `clr_loader` runtime, or one of netfx, |
| 20 | + coreclr, mono, or default. If a string parameter is given, the runtime |
| 21 | + will be created. |
| 22 | + """ |
9 | 23 |
|
10 | | -def set_runtime(runtime): |
11 | 24 | global _RUNTIME |
12 | 25 | if _LOADED: |
13 | | - raise RuntimeError("The runtime {} has already been loaded".format(_RUNTIME)) |
| 26 | + raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded") |
14 | 27 |
|
15 | | - _RUNTIME = runtime |
| 28 | + if isinstance(runtime, str): |
| 29 | + runtime = _create_runtime_from_spec(runtime, params) |
16 | 30 |
|
| 31 | + _RUNTIME = runtime |
17 | 32 |
|
18 | | -def set_default_runtime() -> None: |
19 | | - if sys.platform == "win32": |
20 | | - set_runtime(clr_loader.get_netfx()) |
21 | | - else: |
22 | | - set_runtime(clr_loader.get_mono()) |
23 | 33 |
|
| 34 | +def get_runtime_info() -> Optional[clr_loader.RuntimeInfo]: |
| 35 | + """Retrieve information on the configured runtime""" |
24 | 36 |
|
25 | | -def load(): |
26 | | - global _FFI, _LOADED, _LOADER_ASSEMBLY |
| 37 | + if _RUNTIME is None: |
| 38 | + return None |
| 39 | + else: |
| 40 | + return _RUNTIME.info() |
| 41 | + |
| 42 | + |
| 43 | +def _get_params_from_env(prefix: str) -> Dict[str, str]: |
| 44 | + from os import environ |
| 45 | + |
| 46 | + full_prefix = f"PYTHONNET_{prefix.upper()}_" |
| 47 | + len_ = len(full_prefix) |
| 48 | + |
| 49 | + env_vars = { |
| 50 | + (k[len_:].lower()): v |
| 51 | + for k, v in environ.items() |
| 52 | + if k.upper().startswith(full_prefix) |
| 53 | + } |
| 54 | + |
| 55 | + return env_vars |
| 56 | + |
| 57 | + |
| 58 | +def _create_runtime_from_spec( |
| 59 | + spec: str, params: Optional[Dict[str, Any]] = None |
| 60 | +) -> clr_loader.Runtime: |
| 61 | + was_default = False |
| 62 | + if spec == "default": |
| 63 | + was_default = True |
| 64 | + if sys.platform == "win32": |
| 65 | + spec = "netfx" |
| 66 | + else: |
| 67 | + spec = "mono" |
| 68 | + |
| 69 | + params = params or _get_params_from_env(spec) |
| 70 | + |
| 71 | + try: |
| 72 | + if spec == "netfx": |
| 73 | + return clr_loader.get_netfx(**params) |
| 74 | + elif spec == "mono": |
| 75 | + return clr_loader.get_mono(**params) |
| 76 | + elif spec == "coreclr": |
| 77 | + return clr_loader.get_coreclr(**params) |
| 78 | + else: |
| 79 | + raise RuntimeError(f"Invalid runtime name: '{spec}'") |
| 80 | + except Exception as exc: |
| 81 | + if was_default: |
| 82 | + raise RuntimeError( |
| 83 | + f"""Failed to create a default .NET runtime, which would |
| 84 | + have been "{spec}" on this system. Either install a |
| 85 | + compatible runtime or configure it explicitly via |
| 86 | + `set_runtime` or the `PYTHONNET_*` environment variables |
| 87 | + (see set_runtime_from_env).""" |
| 88 | + ) from exc |
| 89 | + else: |
| 90 | + raise RuntimeError( |
| 91 | + f"""Failed to create a .NET runtime ({spec}) using the |
| 92 | + parameters {params}.""" |
| 93 | + ) from exc |
| 94 | + |
| 95 | + |
| 96 | +def set_runtime_from_env() -> None: |
| 97 | + """Set up the runtime using the environment |
| 98 | +
|
| 99 | + This will use the environment variable PYTHONNET_RUNTIME to decide the |
| 100 | + runtime to use, which may be one of netfx, coreclr or mono. The parameters |
| 101 | + of the respective clr_loader.get_<runtime> functions can also be given as |
| 102 | + environment variables, named `PYTHONNET_<RUNTIME>_<PARAM_NAME>`. In |
| 103 | + particular, to use `PYTHONNET_RUNTIME=coreclr`, the variable |
| 104 | + `PYTHONNET_CORECLR_RUNTIME_CONFIG` has to be set to a valid |
| 105 | + `.runtimeconfig.json`. |
| 106 | +
|
| 107 | + If no environment variable is specified, a globally installed Mono is used |
| 108 | + for all environments but Windows, on Windows the legacy .NET Framework is |
| 109 | + used. |
| 110 | + """ |
| 111 | + from os import environ |
| 112 | + |
| 113 | + spec = environ.get("PYTHONNET_RUNTIME", "default") |
| 114 | + runtime = _create_runtime_from_spec(spec) |
| 115 | + set_runtime(runtime) |
| 116 | + |
| 117 | + |
| 118 | +def load(runtime: Union[clr_loader.Runtime, str, None] = None, **params: str) -> None: |
| 119 | + """Load Python.NET in the specified runtime |
| 120 | +
|
| 121 | + The same parameters as for `set_runtime` can be used. By default, |
| 122 | + `set_default_runtime` is called if no environment has been set yet and no |
| 123 | + parameters are passed. |
| 124 | +
|
| 125 | + After a successful call, further invocations will return immediately.""" |
| 126 | + global _LOADED, _LOADER_ASSEMBLY |
27 | 127 |
|
28 | 128 | if _LOADED: |
29 | 129 | return |
30 | 130 |
|
31 | | - from os.path import join, dirname |
| 131 | + if _RUNTIME is None: |
| 132 | + if runtime is None: |
| 133 | + set_runtime_from_env() |
| 134 | + else: |
| 135 | + set_runtime(runtime, **params) |
32 | 136 |
|
33 | 137 | 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() |
| 138 | + raise RuntimeError("No valid runtime selected") |
37 | 139 |
|
38 | | - dll_path = join(dirname(__file__), "runtime", "Python.Runtime.dll") |
| 140 | + dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll" |
39 | 141 |
|
40 | | - _LOADER_ASSEMBLY = _RUNTIME.get_assembly(dll_path) |
| 142 | + _LOADER_ASSEMBLY = assembly = _RUNTIME.get_assembly(str(dll_path)) |
| 143 | + func = assembly.get_function("Python.Runtime.Loader.Initialize") |
41 | 144 |
|
42 | | - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"] |
43 | 145 | if func(b"") != 0: |
44 | 146 | raise RuntimeError("Failed to initialize Python.Runtime.dll") |
| 147 | + |
| 148 | + _LOADED = True |
45 | 149 |
|
46 | 150 | import atexit |
47 | 151 |
|
48 | 152 | atexit.register(unload) |
49 | 153 |
|
50 | 154 |
|
51 | | -def unload(): |
52 | | - global _RUNTIME |
| 155 | +def unload() -> None: |
| 156 | + """Explicitly unload a loaded runtime and shut down Python.NET""" |
| 157 | + |
| 158 | + global _RUNTIME, _LOADER_ASSEMBLY |
53 | 159 | if _LOADER_ASSEMBLY is not None: |
54 | | - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"] |
| 160 | + func = _LOADER_ASSEMBLY.get_function("Python.Runtime.Loader.Shutdown") |
55 | 161 | if func(b"full_shutdown") != 0: |
56 | 162 | raise RuntimeError("Failed to call Python.NET shutdown") |
57 | 163 |
|
| 164 | + _LOADER_ASSEMBLY = None |
| 165 | + |
58 | 166 | if _RUNTIME is not None: |
59 | | - # TODO: Add explicit `close` to clr_loader |
| 167 | + _RUNTIME.shutdown() |
60 | 168 | _RUNTIME = None |
0 commit comments