Skip to content

Commit 7724185

Browse files
authored
build: drop python 3.9, add 3.14 (#249)
* drop 39, add 314 * update syntax * change windows pin * fix pinning
1 parent 40a6ad8 commit 7724185

22 files changed

Lines changed: 125 additions & 114 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ jobs:
2626
fail-fast: false
2727
matrix:
2828
os: [windows-latest, macos-latest, ubuntu-latest]
29-
python-version: ["3.9", "3.11", "3.13"]
29+
python-version: ["3.10", "3.12", "3.14"]
3030
include:
3131
- os: ubuntu-latest
32-
python-version: "3.11"
32+
python-version: "3.10"
33+
resolution: "lowest-direct"
34+
- os: windows-latest
35+
python-version: "3.13"
3336
resolution: "lowest-direct"
3437
- os: ubuntu-latest
3538
python-version: "3.13"

pyproject.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ name = "useq-schema"
99
description = "Schema for multi-dimensional microscopy experiments"
1010
readme = "README.md"
1111
keywords = ["microscopy", "schema"]
12-
requires-python = ">=3.9"
12+
requires-python = ">=3.10"
1313
license = { text = "BSD 3-Clause License" }
1414
authors = [
1515
{ email = "talley.lambert@gmail.com", name = "Talley Lambert" },
@@ -23,11 +23,11 @@ classifiers = [
2323
"Operating System :: OS Independent",
2424
"Programming Language :: Python",
2525
"Programming Language :: Python :: 3 :: Only",
26-
"Programming Language :: Python :: 3.9",
2726
"Programming Language :: Python :: 3.10",
2827
"Programming Language :: Python :: 3.11",
2928
"Programming Language :: Python :: 3.12",
3029
"Programming Language :: Python :: 3.13",
30+
"Programming Language :: Python :: 3.14",
3131
"Topic :: Scientific/Engineering",
3232
"Topic :: Scientific/Engineering :: Medical Science Apps.",
3333
"Topic :: Scientific/Engineering :: Image Processing",
@@ -37,24 +37,36 @@ classifiers = [
3737
]
3838
dynamic = ["version"]
3939
dependencies = [
40+
"pydantic >=2.12; python_version >= '3.14'",
41+
"pydantic >=2.8; python_version >= '3.13'",
4042
"pydantic >=2.6",
43+
"numpy >=2.3.2; python_version >= '3.14'",
4144
"numpy >=2.1.0; python_version >= '3.13'",
4245
"numpy >=1.26.0; python_version >= '3.12'",
4346
"numpy >=1.25.2",
4447
"typing-extensions >=4",
48+
"shapely>=2.1.2; python_version >= '3.14'",
4549
"shapely>=2.0.7",
4650
]
4751

4852
# extras
4953
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
5054
[project.optional-dependencies]
5155
yaml = ["PyYAML >=5.0"]
52-
plot = ["matplotlib >=3.7"]
56+
plot = [
57+
"matplotlib >=3.10.5; python_version >= '3.14'",
58+
"matplotlib >=3.9.2; python_version >= '3.13'",
59+
"matplotlib >=3.7.3; python_version >= '3.12'",
60+
"matplotlib >=3.7",
61+
]
5362

5463
[dependency-groups]
5564
test = ["psygnal>=0.13.0", "pytest>=8.0", "pytest-cov>=6.1.1", "pyyaml>=6.0.2"]
5665
dev = [
5766
{ include-group = "test" },
67+
"matplotlib >=3.10.5; python_version >= '3.14'",
68+
"matplotlib >=3.9.2; python_version >= '3.13'",
69+
"matplotlib >=3.7.3; python_version >= '3.12'",
5870
"matplotlib >=3.7",
5971
"ipython>=8.18.1",
6072
"mypy>=1.15.0",
@@ -89,7 +101,7 @@ packages = ["src/useq"]
89101
# https://beta.ruff.rs/docs/rules/
90102
[tool.ruff]
91103
line-length = 88
92-
target-version = "py39"
104+
target-version = "py310"
93105
src = ["src", "tests"]
94106
fix = true
95107
unsafe-fixes = true

src/useq/_actions.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from typing import Optional, Union
1+
from typing import Literal
22

33
from pydantic import ConfigDict, Field, TypeAdapter, field_validator
44
from pydantic_core import PydanticSerializationError
5-
from typing_extensions import Literal
65

76
from useq._base_model import FrozenModel
87

@@ -63,8 +62,8 @@ class HardwareAutofocus(Action):
6362
"""
6463

6564
type: Literal["hardware_autofocus"] = "hardware_autofocus" # pyright: ignore[reportIncompatibleVariableOverride]
66-
autofocus_device_name: Optional[str] = None
67-
autofocus_motor_offset: Optional[float] = None
65+
autofocus_device_name: str | None = None
66+
autofocus_motor_offset: float | None = None
6867
max_retries: int = 3
6968

7069

@@ -106,4 +105,4 @@ def _ensure_serializable(cls, data: dict) -> dict:
106105
return data
107106

108107

109-
AnyAction = Union[HardwareAutofocus, AcquireImage, CustomAction]
108+
AnyAction = HardwareAutofocus | AcquireImage | CustomAction

src/useq/_base_model.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
TYPE_CHECKING,
77
Any,
88
ClassVar,
9-
Optional,
109
TypeVar,
11-
Union,
1210
)
1311

1412
import numpy as np
@@ -89,7 +87,7 @@ class MutableModel(_ReplaceableModel):
8987

9088
class UseqModel(FrozenModel):
9189
@classmethod
92-
def from_file(cls: type[_Y], path: Union[str, Path]) -> _Y:
90+
def from_file(cls: type[_Y], path: str | Path) -> _Y:
9391
"""Return an instance of this class from a file. Supports JSON and YAML."""
9492
path = Path(path)
9593
if path.suffix in {".yaml", ".yml"}:
@@ -106,14 +104,14 @@ def from_file(cls: type[_Y], path: Union[str, Path]) -> _Y:
106104
def yaml(
107105
self,
108106
*,
109-
include: Optional[Union[set, dict]] = None,
110-
exclude: Optional[Union[set, dict]] = None,
107+
include: set | dict | None = None,
108+
exclude: set | dict | None = None,
111109
by_alias: bool = False,
112110
exclude_unset: bool = True, # pydantic has False by default
113111
exclude_defaults: bool = False,
114112
exclude_none: bool = False,
115-
stream: Optional[IO[str]] = None,
116-
) -> Optional[str]:
113+
stream: IO[str] | None = None,
114+
) -> str | None:
117115
"""Generate a YAML representation of the model.
118116
119117
Returns

src/useq/_channel.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Optional
1+
from typing import Any
22

33
from pydantic import Field, model_validator
44

@@ -33,11 +33,11 @@ class Channel(FrozenModel):
3333

3434
config: str
3535
group: str = "Channel"
36-
exposure: Optional[float] = Field(None, gt=0.0)
36+
exposure: float | None = Field(None, gt=0.0)
3737
do_stack: bool = True
3838
z_offset: float = 0.0
3939
acquire_every: int = Field(default=1, gt=0) # acquire every n frames
40-
camera: Optional[str] = None
40+
camera: str | None = None
4141

4242
@model_validator(mode="before")
4343
@classmethod

src/useq/_grid.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,20 @@
33
import contextlib
44
import math
55
import warnings
6-
from collections.abc import Iterable, Iterator, Sequence
6+
from collections.abc import Callable, Iterable, Iterator, Sequence
77
from enum import Enum
88
from typing import (
99
TYPE_CHECKING,
1010
Annotated,
1111
Any,
12-
Callable,
13-
Optional,
14-
Union,
12+
TypeAlias,
1513
)
1614

1715
import numpy as np
1816
from annotated_types import Ge, Gt
1917
from pydantic import Field, PrivateAttr, field_validator, model_validator
2018
from shapely import Polygon, box, prepared
21-
from typing_extensions import Self, TypeAlias
19+
from typing_extensions import Self
2220

2321
from useq._point_visiting import OrderMode, TraversalOrder
2422
from useq._position import (
@@ -434,14 +432,14 @@ class GridFromPolygon(_GridPlan[AbsolutePosition]):
434432
),
435433
]
436434
convex_hull: Annotated[
437-
Optional[bool],
435+
bool | None,
438436
Field(
439437
False,
440438
description="If True, the convex hull of the polygon will be used.",
441439
),
442440
] = False
443441
offset: Annotated[
444-
Optional[float],
442+
float | None,
445443
Field(
446444
None,
447445
frozen=True,
@@ -702,10 +700,10 @@ class RandomPoints(_MultiPointPlan[RelativePosition]):
702700
max_width: Annotated[float, Gt(0)] = 1
703701
max_height: Annotated[float, Gt(0)] = 1
704702
shape: Shape = Shape.ELLIPSE
705-
random_seed: Optional[int] = None
703+
random_seed: int | None = None
706704
allow_overlap: bool = True
707-
order: Optional[TraversalOrder] = TraversalOrder.TWO_OPT
708-
start_at: Union[RelativePosition, Annotated[int, Ge(0)]] = 0
705+
order: TraversalOrder | None = TraversalOrder.TWO_OPT
706+
start_at: RelativePosition | Annotated[int, Ge(0)] = 0
709707

710708
@model_validator(mode="after")
711709
def _validate_startat(self) -> Self:
@@ -818,8 +816,8 @@ def _random_points_in_rectangle(
818816

819817

820818
# all of these support __iter__() -> Iterator[PositionBase] and num_positions() -> int
821-
RelativeMultiPointPlan = Union[
822-
GridRowsColumns, GridWidthHeight, RandomPoints, RelativePosition
823-
]
824-
AbsoluteMultiPointPlan = Union[GridFromEdges, GridFromPolygon]
825-
MultiPointPlan = Union[AbsoluteMultiPointPlan, RelativeMultiPointPlan]
819+
RelativeMultiPointPlan = (
820+
GridRowsColumns | GridWidthHeight | RandomPoints | RelativePosition
821+
)
822+
AbsoluteMultiPointPlan = GridFromEdges | GridFromPolygon
823+
MultiPointPlan = AbsoluteMultiPointPlan | RelativeMultiPointPlan

src/useq/_hardware_autofocus.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Optional
1+
from typing import Any
22

33
from pydantic import PrivateAttr
44

@@ -22,8 +22,8 @@ class AutoFocusPlan(FrozenModel):
2222
If None, the autofocus motor should not be moved.
2323
"""
2424

25-
autofocus_device_name: Optional[str] = None
26-
autofocus_motor_offset: Optional[float] = None
25+
autofocus_device_name: str | None = None
26+
autofocus_motor_offset: float | None = None
2727

2828
def as_action(self) -> HardwareAutofocus:
2929
"""Return a [`useq.HardwareAutofocus`][] for this autofocus plan."""
@@ -32,7 +32,7 @@ def as_action(self) -> HardwareAutofocus:
3232
autofocus_motor_offset=self.autofocus_motor_offset,
3333
)
3434

35-
def event(self, event: MDAEvent) -> Optional[MDAEvent]:
35+
def event(self, event: MDAEvent) -> MDAEvent | None:
3636
"""Return an autofocus [`useq.MDAEvent`][] if autofocus should be performed.
3737
3838
The z position of the new [`useq.MDAEvent`][] is also updated if a relative

src/useq/_iter_sequence.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ def _iter_sequence(
151151
if not item: # the case with no events
152152
continue # pragma: no cover
153153
# get axes objects for this event
154-
index, time, position, grid, channel, z_pos = _parse_axes(zip(order, item))
154+
index, time, position, grid, channel, z_pos = _parse_axes(
155+
zip(order, item, strict=False)
156+
)
155157

156158
# skip if necessary
157159
if _should_skip(position, channel, index, sequence.z_plan):

src/useq/_mda_event.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from useq._mda_sequence import MDASequence
3030

31-
ReprArgs = Sequence[tuple[Optional[str], Any]]
31+
ReprArgs = Sequence[tuple[str | None, Any]]
3232

3333

3434
class Channel(UseqModel):
@@ -83,8 +83,8 @@ class SLMImage(UseqModel):
8383
"""
8484

8585
data: Any = Field(..., repr=False)
86-
device: Optional[str] = None
87-
exposure: Optional[float] = Field(default=None, gt=0.0)
86+
device: str | None = None
87+
exposure: float | None = Field(default=None, gt=0.0)
8888

8989
@model_validator(mode="before")
9090
def _cast_data(cls, v: Any) -> Any:
@@ -112,8 +112,8 @@ class Kwargs(TypedDict, total=False):
112112
"""Type for the kwargs passed to the SLM image."""
113113

114114
data: npt.ArrayLike
115-
device: Optional[str]
116-
exposure: Optional[float]
115+
device: str | None
116+
exposure: float | None
117117

118118

119119
class PropertyTuple(NamedTuple):
@@ -134,7 +134,7 @@ class PropertyTuple(NamedTuple):
134134
property_value: Any
135135

136136

137-
def _float_or_none(v: Any) -> Optional[float]:
137+
def _float_or_none(v: Any) -> float | None:
138138
return float(v) if v is not None else v
139139

140140

@@ -238,16 +238,16 @@ class MDAEvent(UseqModel):
238238
"""
239239

240240
index: ReadOnlyDict = Field(default_factory=ReadOnlyDict)
241-
channel: Optional[Channel] = None
242-
exposure: Optional[float] = Field(default=None, gt=0.0)
243-
min_start_time: Optional[float] = None # time in sec
244-
pos_name: Optional[str] = None
245-
x_pos: Optional[float] = None
246-
y_pos: Optional[float] = None
247-
z_pos: Optional[float] = None
248-
slm_image: Optional[SLMImage] = None
241+
channel: Channel | None = None
242+
exposure: float | None = Field(default=None, gt=0.0)
243+
min_start_time: float | None = None # time in sec
244+
pos_name: str | None = None
245+
x_pos: float | None = None
246+
y_pos: float | None = None
247+
z_pos: float | None = None
248+
slm_image: SLMImage | None = None
249249
sequence: Optional["MDASequence"] = Field(default=None, repr=False)
250-
properties: Optional[list[PropertyTuple]] = None
250+
properties: list[PropertyTuple] | None = None
251251
metadata: dict[str, Any] = Field(default_factory=dict)
252252
action: AnyAction = Field(default_factory=AcquireImage, discriminator="type")
253253
keep_shutter_open: bool = False

0 commit comments

Comments
 (0)