Skip to content

Commit

Permalink
Merge pull request #32 from pythonnet/documentation
Browse files Browse the repository at this point in the history
Documentation
  • Loading branch information
filmor authored Sep 16, 2022
2 parents 46143d4 + 4cbd29f commit 80b1732
Show file tree
Hide file tree
Showing 16 changed files with 464 additions and 15 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Documentation

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: ammaraskar/sphinx-action@master
with:
docs-folder: doc/
- name: Upload artifact
# Automatically uploads an artifact from the './_site' directory by default
uses: actions/upload-pages-artifact@v1
with:
path: doc/_build/html/

deploy:
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
60 changes: 55 additions & 5 deletions clr_loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,46 @@
"Runtime",
"Assembly",
"RuntimeInfo",
"DotnetCoreRuntimeSpec",
]


def get_mono(
*,
domain: Optional[str] = None,
# domain: Optional[str] = None,
config_file: Optional[StrOrPath] = None,
global_config_file: Optional[StrOrPath] = None,
libmono: Optional[StrOrPath] = None,
sgen: bool = True,
debug: bool = False,
jit_options: Optional[Sequence[str]] = None,
) -> Runtime:
"""Get a Mono runtime instance
:param config_file:
Path to the domain configuration file
:param global_config_file:
Path to the global configuration file to load (defaults to, e.g.,
``/etc/mono/config``)
:param libmono:
Path to the Mono runtime dll/so/dylib. If this is not specified, we try
to discover a globally installed instance using :py:func:`find_libmono`
:param sgen:
If ``libmono`` is not specified, this is passed to
:py:func:`find_libmono`
:param debug:
Whether to initialise Mono debugging
:param jit_options:
"Command line options" passed to Mono's ``mono_jit_parse_options``
"""
from .mono import Mono

libmono = _maybe_path(libmono)
if libmono is None:
libmono = find_libmono(sgen)
libmono = find_libmono(sgen=sgen)

impl = Mono(
domain=domain,
# domain=domain,
debug=debug,
jit_options=jit_options,
config_file=_maybe_path(config_file),
Expand All @@ -54,6 +73,27 @@ def get_coreclr(
properties: Optional[Dict[str, str]] = None,
runtime_spec: Optional[DotnetCoreRuntimeSpec] = None,
) -> Runtime:
"""Get a CoreCLR (.NET Core) runtime instance
The returned ``DotnetCoreRuntime`` also acts as a mapping of the config
properties. They can be retrieved using the index operator and can be
written until the runtime is initialized. The runtime is initialized when
the first function object is retrieved.
:param runtime_config:
Pass to a ``runtimeconfig.json`` as generated by
``dotnet publish``. If this parameter is not given, a temporary runtime
config will be generated.
:param dotnet_root:
The root directory of the .NET Core installation. If this is not
specified, we try to discover it using :py:func:`find_dotnet_root`.
:param properties:
Additional runtime properties. These can also be passed using the
``configProperties`` section in the runtime config.
: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`."""
from .hostfxr import DotnetCoreRuntime

dotnet_root = _maybe_path(dotnet_root)
Expand Down Expand Up @@ -91,11 +131,21 @@ def get_coreclr(


def get_netfx(
*, name: Optional[str] = None, config_file: Optional[StrOrPath] = None
*, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None
) -> Runtime:
"""Get a .NET Framework runtime instance
:param domain:
Name of the domain to create. If no value is passed, assemblies will be
loaded into the root domain.
:param config_file:
Configuration file to use to initialize the ``AppDomain``. This will
only be used for non-root-domains as we can not control the
configuration of the implicitly loaded root domain.
"""
from .netfx import NetFx

impl = NetFx(name=name, config_file=_maybe_path(config_file))
impl = NetFx(domain=domain, config_file=_maybe_path(config_file))
return impl


Expand Down
2 changes: 1 addition & 1 deletion clr_loader/hostfxr.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def __iter__(self) -> Generator[Tuple[str, str], None, None]:
for i in range(size_ptr[0]):
yield (decode(keys_ptr[i]), decode(values_ptr[i]))

def get_callable(self, assembly_path: StrOrPath, typename: str, function: str):
def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str):
# TODO: Maybe use coreclr_get_delegate as well, supported with newer API
# versions of hostfxr
self._is_initialized = True
Expand Down
2 changes: 1 addition & 1 deletion clr_loader/mono.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(
else:
raise NotImplementedError

def get_callable(self, assembly_path, typename, function):
def _get_callable(self, assembly_path, typename, function):
assembly_path = Path(assembly_path)
assembly = self._assemblies.get(assembly_path)
if not assembly:
Expand Down
14 changes: 9 additions & 5 deletions clr_loader/netfx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,31 @@


class NetFx(Runtime):
def __init__(self, name: Optional[str] = None, config_file: Optional[Path] = None):
def __init__(
self, domain: Optional[str] = None, config_file: Optional[Path] = None
):
initialize()
if config_file is not None:
config_file_s = str(config_file)
else:
config_file_s = ffi.NULL

self._name = name
self._domain = domain
self._config_file = config_file
self._domain = _FW.pyclr_create_appdomain(name or ffi.NULL, config_file_s)
self._domain = _FW.pyclr_create_appdomain(domain or ffi.NULL, config_file_s)

def info(self) -> RuntimeInfo:
return RuntimeInfo(
kind=".NET Framework",
version="<undefined>",
initialized=True,
shutdown=_FW is None,
properties={},
properties=dict(
domain=self._domain or "", config_file=str(self._config_file)
),
)

def get_callable(self, assembly_path: StrOrPath, typename: str, function: str):
def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str):
func = _FW.pyclr_get_function(
self._domain,
str(Path(assembly_path)).encode("utf8"),
Expand Down
53 changes: 51 additions & 2 deletions clr_loader/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@

@dataclass
class RuntimeInfo:
"""Information on a Runtime instance
An informative text can be retrieved from this by converting it to a
``str``, in particular the following results in readable debug information:
>>> ri = RuntimeInfo()
>>> print(ri)
6.12.0.122 (tarball)
Runtime: Mono
=============
Version: 6.12.0.122 (tarball)
Initialized: True
Shut down: False
Properties:
"""

kind: str
version: str
initialized: bool
Expand Down Expand Up @@ -39,7 +55,7 @@ def __init__(
self._class = typename
self._name = func_name

self._callable = runtime.get_callable(assembly, typename, func_name)
self._callable = runtime._get_callable(assembly, typename, func_name)

def __call__(self, buffer: bytes) -> int:
from .ffi import ffi
Expand All @@ -57,6 +73,21 @@ def __init__(self, runtime: "Runtime", path: StrOrPath):
self._path = path

def get_function(self, name: str, func: Optional[str] = None) -> ClrFunction:
"""Get a wrapped .NET function instance
The function must be ``static``, and it must have the signature
``int Func(IntPtr ptr, int size)``. The returned wrapped instance will
take a ``binary`` and call the .NET function with a pointer to that
buffer and the buffer length. The buffer is reflected using CFFI's
`from_buffer`.
:param name: If ``func`` is not given, this is the fully qualified name
of the function. If ``func`` is given, this is the fully
qualified name of the containing class
:param func: Name of the function
:return: A function object that takes a single ``binary`` parameter
and returns an ``int``
"""
if func is None:
name, func = name.rsplit(".", 1)

Expand All @@ -67,21 +98,39 @@ def __repr__(self) -> str:


class Runtime(metaclass=ABCMeta):
"""CLR Runtime
Encapsulates the lifetime of a CLR (.NET) runtime. If the instance is
deleted, the runtime will be shut down.
"""

@abstractmethod
def info(self) -> RuntimeInfo:
"""Get configuration and version information"""
pass

def get_assembly(self, assembly_path: StrOrPath) -> Assembly:
"""Get an assembly wrapper
This function does not guarantee that the respective assembly is or can
be loaded. Due to the design of the different hosting APIs, loading only
happens when the first function is referenced, and only then potential
errors will be raised."""
return Assembly(self, assembly_path)

@abstractmethod
def get_callable(
def _get_callable(
self, assembly_path: StrOrPath, typename: str, function: str
) -> Callable[[Any, int], Any]:
"""Private function to retrieve a low-level callable object"""
pass

@abstractmethod
def shutdown(self) -> None:
"""Shut down the runtime as much as possible
Implementations should still be able to "reinitialize", thus the final
cleanup will usually happen in an ``atexit`` handler."""
pass

def __del__(self) -> None:
Expand Down
37 changes: 36 additions & 1 deletion clr_loader/util/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ def find_dotnet_cli() -> Optional[Path]:


def find_dotnet_root() -> Path:
"""Try to discover the .NET Core root directory
If the environment variable ``DOTNET_ROOT`` is defined, we will use that.
Otherwise, we probe the default installation paths on Windows and macOS.
If none of these lead to a result, we try to discover the ``dotnet`` CLI
tool and use its (real) parent directory.
Otherwise, this function raises an exception.
:return: Path to the .NET Core root
"""

dotnet_root = os.environ.get("DOTNET_ROOT", None)
if dotnet_root is not None:
return Path(dotnet_root)
Expand Down Expand Up @@ -69,6 +82,17 @@ def find_runtimes_in_root(dotnet_root: Path) -> Iterator[DotnetCoreRuntimeSpec]:


def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]:
"""Find installed .NET Core runtimes
If the ``dotnet`` CLI can be found, we will call it as ``dotnet
--list-runtimes`` and parse the result.
If it is not available, we try to discover the dotnet root directory using
:py:func:`find_dotnet_root` and enumerate the runtimes installed in the
``shared`` subdirectory.
:return: Iterable of :py:class:`DotnetCoreRuntimeSpec` objects
"""
dotnet_cli = find_dotnet_cli()
if dotnet_cli is not None:
return find_runtimes_using_cli(dotnet_cli)
Expand All @@ -77,7 +101,18 @@ def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]:
return find_runtimes_in_root(dotnet_root)


def find_libmono(sgen: bool = True) -> Path:
def find_libmono(*, sgen: bool = True) -> Path:
"""Find a suitable libmono dynamic library
On Windows and macOS, we check the default installation directories.
:param sgen:
Whether to look for an SGen or Boehm GC instance. This parameter is
ignored on Windows, as only ``sgen`` is installed with the default
installer
:return:
Path to usable ``libmono``
"""
unix_name = f"mono{'sgen' if sgen else ''}-2.0"
if sys.platform == "win32":
if sys.maxsize > 2**32:
Expand Down
2 changes: 2 additions & 0 deletions clr_loader/util/runtime_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

@dataclass
class DotnetCoreRuntimeSpec:
"""Specification of an installed .NET Core runtime"""

name: str
version: str
path: Path
Expand Down
1 change: 1 addition & 0 deletions doc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_build/
20 changes: 20 additions & 0 deletions doc/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
Loading

0 comments on commit 80b1732

Please sign in to comment.