Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
# - Update macos images to at least 14 as 13 is retired
# - Update ubuntu images
os: [ubuntu-22.04, ubuntu-22.04-arm, windows-latest, macos-13]
python: ['3.14', '3.13', '3.12', '3.11', '3.10', '3.9', '3.8'] # pypy3
python: ['3.14', '3.13', '3.12', '3.11', '3.10'] # pypy3

steps:
- name: Set Environment on macOS
Expand Down
48 changes: 29 additions & 19 deletions clr_loader/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from collections.abc import Sequence
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Dict, Optional, Sequence

from .types import Assembly, Runtime, RuntimeInfo
from .util import StrOrPath
from .types import Assembly, Runtime, RuntimeInfo, StrOrPath
from .util.find import find_dotnet_root, find_libmono, find_runtimes
from .util.runtime_spec import DotnetCoreRuntimeSpec

Expand All @@ -24,17 +23,17 @@
def get_mono(
*,
# domain: Optional[str] = None,
config_file: Optional[StrOrPath] = None,
global_config_file: Optional[StrOrPath] = None,
libmono: Optional[StrOrPath] = None,
config_file: StrOrPath | None = None,
global_config_file: StrOrPath | None = None,
libmono: StrOrPath | None = None,
sgen: bool = True,
debug: bool = False,
jit_options: Optional[Sequence[str]] = None,
assembly_dir: Optional[str] = None,
config_dir: Optional[str] = None,
jit_options: Sequence[str] | None = None,
assembly_dir: StrOrPath | None = None,
config_dir: StrOrPath | None = None,
set_signal_chaining: bool = False,
trace_mask: Optional[str] = None,
trace_level: Optional[str] = None,
trace_mask: str | None = None,
trace_level: str | None = None,
) -> Runtime:
"""Get a Mono runtime instance

Expand Down Expand Up @@ -92,11 +91,12 @@ def get_mono(

def get_coreclr(
*,
runtime_config: Optional[StrOrPath] = None,
entry_dll: Optional[StrOrPath] = None,
dotnet_root: Optional[StrOrPath] = None,
properties: Optional[Dict[str, str]] = None,
runtime_spec: Optional[DotnetCoreRuntimeSpec] = None,
runtime_config: StrOrPath | None = None,
entry_dll: StrOrPath | None = None,
dotnet_root: StrOrPath | None = None,
properties: dict[str, str] | None = None,
runtime_spec: DotnetCoreRuntimeSpec | None = None,
runtime_version: str | None = None,
) -> Runtime:
"""Get a CoreCLR (.NET Core) runtime instance

Expand All @@ -122,7 +122,11 @@ def get_coreclr(
:param runtime_spec:
If the ``runtime_config`` is not specified, the concrete runtime to use
can be controlled by passing this parameter. Possible values can be
retrieved using :py:func:`find_runtimes`."""
retrieved using :py:func:`find_runtimes`.
:param runtime_version:
If neither the ``runtime_config`` nor the ``runtime_spec`` are specified,
the concrete runtime version to use can be controlled by passing the
major.minor version to this parameter."""

from .hostfxr import DotnetCoreRuntime

Expand All @@ -139,6 +143,12 @@ def get_coreclr(
candidates = [
rt for rt in find_runtimes() if rt.name == "Microsoft.NETCore.App"
]
if runtime_version is not None:
candidates = [
rt
for rt in candidates
if f"{rt.version_info[0]}.{rt.version_info[1]}" == runtime_version
]
candidates.sort(key=lambda spec: spec.version_info, reverse=True)
if not candidates:
raise RuntimeError("Failed to find a suitable runtime")
Expand Down Expand Up @@ -168,7 +178,7 @@ def get_coreclr(


def get_netfx(
*, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None
*, domain: str | None = None, config_file: StrOrPath | None = None
) -> Runtime:
"""Get a .NET Framework runtime instance

Expand All @@ -186,7 +196,7 @@ def get_netfx(
return impl


def _maybe_path(p: Optional[StrOrPath]) -> Optional[Path]:
def _maybe_path(p: StrOrPath | None) -> Path | None:
if p is None:
return None
else:
Expand Down
11 changes: 5 additions & 6 deletions clr_loader/ffi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import sys
from pathlib import Path
from typing import Optional, Tuple

import cffi # type: ignore
import cffi

from . import hostfxr, mono, netfx

__all__ = ["ffi", "load_hostfxr", "load_mono", "load_netfx"]

ffi = cffi.FFI() # type: ignore
ffi = cffi.FFI()

for cdef in hostfxr.cdef + mono.cdef + netfx.cdef:
ffi.cdef(cdef)
Expand All @@ -22,7 +21,7 @@ def load_hostfxr(dotnet_root: Path):
hostfxr_path = dotnet_root / "host" / "fxr"
hostfxr_paths = hostfxr_path.glob(f"*/{hostfxr_name}")

error_report = list()
error_report: list[str] = []

for hostfxr_path in reversed(sorted(hostfxr_paths, key=_path_to_version)):
try:
Expand All @@ -41,7 +40,7 @@ def load_hostfxr(dotnet_root: Path):
)


def load_mono(path: Optional[Path] = None):
def load_mono(path: Path | None = None):
# Preload C++ standard library, Mono needs that and doesn't properly link against it
if sys.platform == "linux":
ffi.dlopen("stdc++", ffi.RTLD_GLOBAL)
Expand All @@ -65,7 +64,7 @@ def load_netfx():
return ffi.dlopen(str(path))


def _path_to_version(path: Path) -> Tuple[int, int, int]:
def _path_to_version(path: Path) -> tuple[int, int, int]:
name = path.parent.name
try:
# Handle pre-release versions like "10.0.0-rc.1" by taking only the version part
Expand Down
8 changes: 4 additions & 4 deletions clr_loader/hostfxr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
from pathlib import Path
from typing import Generator, Tuple, Optional
from collections.abc import Generator

from .ffi import ffi, load_hostfxr
from .types import Runtime, RuntimeInfo, StrOrPath
Expand All @@ -18,8 +18,8 @@ def __init__(
self,
*,
dotnet_root: Path,
runtime_config: Optional[Path] = None,
entry_dll: Optional[Path] = None,
runtime_config: Path | None = None,
entry_dll: Path | None = None,
**params: str,
):
self._handle = None
Expand Down Expand Up @@ -81,7 +81,7 @@ def __setitem__(self, key: str, value: str) -> None:
)
check_result(res)

def __iter__(self) -> Generator[Tuple[str, str], None, None]:
def __iter__(self) -> Generator[tuple[str, str], None, None]:
if self.is_shutdown:
raise RuntimeError("Runtime is shut down")
max_size = 100
Expand Down
44 changes: 24 additions & 20 deletions clr_loader/mono.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import atexit
import re
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
from typing import Any
from collections.abc import Sequence

from .ffi import ffi, load_mono
from .types import Runtime, RuntimeInfo, StrOrPath
Expand All @@ -17,20 +18,20 @@
class Mono(Runtime):
def __init__(
self,
libmono: Optional[Path],
libmono: Path | None,
*,
domain: Optional[str] = None,
domain: str | None = None,
debug: bool = False,
jit_options: Optional[Sequence[str]] = None,
config_file: Optional[Path] = None,
global_config_file: Optional[Path] = None,
assembly_dir: Optional[str] = None,
config_dir: Optional[str] = None,
jit_options: Sequence[str] | None = None,
config_file: Path | None = None,
global_config_file: Path | None = None,
assembly_dir: StrOrPath | None = None,
config_dir: StrOrPath | None = None,
set_signal_chaining: bool = False,
trace_mask: Optional[str] = None,
trace_level: Optional[str] = None,
trace_mask: str | None = None,
trace_level: str | None = None,
):
self._assemblies: Dict[Path, Any] = {}
self._assemblies: dict[Path, Any] = {}

self._version: str = initialize(
config_file=optional_path_as_string(config_file),
Expand Down Expand Up @@ -129,16 +130,16 @@ def __call__(self, ptr, size):


def initialize(
libmono: Optional[Path],
libmono: Path | None,
debug: bool = False,
jit_options: Optional[Sequence[str]] = None,
config_file: Optional[str] = None,
global_config_file: Optional[str] = None,
assembly_dir: Optional[str] = None,
config_dir: Optional[str] = None,
jit_options: Sequence[str] | None = None,
config_file: str | None = None,
global_config_file: str | None = None,
assembly_dir: StrOrPath | None = None,
config_dir: StrOrPath | None = None,
set_signal_chaining: bool = False,
trace_mask: Optional[str] = None,
trace_level: Optional[str] = None,
trace_mask: str | None = None,
trace_level: str | None = None,
) -> str:
global _MONO, _ROOT_DOMAIN
if _MONO is None:
Expand All @@ -151,7 +152,10 @@ def initialize(
_MONO.mono_trace_set_level_string(trace_level.encode("utf8"))

if assembly_dir is not None and config_dir is not None:
_MONO.mono_set_dirs(assembly_dir.encode("utf8"), config_dir.encode("utf8"))
_MONO.mono_set_dirs(
path_as_string(assembly_dir).encode("utf8"),
path_as_string(config_dir).encode("utf8")
)

# Load in global config (i.e /etc/mono/config)
global_encoded = global_config_file or ffi.NULL
Expand Down
12 changes: 5 additions & 7 deletions clr_loader/netfx.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import atexit
from pathlib import Path
from typing import Any, Optional
from typing import Any

from .ffi import ffi, load_netfx
from .types import Runtime, RuntimeInfo, StrOrPath
Expand All @@ -9,10 +9,8 @@


class NetFx(Runtime):
def __init__(
self, domain: Optional[str] = None, config_file: Optional[Path] = None
):
self._domain: Optional[str] = None
def __init__(self, domain: str | None = None, config_file: Path | None = None):
self._domain: str | None = None

initialize()
if config_file is not None:
Expand All @@ -22,8 +20,8 @@ def __init__(

domain_s = domain.encode("utf8") if domain else ffi.NULL

self._domain_name: Optional[str] = domain
self._config_file: Optional[Path] = config_file
self._domain_name: str | None = domain
self._config_file: Path | None = config_file
self._domain = _FW.pyclr_create_appdomain(domain_s, config_file_s)

def info(self) -> RuntimeInfo:
Expand Down
12 changes: 5 additions & 7 deletions clr_loader/types.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from os import PathLike
from typing import Any, Callable, Dict, Optional, Union
from typing import Any
from collections.abc import Callable

__all__ = ["StrOrPath"]

try:
StrOrPath = Union[str, PathLike[str]]
except TypeError:
StrOrPath = Union[str, PathLike]
StrOrPath = str | PathLike[str]


@dataclass
Expand All @@ -33,7 +31,7 @@ class RuntimeInfo:
version: str
initialized: bool
shutdown: bool
properties: Dict[str, str] = field(repr=False)
properties: dict[str, str] = field(repr=False)

def __str__(self) -> str:
return (
Expand Down Expand Up @@ -75,7 +73,7 @@ def __init__(self, runtime: "Runtime", path: StrOrPath):
self._runtime: "Runtime" = runtime
self._path: StrOrPath = path

def get_function(self, name: str, func: Optional[str] = None) -> ClrFunction:
def get_function(self, name: str, func: str | None = None) -> ClrFunction:
"""Get a wrapped .NET function instance

The function must be ``static``, and it must have the signature
Expand Down
3 changes: 1 addition & 2 deletions clr_loader/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from typing import Optional

from ..types import StrOrPath
from .clr_error import ClrError
Expand All @@ -15,7 +14,7 @@
]


def optional_path_as_string(path: Optional[StrOrPath]) -> Optional[str]:
def optional_path_as_string(path: StrOrPath | None) -> str | None:
if path is None:
return None
return path_as_string(path)
Expand Down
7 changes: 3 additions & 4 deletions clr_loader/util/clr_error.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from typing import Optional


class ClrError(Exception):
def __init__(
self,
hresult: int,
name: Optional[str] = None,
message: Optional[str] = None,
comment: Optional[str] = None,
name: str | None = None,
message: str | None = None,
comment: str | None = None,
):
self.hresult = hresult
self.name = name
Expand Down
12 changes: 5 additions & 7 deletions clr_loader/util/coreclr_errors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from typing import Dict, Optional

from .clr_error import ClrError


def get_coreclr_error(hresult: int) -> Optional[ClrError]:
def get_coreclr_error(hresult: int) -> ClrError | None:
name = SymbolicName.get(hresult)
if not name:
return None
Expand All @@ -16,9 +14,9 @@ def get_coreclr_error(hresult: int) -> Optional[ClrError]:
)


Comment: Dict[int, str] = {}
SymbolicName: Dict[int, str] = {}
Message: Dict[int, str] = {}
Comment: dict[int, str] = {}
SymbolicName: dict[int, str] = {}
Message: dict[int, str] = {}


if __name__ == "__main__":
Expand All @@ -38,7 +36,7 @@ def get_coreclr_error(hresult: int) -> Optional[ClrError]:

marker = "# == Autogenerated from corerror.xml =="

with open(__file__, "r") as f:
with open(__file__) as f:
current = f.read()
before, _, _ = current.rpartition(marker)

Expand Down
Loading
Loading