Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,25 @@ Pre-releases (`b*`, `rc*`) are not listed.
The SDK derives this value as midnight on the calibration day in the host's standard local time, so unlike the other timestamps the instant it denotes shifts with the reading machine; treat it as a day, not as an exact moment.
- `cuvis.Measurement.factory_calibration` - type changed from `datetime.datetime` to `Optional[datetime.datetime]`, matching the existing fallback to `None` for SDK values the `datetime` range cannot represent.
Only the day carries meaning: the SDK stores it as midnight in the local time of the machine that wrote the file, so the time component is an artifact of that machine and the day can be off by one when the file is read in another timezone.
- `pyproject.toml` - `requires-python` raised from `>=3.9` to `>=3.10`, Python 3.9 having reached end of life in October 2025.
- Annotations throughout `cuvis` restated in the forms Python 3.10 provides: `Union[A, B]` and `Optional[A]` became `A | B` and `A | None`, and `Tuple`, `FrozenSet`, `Sequence`, `Callable` and `Awaitable` now come from `builtins` and `collections.abc` rather than `typing`.
Every signature denotes what it denoted before; `cuvis.cube_utils.ImageData.__getitem__` keeps `Union`, because its member list contains a forward reference and `|` cannot join a type to a string at runtime.

### Removed

- `CI` - `.github/workflows/tests.yml` and `.github/workflows/publish_version.yml` removed; their jobs moved into `ci.yml` and `release.yml`.
- `git-hash.txt` at the repository root - removed; the file is generated into `cuvis/` by `prebuild.py` and is no longer tracked.
- Unused imports and locals dropped from `cuvis.AcquisitionContext`, `cuvis.Export`, `cuvis.General`, `cuvis.Measurement`, `cuvis.Viewer`, `cuvis.Worker`, `cuvis.cuvis_aux`, `cuvis.doc` and the test modules; no public name was removed.
- `pyproject.toml` - the `typing_extensions` dependency removed; `ParamSpec` and `TypeAlias` are in `typing` from Python 3.10.
- `pyproject.toml` - the `Programming Language :: Python :: 3.9` classifier removed.

### Fixed

- `cuvis.General.wrapper_version` - raised `FileNotFoundError` on every pip-installed release since 3.3.1, because it read `git-hash.txt` from the directory above the package while no distribution ever shipped that file.
The hash is now read from `cuvis/git-hash.txt`, which the wheel and the sdist do ship, and a missing file degrades to reporting the version alone instead of raising.
- `tests/test_general.py` - `test_wrapper_version` asserted the literal `3.5.3`, so it had to be edited on every SDK bump and passed only because an editable install exposed the untracked root `git-hash.txt`.
It now compares against the installed distribution version.
- `cuvis.AcquisitionContext.register_ready_callback` - parameter `callback` was annotated `Callable[None, Awaitable[None]]`, which is not a valid `Callable` form; it is now `Callable[[], Awaitable[None]]`, matching the no-argument call the implementation makes.

## [3.5.3.2] - 2026-08-19

Expand Down
12 changes: 4 additions & 8 deletions cuvis/AcquisitionContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .cuvis_aux import SDKException, SessionData, ComponentInfo
from .cuvis_types import HardwareState, OperationMode

from typing import Callable, Awaitable, Union, Optional
from collections.abc import Callable, Awaitable
from .doc import copydoc

import cuvis.cuvis_types as internal
Expand All @@ -15,9 +15,7 @@


class AcquisitionContext(object):
def __init__(
self, base: Union[Calibration, SessionFile], *, simulate: bool = False
):
def __init__(self, base: Calibration | SessionFile, *, simulate: bool = False):
self._handle = None
self._simulate = simulate
self._state_poll_task = None
Expand Down Expand Up @@ -251,7 +249,7 @@ def _set_integration_time_factor_async(self, idref: int, val: float) -> Async:
return Async(cuvis_il.p_int_value(_pasync))

@copydoc(cuvis_il.cuvis_acq_cont_capture_async)
def capture(self, to_interal=False) -> Optional[AsyncMesu]:
def capture(self, to_interal=False) -> AsyncMesu | None:
if not to_interal:
_pasync = cuvis_il.new_p_int()
if cuvis_il.status_ok != cuvis_il.cuvis_acq_cont_capture_async(
Expand Down Expand Up @@ -538,9 +536,7 @@ def set_auto_exp_comp_async(self, val: float) -> Async:
raise SDKException()
return Async(cuvis_il.p_int_value(_pasync))

def register_ready_callback(
self, callback: Callable[None, Awaitable[None]]
) -> None:
def register_ready_callback(self, callback: Callable[[], Awaitable[None]]) -> None:
self.reset_ready_callback()

async def _internal_ready_loop():
Expand Down
11 changes: 5 additions & 6 deletions cuvis/Async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

import asyncio as a

from typing import Optional, Union
from datetime import timedelta


def _to_ms(value: Union[int, timedelta]) -> int:
def _to_ms(value: int | timedelta) -> int:
if isinstance(value, timedelta):
return int(value / timedelta(milliseconds=1))
elif isinstance(value, int):
Expand All @@ -25,8 +24,8 @@ def __init__(self, handle):
pass

def get(
self, timeout_ms: Union[int, timedelta]
) -> tuple[Optional[Measurement], AsyncResult]:
self, timeout_ms: int | timedelta
) -> tuple[Measurement | None, AsyncResult]:
_ptr = cuvis_il.new_p_int()
_pmesu = cuvis_il.new_p_int()
cuvis_il.p_int_assign(_ptr, self._handle)
Expand All @@ -45,7 +44,7 @@ def get(

# Python Magic Methods

def __await__(self) -> Optional[Measurement]:
def __await__(self) -> Measurement | None:
async def _wait_for_return():
_status_ptr = cuvis_il.new_p_cuvis_status_t()
while True:
Expand Down Expand Up @@ -80,7 +79,7 @@ class Async(object):
def __init__(self, handle):
self._handle = handle

def get(self, timeout_ms: Union[int, timedelta]) -> AsyncResult:
def get(self, timeout_ms: int | timedelta) -> AsyncResult:
_ptr = cuvis_il.new_p_int()
cuvis_il.p_int_assign(_ptr, self._handle)
res = cuvis_il.cuvis_async_call_get(_ptr, _to_ms(timeout_ms))
Expand Down
4 changes: 1 addition & 3 deletions cuvis/Calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@

import cuvis.cuvis_types as internal

from typing import Union


class Calibration(object):
def __init__(self, base: Union[Path, str, SessionFile]):
def __init__(self, base: Path | str | SessionFile):
self._handle = None
_ptr = cuvis_il.new_p_int()
if isinstance(base, SessionFile):
Expand Down
71 changes: 35 additions & 36 deletions cuvis/FileWriteSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import warnings

from dataclasses import dataclass, fields, field, InitVar
from typing import Optional


@dataclass
Expand Down Expand Up @@ -72,25 +71,25 @@ class GeneralExportSettings(object):
pan_sharpening: PanSharpeningSettings = field(default_factory=PanSharpeningSettings)
add_fullscale_pan: bool = False
permissive: bool = False
channel_selection: InitVar[Optional[str]] = None
spectra_multiplier: InitVar[Optional[int]] = None
pan_scale: InitVar[Optional[float]] = None
channel_selection: InitVar[str | None] = None
spectra_multiplier: InitVar[int | None] = None
pan_scale: InitVar[float | None] = None
pan_sharpening_interpolation_type: InitVar[
Optional[PanSharpeningInterpolationType]
PanSharpeningInterpolationType | None
] = None
pan_sharpening_algorithm: InitVar[Optional[PanSharpeningAlgorithm]] = None
pre_pan_sharpen_cube: InitVar[Optional[bool]] = None
add_pan: InitVar[Optional[bool]] = None
pan_sharpening_algorithm: InitVar[PanSharpeningAlgorithm | None] = None
pre_pan_sharpen_cube: InitVar[bool | None] = None
add_pan: InitVar[bool | None] = None

def __post_init__(
self,
channel_selection: Optional[str],
spectra_multiplier: Optional[int],
pan_scale: Optional[float],
pan_sharpening_interpolation_type: Optional[PanSharpeningInterpolationType],
pan_sharpening_algorithm: Optional[PanSharpeningAlgorithm],
pre_pan_sharpen_cube: Optional[bool],
add_pan: Optional[bool],
channel_selection: str | None,
spectra_multiplier: int | None,
pan_scale: float | None,
pan_sharpening_interpolation_type: PanSharpeningInterpolationType | None,
pan_sharpening_algorithm: PanSharpeningAlgorithm | None,
pre_pan_sharpen_cube: bool | None,
add_pan: bool | None,
) -> None:
updates = (
("channel_selection", channel_selection),
Expand Down Expand Up @@ -246,13 +245,13 @@ class ViewExportSettings(GeneralExportSettings):

def __post_init__(
self,
channel_selection: Optional[str],
spectra_multiplier: Optional[int],
pan_scale: Optional[float],
pan_sharpening_interpolation_type: Optional[PanSharpeningInterpolationType],
pan_sharpening_algorithm: Optional[PanSharpeningAlgorithm],
pre_pan_sharpen_cube: Optional[bool],
add_pan: Optional[bool],
channel_selection: str | None,
spectra_multiplier: int | None,
pan_scale: float | None,
pan_sharpening_interpolation_type: PanSharpeningInterpolationType | None,
pan_sharpening_algorithm: PanSharpeningAlgorithm | None,
pre_pan_sharpen_cube: bool | None,
add_pan: bool | None,
userplugin: str,
):
super().__post_init__(
Expand Down Expand Up @@ -422,26 +421,26 @@ class ViewerSettings:
pan_failback: bool = True
complete: bool = False
pan_sharpening: PanSharpeningSettings = field(default_factory=PanSharpeningSettings)
channel_selection: InitVar[Optional[str]] = None
spectra_multiplier: InitVar[Optional[int]] = None
pan_scale: InitVar[Optional[float]] = None
channel_selection: InitVar[str | None] = None
spectra_multiplier: InitVar[int | None] = None
pan_scale: InitVar[float | None] = None
pan_sharpening_interpolation_type: InitVar[
Optional[PanSharpeningInterpolationType]
PanSharpeningInterpolationType | None
] = None
pan_sharpening_algorithm: InitVar[Optional[PanSharpeningAlgorithm]] = None
pre_pan_sharpen_cube: InitVar[Optional[bool]] = None
add_pan: InitVar[Optional[bool]] = None
pan_sharpening_algorithm: InitVar[PanSharpeningAlgorithm | None] = None
pre_pan_sharpen_cube: InitVar[bool | None] = None
add_pan: InitVar[bool | None] = None
userplugin: InitVar[str] = None

def __post_init__(
self,
channel_selection: Optional[str],
spectra_multiplier: Optional[int],
pan_scale: Optional[float],
pan_sharpening_interpolation_type: Optional[PanSharpeningInterpolationType],
pan_sharpening_algorithm: Optional[PanSharpeningAlgorithm],
pre_pan_sharpen_cube: Optional[bool],
add_pan: Optional[bool],
channel_selection: str | None,
spectra_multiplier: int | None,
pan_scale: float | None,
pan_sharpening_interpolation_type: PanSharpeningInterpolationType | None,
pan_sharpening_algorithm: PanSharpeningAlgorithm | None,
pre_pan_sharpen_cube: bool | None,
add_pan: bool | None,
userplugin: str,
):
updates = (
Expand Down
10 changes: 4 additions & 6 deletions cuvis/General.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@

import cuvis.cuvis_types as internal

from typing import Union, Optional


def init(
settings_path: Union[str, Path, SdkSettings] = ".",
global_loglevel: Union[int, str] = logging.DEBUG,
logfile_name: Optional[str] = None,
settings_path: str | Path | SdkSettings = ".",
global_loglevel: int | str = logging.DEBUG,
logfile_name: str | None = None,
):
with ExitStack() as stack:
if isinstance(settings_path, SdkSettings):
Expand Down Expand Up @@ -62,7 +60,7 @@ def wrapper_version() -> str:
return f"{pip_version} {git_hash.read_text().splitlines()[0]}".strip()


def set_log_level(lvl: Union[int, str]):
def set_log_level(lvl: int | str):
if isinstance(lvl, str):
# also support string as input argument
lvl = internal.__strToLogLevel__[lvl]
Expand Down
7 changes: 3 additions & 4 deletions cuvis/Measurement.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Optional, Union
from .FileWriteSettings import SaveArgs
import datetime
from pathlib import Path
Expand All @@ -25,7 +24,7 @@ class Measurement(object):
measurement_flags: MeasurementFlags # read-only
path: str # read-only
comment: str
factory_calibration: Optional[datetime.datetime] # read-only
factory_calibration: datetime.datetime | None # read-only
assembly: str # read-only
integration_time: int # read-only
averages: int # read-only
Expand All @@ -37,7 +36,7 @@ class Measurement(object):
session_info: SessionData # read-only
frame_id: int # read-only

def __init__(self, base: Union[int, str, Path]):
def __init__(self, base: int | str | Path):
self._handle = None
self._session = None

Expand Down Expand Up @@ -178,7 +177,7 @@ def comment(self, comment: str) -> None:
pass

@property
def factory_calibration(self) -> Optional[datetime.datetime]:
def factory_calibration(self) -> datetime.datetime | None:
"""The calibration day, or None if the SDK reported a value datetime cannot hold.

Timezone-aware UTC, but only the day carries meaning. The SDK stores the day as
Expand Down
3 changes: 1 addition & 2 deletions cuvis/ProcessingContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@

import cuvis.cuvis_types as internal

from typing import Union

import dataclasses


class ProcessingContext(object):
def __init__(
self,
base: Union[Calibration, SessionFile, Measurement],
base: Calibration | SessionFile | Measurement,
load_references: bool = True,
):
self._handle = None
Expand Down
10 changes: 3 additions & 7 deletions cuvis/SessionFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@

import cuvis.cuvis_types as internal

from typing import Union, Optional


class SessionFile(object):
def __init__(self, base: Union[Path, str]):
def __init__(self, base: Path | str):
base = Path(base)
self._handle = None
self._pc = None
Expand All @@ -30,7 +28,7 @@ def __init__(self, base: Union[Path, str]):

def get_measurement(
self, frameNo: int = 0, itemtype: SessionItemType = SessionItemType.no_gaps
) -> Optional[Measurement]:
) -> Measurement | None:
_ptr = cuvis_il.new_p_int()
ret = cuvis_il.cuvis_session_file_get_mesu(
self._handle, frameNo, internal.__CuvisSessionItemType__[itemtype], _ptr
Expand All @@ -43,9 +41,7 @@ def get_measurement(
mesu._session = self
return mesu

def get_reference(
self, frameNo: int, reftype: ReferenceType
) -> Optional[Measurement]:
def get_reference(self, frameNo: int, reftype: ReferenceType) -> Measurement | None:
_ptr = cuvis_il.new_p_int()
ret = cuvis_il.cuvis_session_file_get_reference_mesu(
self._handle, frameNo, internal.__CuvisReferenceType__[reftype], _ptr
Expand Down
10 changes: 3 additions & 7 deletions cuvis/Viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

from .FileWriteSettings import ViewerSettings

from typing import Union


class Viewer(object):
def __init__(self, settings: Union[int, ViewerSettings]):
def __init__(self, settings: int | ViewerSettings):
self._handle = None
if isinstance(settings, int):
self._handle = settings
Expand All @@ -25,9 +23,7 @@ def __init__(self, settings: Union[int, ViewerSettings]):
)
pass

def _create_view_data(
self, new_handle: int
) -> Union[dict[str, ImageData], ImageData]:
def _create_view_data(self, new_handle: int) -> dict[str, ImageData] | ImageData:

_ptr = cuvis_il.new_p_int()
if cuvis_il.status_ok != cuvis_il.cuvis_view_get_data_count(new_handle, _ptr):
Expand All @@ -54,7 +50,7 @@ def _create_view_data(
else:
return view_array

def apply(self, mesu: Measurement) -> Union[dict[str, ImageData], ImageData]:
def apply(self, mesu: Measurement) -> dict[str, ImageData] | ImageData:
_ptr = cuvis_il.new_p_int()
if cuvis_il.status_ok != cuvis_il.cuvis_viewer_apply(
self._handle, mesu._handle, _ptr
Expand Down
2 changes: 1 addition & 1 deletion cuvis/Worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .doc import copydoc

from dataclasses import dataclass
from typing import Callable, Awaitable
from collections.abc import Callable, Awaitable


@dataclass
Expand Down
Loading