diff --git a/CHANGELOG.md b/CHANGELOG.md index 414bfb0..6ad2d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,12 +47,17 @@ 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 @@ -60,6 +65,7 @@ Pre-releases (`b*`, `rc*`) are not listed. 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 diff --git a/cuvis/AcquisitionContext.py b/cuvis/AcquisitionContext.py index ab136f6..0b05f89 100644 --- a/cuvis/AcquisitionContext.py +++ b/cuvis/AcquisitionContext.py @@ -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 @@ -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 @@ -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( @@ -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(): diff --git a/cuvis/Async.py b/cuvis/Async.py index bc8a1b3..42d4920 100644 --- a/cuvis/Async.py +++ b/cuvis/Async.py @@ -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): @@ -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) @@ -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: @@ -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)) diff --git a/cuvis/Calibration.py b/cuvis/Calibration.py index b84f694..1a08bfc 100644 --- a/cuvis/Calibration.py +++ b/cuvis/Calibration.py @@ -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): diff --git a/cuvis/FileWriteSettings.py b/cuvis/FileWriteSettings.py index bcd2bbd..f06d6ec 100644 --- a/cuvis/FileWriteSettings.py +++ b/cuvis/FileWriteSettings.py @@ -15,7 +15,6 @@ import warnings from dataclasses import dataclass, fields, field, InitVar -from typing import Optional @dataclass @@ -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), @@ -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__( @@ -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 = ( diff --git a/cuvis/General.py b/cuvis/General.py index 983250d..d30be88 100644 --- a/cuvis/General.py +++ b/cuvis/General.py @@ -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): @@ -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] diff --git a/cuvis/Measurement.py b/cuvis/Measurement.py index a7ff1b3..292c888 100644 --- a/cuvis/Measurement.py +++ b/cuvis/Measurement.py @@ -1,4 +1,3 @@ -from typing import Optional, Union from .FileWriteSettings import SaveArgs import datetime from pathlib import Path @@ -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 @@ -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 @@ -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 diff --git a/cuvis/ProcessingContext.py b/cuvis/ProcessingContext.py index ac8588d..68ccb3c 100644 --- a/cuvis/ProcessingContext.py +++ b/cuvis/ProcessingContext.py @@ -8,7 +8,6 @@ import cuvis.cuvis_types as internal -from typing import Union import dataclasses @@ -16,7 +15,7 @@ class ProcessingContext(object): def __init__( self, - base: Union[Calibration, SessionFile, Measurement], + base: Calibration | SessionFile | Measurement, load_references: bool = True, ): self._handle = None diff --git a/cuvis/SessionFile.py b/cuvis/SessionFile.py index e8a7ef1..3c3e901 100644 --- a/cuvis/SessionFile.py +++ b/cuvis/SessionFile.py @@ -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 @@ -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 @@ -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 diff --git a/cuvis/Viewer.py b/cuvis/Viewer.py index 124c508..e022166 100644 --- a/cuvis/Viewer.py +++ b/cuvis/Viewer.py @@ -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 @@ -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): @@ -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 diff --git a/cuvis/Worker.py b/cuvis/Worker.py index 1d18a7c..6039eff 100644 --- a/cuvis/Worker.py +++ b/cuvis/Worker.py @@ -12,7 +12,7 @@ from .doc import copydoc from dataclasses import dataclass -from typing import Callable, Awaitable +from collections.abc import Callable, Awaitable @dataclass diff --git a/cuvis/binding.py b/cuvis/binding.py index 8a89a1c..9b03bf1 100644 --- a/cuvis/binding.py +++ b/cuvis/binding.py @@ -35,7 +35,6 @@ """ from dataclasses import dataclass, field -from typing import FrozenSet, Tuple from ._cuvis_il import cuvis_il from .cuvis_aux import SDKException @@ -94,7 +93,7 @@ class BindingInfo: built_against: str library_version: str library_path: str - missing_symbols: Tuple[str, ...] = field(default_factory=tuple) + missing_symbols: tuple[str, ...] = field(default_factory=tuple) @property def is_complete(self) -> bool: @@ -152,7 +151,7 @@ def info() -> BindingInfo: ) -def missing_symbols() -> FrozenSet[str]: +def missing_symbols() -> frozenset[str]: """Functions the binding imports that the installed cuvis library does not export. :return: the set of C function names, empty when the SDK matches the binding and diff --git a/cuvis/cube_utils.py b/cuvis/cube_utils.py index 9ecf9f5..cfb5fef 100644 --- a/cuvis/cube_utils.py +++ b/cuvis/cube_utils.py @@ -1,4 +1,5 @@ -from typing import Optional, Sequence, Union +from typing import Union +from collections.abc import Sequence from ._cuvis_il import cuvis_il import numpy as np import operator @@ -131,7 +132,7 @@ def __init__(self, img_buf=None, dformat=None): ] @property - def shape(self) -> Optional[tuple]: + def shape(self) -> tuple | None: """Shape of :attr:`array`, ``(height, width, channels)``, or ``None`` when empty.""" return None if self.array is None else self.array.shape @@ -241,7 +242,7 @@ def __getitem__(self, key) -> Union[np.ndarray, tuple, "ImageData", np.generic]: return sliced_array, self._wavelengths_at(bands, len(sliced_array)) return sliced_array - def _band_indices(self, key) -> Optional[Sequence[int]]: + def _band_indices(self, key) -> Sequence[int] | None: """ The band indices a key selects, in the order they appear in the result. @@ -271,8 +272,8 @@ def _band_indices(self, key) -> Optional[Sequence[int]]: return None def _wavelengths_at( - self, bands: Optional[Sequence[int]], expected: int - ) -> Optional[list]: + self, bands: Sequence[int] | None, expected: int + ) -> list | None: """The wavelengths of the selected bands, or None if they do not line up.""" if self.wavelength is None or bands is None or len(bands) != expected: return None diff --git a/cuvis/doc.py b/cuvis/doc.py index dd852e1..003cc08 100644 --- a/cuvis/doc.py +++ b/cuvis/doc.py @@ -1,11 +1,7 @@ # taken from https://stackoverflow.com/questions/68901049/copying-the-docstring-of-function-onto-another-function-by-name -from typing import Callable, TypeVar, Any - -try: - from typing_extensions import ParamSpec, TypeAlias # type: ignore -except ImportError: - from typing import ParamSpec, TypeAlias +from typing import Any, ParamSpec, TypeAlias, TypeVar +from collections.abc import Callable T = TypeVar("T") diff --git a/cuvis/sdk_settings.py b/cuvis/sdk_settings.py index a02ba98..bbc9090 100644 --- a/cuvis/sdk_settings.py +++ b/cuvis/sdk_settings.py @@ -3,7 +3,7 @@ from collections.abc import Iterator, MutableMapping from enum import Enum from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from xml.etree import ElementTree as ET SETTINGS_NAMESPACE = "http://cubert-gmbh.de/core/settings.xsd" @@ -14,7 +14,7 @@ _PROPERTY_TAG = "{{{}}}property".format(SETTINGS_NAMESPACE) -def _resolve_source(source: Union[str, Path]) -> Path: +def _resolve_source(source: str | Path) -> Path: source = Path(source) if not source.is_dir(): return source @@ -132,7 +132,7 @@ class SdkSettings(MutableMapping): id is empty or contains whitespace. """ - def __init__(self, source: Optional[Union[str, Path]] = None, /, **kwargs): + def __init__(self, source: str | Path | None = None, /, **kwargs): self._tmpdir = None ET.register_namespace("", SETTINGS_NAMESPACE) @@ -198,7 +198,7 @@ def xml_str(self) -> str: self._tree.write(buf, xml_declaration=True, encoding="utf-8") return buf.getvalue().decode("utf-8") - def save(self, path: Union[str, Path]) -> None: + def save(self, path: str | Path) -> None: path = Path(path) if path.is_dir(): path = path / SETTINGS_FILENAME diff --git a/pyproject.toml b/pyproject.toml index b438a4d..2599ca3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "cuvis" version = "3.5.3.2" description = "CUVIS Python SDK." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = "Apache-2.0" authors = [ { name = "Cubert GmbH, Ulm, Germany", email = "sdk@cubert-gmbh.com" } @@ -15,14 +15,12 @@ authors = [ dependencies = [ "cuvis-il>=3.5.0,<3.6.0", - "typing_extensions; python_version<'3.10'", # Only for Python 3.9 "setuptools; python_version>='3.12'" # Only for Python 3.12+ ] classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -56,7 +54,7 @@ dev = [ ] [tool.ruff] -target-version = "py39" +target-version = "py310" [tool.ruff.lint] select = ["E4", "E7", "E9", "F"]