From 9a2f93ce17032c7900a03afabcc814100d195862 Mon Sep 17 00:00:00 2001 From: Simon Birkholz Date: Tue, 18 Aug 2026 14:51:33 +0200 Subject: [PATCH 1/5] Expose what the binding needs and what the installed SDK provides The binding is compiled against one cuvis SDK and can run against another, since the library is installed separately. When the installed one is older it may not export everything the binding imports; the binding survives that and reports it, but there was no clean way to ask about it from here. cuvis.binding adds that: info() returns a BindingInfo with the version built against, the version and path actually loaded and any functions the library does not provide, and prints as a report fit for pasting into a bug report. missing_symbols(), available() and require() cover the common checks, and require() raises UnavailableSDKFunction, an SDKException, so an unavailable function is caught as an ordinary cuvis error rather than a bare RuntimeError. Degrades quietly against a binding too old to report any of this: everything reads empty and available() stays true. --- cuvis/__init__.py | 2 + cuvis/binding.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 cuvis/binding.py diff --git a/cuvis/__init__.py b/cuvis/__init__.py index 9bcb06c..0bcaa2c 100644 --- a/cuvis/__init__.py +++ b/cuvis/__init__.py @@ -35,6 +35,8 @@ WorkerSettings, ViewerSettings, ) +from . import binding +from .binding import BindingInfo, UnavailableSDKFunction from .Export import CubeExporter, EnviExporter, TiffExporter, ViewExporter from .Calibration import Calibration from .AcquisitionContext import AcquisitionContext diff --git a/cuvis/binding.py b/cuvis/binding.py new file mode 100644 index 0000000..6da8571 --- /dev/null +++ b/cuvis/binding.py @@ -0,0 +1,107 @@ +"""What the compiled binding expects, and what the installed cuvis library provides. + +The binding is compiled against one version of the cuvis SDK and can end up running +against another, because the library is installed separately. When the installed one is +older it may not export everything the binding imports. The binding survives that and +reports it; this module is the place to ask about it: + + from cuvis import binding + + print(binding.info()) # a report fit for a bug report + if not binding.info().is_complete: + ... # some SDK functions are unavailable + binding.require("cuvis_measurement_get_data_image_cuda") # raises if unavailable + +Calling an unavailable function raises `UnavailableSDKFunction`, so it can be caught as +an ordinary cuvis error rather than a bare RuntimeError from the binding layer. +""" +from dataclasses import dataclass, field +from typing import FrozenSet, Tuple + +from ._cuvis_il import cuvis_il +from .cuvis_aux import SDKException + + +class UnavailableSDKFunction(SDKException): + """The installed cuvis library does not export a function the binding needs.""" + + def __init__(self, *names: str): + self.names = tuple(names) + current = info() + message = ( + "the installed CUVIS SDK ({}) does not provide {}; this binding was built " + "against {}".format( + current.library_version or "unknown version", + ", ".join(self.names) or "a required function", + current.built_against or "an unknown version")) + # Deliberately not SDKException.__init__: there is no SDK-side last error to read, + # the library never got as far as being called. + Exception.__init__(self, message) + self.message = message + + +@dataclass(frozen=True) +class BindingInfo: + """A snapshot of the binding and the cuvis library it loaded.""" + + built_against: str + library_version: str + library_path: str + missing_symbols: Tuple[str, ...] = field(default_factory=tuple) + + @property + def is_complete(self) -> bool: + """True when the loaded library exports everything the binding imports.""" + return not self.missing_symbols + + def __str__(self) -> str: + lines = [ + "cuvis binding", + " built against : {}".format(self.built_against or "unknown"), + " loaded library: {}".format(self.library_version or "unknown"), + " library path : {}".format(self.library_path or "unknown"), + ] + if self.is_complete: + lines.append(" status : complete") + else: + lines.append(" status : {} function(s) not provided by this SDK" + .format(len(self.missing_symbols))) + lines.extend(" {}".format(name) + for name in self.missing_symbols) + return "\n".join(lines) + + +def info() -> BindingInfo: + """Everything known about the binding and the library it is running against.""" + return BindingInfo( + built_against=getattr(cuvis_il, "built_against_version", ""), + library_version=getattr(cuvis_il, "library_version", ""), + library_path=getattr(cuvis_il, "library_path", ""), + missing_symbols=tuple(getattr(cuvis_il, "missing_symbols", ())), + ) + + +def missing_symbols() -> FrozenSet[str]: + """Functions the binding imports that the installed cuvis library does not export. + + Empty with a matching SDK, and also empty on a binding too old to report it. + """ + return frozenset(getattr(cuvis_il, "missing_symbols", ())) + + +def available(*names: str) -> bool: + """True when every named function is provided by the installed cuvis library.""" + absent = missing_symbols() + return not any(name in absent for name in names) + + +def require(*names: str) -> None: + """Raise UnavailableSDKFunction naming whichever of `names` is not provided.""" + absent = missing_symbols() + unavailable = tuple(name for name in names if name in absent) + if unavailable: + raise UnavailableSDKFunction(*unavailable) + + +__all__ = ["BindingInfo", "UnavailableSDKFunction", "info", "missing_symbols", + "available", "require"] From c2c9ca559d3ab3fd323eb4bcaa4d28beed47b400 Mon Sep 17 00:00:00 2001 From: Simon Birkholz Date: Tue, 18 Aug 2026 16:03:35 +0200 Subject: [PATCH 2/5] Document cuvis.binding, and make its error catchable either way The module now carries full docstrings: what the feature is for, which error each route raises, what every BindingInfo field means, and what each function returns or raises, with short examples in the reST style the repo already uses in doc.py. Writing them turned up two things worth correcting rather than describing. The module claimed that calling an unavailable function raises UnavailableSDKFunction. It does not: the binding layer raises a plain RuntimeError, and only require() raised the typed one, so the same condition surfaced as two unrelated exception types. UnavailableSDKFunction now derives from RuntimeError as well as SDKException, so one except clause covers either route and the documented behaviour is true. The report also printed "status: complete" for a binding too old to report missing functions, asserting something it cannot know. It now says the status is unknown, and is_complete documents that an empty list is all such a binding can offer. --- cuvis/binding.py | 128 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/cuvis/binding.py b/cuvis/binding.py index 6da8571..fe34ba5 100644 --- a/cuvis/binding.py +++ b/cuvis/binding.py @@ -1,19 +1,37 @@ -"""What the compiled binding expects, and what the installed cuvis library provides. +"""Inspect the compiled binding and the cuvis library it is running against. -The binding is compiled against one version of the cuvis SDK and can end up running -against another, because the library is installed separately. When the installed one is -older it may not export everything the binding imports. The binding survives that and -reports it; this module is the place to ask about it: +The Python binding (``cuvis_il``) is compiled against one version of the cuvis SDK, +but the SDK itself is installed separately on the machine. The two can therefore +disagree: an installed library older than the binding may not export every function +the binding imports. The binding tolerates that rather than failing to import, and +records what it found; this module is how that information is read back. + +Nothing here needs the SDK to be initialised, so it can be called before +:func:`cuvis.init` to decide whether an operation is worth attempting at all. + +.. code-block:: python3 from cuvis import binding - print(binding.info()) # a report fit for a bug report + print(binding.info()) # human readable, fit for a bug report + if not binding.info().is_complete: - ... # some SDK functions are unavailable - binding.require("cuvis_measurement_get_data_image_cuda") # raises if unavailable + ... # this SDK is missing something + + binding.require("cuvis_measurement_get_data_image_cuda") # or raise + +Which error you get depends on how the unavailable function is reached: + +* calling one through ``cuvis_il`` directly, or through a wrapper such as + :meth:`cuvis.Measurement.get_cube_cuda` that calls into it, raises + :class:`RuntimeError` from the binding layer, naming the function; +* calling :func:`require` first raises :class:`UnavailableSDKFunction`, which also + derives from :class:`RuntimeError`, so a single ``except RuntimeError`` covers + both, while ``except SDKException`` still catches it as an ordinary cuvis error. -Calling an unavailable function raises `UnavailableSDKFunction`, so it can be caught as -an ordinary cuvis error rather than a bare RuntimeError from the binding layer. +Against a binding too old to report any of this (an older ``cuvis_il`` wheel), every +query answers empty: :func:`missing_symbols` is empty, :func:`available` is ``True`` +and :func:`require` never raises. Absence of evidence, not evidence of absence. """ from dataclasses import dataclass, field from typing import FrozenSet, Tuple @@ -22,8 +40,18 @@ from .cuvis_aux import SDKException -class UnavailableSDKFunction(SDKException): - """The installed cuvis library does not export a function the binding needs.""" +class UnavailableSDKFunction(SDKException, RuntimeError): + """The installed cuvis library does not provide a function that was required. + + Raised by :func:`require`. It derives from both :class:`SDKException` and + :class:`RuntimeError` on purpose: the binding layer raises a plain + :class:`RuntimeError` when an unavailable function is called directly, so + deriving from it lets one ``except RuntimeError`` handle either route, without + giving up ``except SDKException`` for code that treats all cuvis errors alike. + + :ivar names: the functions that were required but are not provided, in the order + they were requested. + """ def __init__(self, *names: str): self.names = tuple(names) @@ -34,15 +62,29 @@ def __init__(self, *names: str): current.library_version or "unknown version", ", ".join(self.names) or "a required function", current.built_against or "an unknown version")) - # Deliberately not SDKException.__init__: there is no SDK-side last error to read, - # the library never got as far as being called. + # Deliberately not SDKException.__init__: that reads the SDK's last-error + # string, and here the library was never reached to set one. Exception.__init__(self, message) self.message = message @dataclass(frozen=True) class BindingInfo: - """A snapshot of the binding and the cuvis library it loaded.""" + """A snapshot of the binding and the cuvis library loaded alongside it. + + Obtained from :func:`info`; printing it yields a short report suitable for + pasting into a bug report. + + :ivar built_against: version of the cuvis SDK the binding was compiled against, + for example ``"3.5.3"``. Empty if the binding predates this feature. + :ivar library_version: full version banner reported by the library that was + actually loaded, for example ``"CUBERT SDK v. 3.4.1 build: d20de35..."``. + Empty if it could not be read. + :ivar library_path: file the binding loaded, for example + ``"/lib/cuvis/libcuvis.so"``. Useful when several copies are installed. + :ivar missing_symbols: names of functions the binding imports that the loaded + library does not export. Empty when the two agree. + """ built_against: str library_version: str @@ -51,17 +93,29 @@ class BindingInfo: @property def is_complete(self) -> bool: - """True when the loaded library exports everything the binding imports.""" + """Whether the loaded library provides everything the binding imports. + + ``True`` also when the binding is too old to report missing functions, since + an empty list is all it can offer. + """ return not self.missing_symbols def __str__(self) -> str: + """Render the snapshot as a short multi-line report. + + A binding that predates this feature is reported as unknown rather than as + complete, since an empty list of missing functions is all it can offer and + that is not the same as having checked. + """ lines = [ "cuvis binding", " built against : {}".format(self.built_against or "unknown"), " loaded library: {}".format(self.library_version or "unknown"), " library path : {}".format(self.library_path or "unknown"), ] - if self.is_complete: + if not self.built_against: + lines.append(" status : unknown, this binding does not report it") + elif self.is_complete: lines.append(" status : complete") else: lines.append(" status : {} function(s) not provided by this SDK" @@ -72,7 +126,15 @@ def __str__(self) -> str: def info() -> BindingInfo: - """Everything known about the binding and the library it is running against.""" + """Report the binding, the library it loaded and any functions it lacks. + + Cheap: the binding works all of this out once while being imported, so this only + reads the result. + + :return: a :class:`BindingInfo` snapshot. Fields the binding cannot supply, which + is everything when it predates this feature, come back empty rather than + raising. + """ return BindingInfo( built_against=getattr(cuvis_il, "built_against_version", ""), library_version=getattr(cuvis_il, "library_version", ""), @@ -84,19 +146,43 @@ def info() -> BindingInfo: def missing_symbols() -> FrozenSet[str]: """Functions the binding imports that the installed cuvis library does not export. - Empty with a matching SDK, and also empty on a binding too old to report it. + :return: the set of C function names, empty when the SDK matches the binding and + also when the binding is too old to report them. """ return frozenset(getattr(cuvis_il, "missing_symbols", ())) def available(*names: str) -> bool: - """True when every named function is provided by the installed cuvis library.""" + """Whether every named function is provided by the installed cuvis library. + + .. code-block:: python3 + + if binding.available("cuvis_measurement_get_data_image_cuda"): + cube = mesu.get_cube_cuda() + + :param names: C function names as they appear in ``cuvis.h``. + :return: ``True`` if none of them is reported missing. With a binding too old to + report anything this is always ``True``, so treat it as "nothing known to be + missing" rather than a guarantee. + """ absent = missing_symbols() return not any(name in absent for name in names) def require(*names: str) -> None: - """Raise UnavailableSDKFunction naming whichever of `names` is not provided.""" + """Raise unless every named function is provided by the installed cuvis library. + + Use it at the start of an operation to fail with a clear explanation, instead of + letting a call fail deeper in with less context. + + .. code-block:: python3 + + binding.require("cuvis_cuda_mem_get_view", "cuvis_cuda_mem_free") + + :param names: C function names as they appear in ``cuvis.h``. + :raises UnavailableSDKFunction: naming whichever of them are missing; the message + also states the loaded SDK version and the one the binding expects. + """ absent = missing_symbols() unavailable = tuple(name for name in names if name in absent) if unavailable: From df017e693708a17272d9ef936634f481c309a001 Mon Sep 17 00:00:00 2001 From: Simon Birkholz Date: Wed, 19 Aug 2026 10:13:22 +0200 Subject: [PATCH 3/5] wip docs --- cuvis/binding.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cuvis/binding.py b/cuvis/binding.py index fe34ba5..4bb0dca 100644 --- a/cuvis/binding.py +++ b/cuvis/binding.py @@ -75,10 +75,12 @@ class BindingInfo: Obtained from :func:`info`; printing it yields a short report suitable for pasting into a bug report. - :ivar built_against: version of the cuvis SDK the binding was compiled against, - for example ``"3.5.3"``. Empty if the binding predates this feature. - :ivar library_version: full version banner reported by the library that was - actually loaded, for example ``"CUBERT SDK v. 3.4.1 build: d20de35..."``. + :ivar built_against: version banner of the cuvis SDK the binding was compiled + against, for example ``"CUBERT SDK v. 3.5.3 build: 0f416fb..."``. Reported in + the same form as :attr:`library_version` so the two can be read side by side: + the build hash is what tells apart two libraries that report the same version. + Empty if the binding predates this feature. + :ivar library_version: the same banner, from the library that was actually loaded. Empty if it could not be read. :ivar library_path: file the binding loaded, for example ``"/lib/cuvis/libcuvis.so"``. Useful when several copies are installed. From beef27260a2424ca8bc76ea433d3a71713bcb90c Mon Sep 17 00:00:00 2001 From: Simon Birkholz Date: Wed, 19 Aug 2026 14:03:07 +0200 Subject: [PATCH 4/5] chore: run ruff format --- cuvis/binding.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cuvis/binding.py b/cuvis/binding.py index 4bb0dca..8a89a1c 100644 --- a/cuvis/binding.py +++ b/cuvis/binding.py @@ -33,6 +33,7 @@ query answers empty: :func:`missing_symbols` is empty, :func:`available` is ``True`` and :func:`require` never raises. Absence of evidence, not evidence of absence. """ + from dataclasses import dataclass, field from typing import FrozenSet, Tuple @@ -61,7 +62,9 @@ def __init__(self, *names: str): "against {}".format( current.library_version or "unknown version", ", ".join(self.names) or "a required function", - current.built_against or "an unknown version")) + current.built_against or "an unknown version", + ) + ) # Deliberately not SDKException.__init__: that reads the SDK's last-error # string, and here the library was never reached to set one. Exception.__init__(self, message) @@ -120,10 +123,14 @@ def __str__(self) -> str: elif self.is_complete: lines.append(" status : complete") else: - lines.append(" status : {} function(s) not provided by this SDK" - .format(len(self.missing_symbols))) - lines.extend(" {}".format(name) - for name in self.missing_symbols) + lines.append( + " status : {} function(s) not provided by this SDK".format( + len(self.missing_symbols) + ) + ) + lines.extend( + " {}".format(name) for name in self.missing_symbols + ) return "\n".join(lines) @@ -191,5 +198,11 @@ def require(*names: str) -> None: raise UnavailableSDKFunction(*unavailable) -__all__ = ["BindingInfo", "UnavailableSDKFunction", "info", "missing_symbols", - "available", "require"] +__all__ = [ + "BindingInfo", + "UnavailableSDKFunction", + "info", + "missing_symbols", + "available", + "require", +] From 425f60ed697dd0e213e2d5e45af73fedee99789a Mon Sep 17 00:00:00 2001 From: Simon Birkholz Date: Wed, 19 Aug 2026 15:03:15 +0200 Subject: [PATCH 5/5] chore: updating CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a36de..36ec078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ Pre-releases (`b*`, `rc*`) are not listed. - `CI` - `.github/workflows/release.yml` is driven by `v*.*.*.*` tags: it validates the tag against `pyproject.toml` and against this file, builds, publishes to TestPyPI, and publishes to PyPI plus a GitHub Release after manual approval. - `CI` - `scripts/check_changelog.py` validates this file's structure (header format, allowed section names, descending versions) and the tag/version/changelog agreement at release time. - `CONTRIBUTING.md` - documents the branch model, the version scheme, the changelog conventions and the release checklist. +- `cuvis.BindingInfo` - new frozen dataclass with the fields `built_against: str`, `library_version: str`, `library_path: str` and `missing_symbols: Tuple[str, ...]`, the read-only property `is_complete: bool`, and a `__str__` rendering a report fit for a bug report. +- `cuvis.UnavailableSDKFunction` - new exception deriving from both `cuvis.cuvis_aux.SDKException` and `RuntimeError`, with the field `names: Tuple[str, ...]`. +- `cuvis.binding` - new module reporting the compiled binding, the cuvis library loaded beside it, and the functions that library does not provide. + Nothing in it needs the SDK to be initialised, so it can be called before `cuvis.init`. +- `cuvis.binding.available` - new function, returns `bool`. +- `cuvis.binding.info` - new function, returns `BindingInfo`. +- `cuvis.binding.missing_symbols` - new function, returns `FrozenSet[str]`. +- `cuvis.binding.require` - new function, raises `UnavailableSDKFunction` naming whichever of the given functions the installed cuvis library does not provide. - `pyproject.toml` - `dev` extra pinning `ruff==0.16.3`, plus `[tool.ruff]` configuration selecting the `E4`, `E7`, `E9` and `F` rule sets. ### Changed