diff --git a/pythonnet/__init__.py b/pythonnet/__init__.py index 10dc403e4..5c1ca108a 100644 --- a/pythonnet/__init__.py +++ b/pythonnet/__init__.py @@ -1,60 +1,168 @@ +"""Python.NET runtime loading and configuration""" + import sys +from pathlib import Path +from typing import Dict, Optional, Union, Any import clr_loader -_RUNTIME = None -_LOADER_ASSEMBLY = None -_FFI = None -_LOADED = False +__all__ = ["set_runtime", "set_runtime_from_env", "load", "unload", "get_runtime_info"] + +_RUNTIME: Optional[clr_loader.Runtime] = None +_LOADER_ASSEMBLY: Optional[clr_loader.Assembly] = None +_LOADED: bool = False + + +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. + """ -def set_runtime(runtime): global _RUNTIME if _LOADED: - raise RuntimeError("The runtime {} has already been loaded".format(_RUNTIME)) + raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded") - _RUNTIME = runtime + 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()) - else: - set_runtime(clr_loader.get_mono()) +def get_runtime_info() -> Optional[clr_loader.RuntimeInfo]: + """Retrieve information on the configured runtime""" -def load(): - global _FFI, _LOADED, _LOADER_ASSEMBLY + if _RUNTIME is None: + return None + else: + return _RUNTIME.info() + + +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, Any]] = None +) -> clr_loader.Runtime: + was_default = False + if spec == "default": + was_default = True + if sys.platform == "win32": + spec = "netfx" + else: + spec = "mono" + + params = params or _get_params_from_env(spec) + + try: + 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: + raise RuntimeError(f"Invalid runtime name: '{spec}'") + except Exception as exc: + if was_default: + raise RuntimeError( + f"""Failed to create a default .NET runtime, which would + have been "{spec}" on this system. Either install a + compatible runtime or configure it explicitly via + `set_runtime` or the `PYTHONNET_*` environment variables + (see set_runtime_from_env).""" + ) from exc + else: + raise RuntimeError( + f"""Failed to create a .NET runtime ({spec}) using the + parameters {params}.""" + ) from exc + + +def set_runtime_from_env() -> None: + """Set up the runtime using the environment + + 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_ functions can also be given as + environment variables, named `PYTHONNET__`. 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 + + spec = environ.get("PYTHONNET_RUNTIME", "default") + runtime = _create_runtime_from_spec(spec) + set_runtime(runtime) + + +def load(runtime: Union[clr_loader.Runtime, str, None] = None, **params: 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. + + After a successful call, further invocations will return immediately.""" + global _LOADED, _LOADER_ASSEMBLY if _LOADED: return - from os.path import join, dirname + if _RUNTIME is None: + if runtime is None: + set_runtime_from_env() + else: + 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 = assembly = _RUNTIME.get_assembly(str(dll_path)) + func = assembly.get_function("Python.Runtime.Loader.Initialize") - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"] if func(b"") != 0: raise RuntimeError("Failed to initialize Python.Runtime.dll") + + _LOADED = True import atexit atexit.register(unload) -def unload(): - global _RUNTIME +def unload() -> None: + """Explicitly unload a loaded runtime and shut down Python.NET""" + + global _RUNTIME, _LOADER_ASSEMBLY if _LOADER_ASSEMBLY is not None: - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"] + func = _LOADER_ASSEMBLY.get_function("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.shutdown() _RUNTIME = None diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 8f00c10f1..dd31e6b21 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 286e75938..5e90074cf 100644 --- a/src/runtime/Properties/AssemblyInfo.cs +++ b/src/runtime/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] -[assembly: AssemblyVersion("2.0.51")] -[assembly: AssemblyFileVersion("2.0.51")] +[assembly: AssemblyVersion("2.0.52")] +[assembly: AssemblyFileVersion("2.0.52")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 840482980..85aae5de1 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -5,7 +5,7 @@ Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.51 + 2.0.52 false LICENSE https://github.com/pythonnet/pythonnet