Skip to content

Commit c412930

Browse files
Drop support for Python 3.8, 3.9 (#128)
1 parent bbb5c73 commit c412930

File tree

13 files changed

+128
-149
lines changed

13 files changed

+128
-149
lines changed

.github/pages/make_switcher.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,27 @@
33
from argparse import ArgumentParser
44
from pathlib import Path
55
from subprocess import CalledProcessError, check_output
6-
from typing import List, Optional
76

87

9-
def report_output(stdout: bytes, label: str) -> List[str]:
8+
def report_output(stdout: bytes, label: str) -> list[str]:
109
ret = stdout.decode().strip().split("\n")
1110
print(f"{label}: {ret}")
1211
return ret
1312

1413

15-
def get_branch_contents(ref: str) -> List[str]:
14+
def get_branch_contents(ref: str) -> list[str]:
1615
"""Get the list of directories in a branch."""
1716
stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref])
1817
return report_output(stdout, "Branch contents")
1918

2019

21-
def get_sorted_tags_list() -> List[str]:
20+
def get_sorted_tags_list() -> list[str]:
2221
"""Get a list of sorted tags in descending order from the repository."""
2322
stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"])
2423
return report_output(stdout, "Tags list")
2524

2625

27-
def get_versions(ref: str, add: Optional[str]) -> List[str]:
26+
def get_versions(ref: str, add: str | None) -> list[str]:
2827
"""Generate the file containing the list of all GitHub Pages builds."""
2928
# Get the directories (i.e. builds) from the GitHub Pages branch
3029
try:
@@ -41,7 +40,7 @@ def get_versions(ref: str, add: Optional[str]) -> List[str]:
4140
tags = get_sorted_tags_list()
4241

4342
# Make the sorted versions list from main branches and tags
44-
versions: List[str] = []
43+
versions: list[str] = []
4544
for version in ["master", "main"] + tags:
4645
if version in builds:
4746
versions.append(version)

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
strategy:
2222
matrix:
2323
runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest
24-
python-version: ["3.8", "3.9", "3.10", "3.11"]
24+
python-version: ["3.10", "3.11"]
2525
include:
2626
# Include one that runs in the dev environment
2727
- runs-on: "ubuntu-latest"

docs/how-to/iterate-a-spec.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ frame. You can get these by using the `Spec.midpoints()` method to produce a
1818
>>> for d in spec.midpoints():
1919
... print(d)
2020
...
21-
{'x': 1.0}
22-
{'x': 1.5}
23-
{'x': 2.0}
21+
{'x': np.float64(1.0)}
22+
{'x': np.float64(1.5)}
23+
{'x': np.float64(2.0)}
2424

2525
This is simple, but not particularly performant, as the numpy arrays of
2626
points are unpacked point by point into point dictionaries

pyproject.toml

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,19 @@ name = "scanspec"
77
classifiers = [
88
"Development Status :: 3 - Alpha",
99
"License :: OSI Approved :: Apache Software License",
10-
"Programming Language :: Python :: 3.7",
11-
"Programming Language :: Python :: 3.8",
12-
"Programming Language :: Python :: 3.9",
1310
"Programming Language :: Python :: 3.10",
1411
"Programming Language :: Python :: 3.11",
1512
]
1613
description = "Specify step and flyscan paths in a serializable, efficient and Pythonic way"
17-
dependencies = [
18-
"numpy>=1.19.3",
19-
"click==8.1.3",
20-
"pydantic<2.0",
21-
"typing_extensions",
22-
]
14+
dependencies = ["numpy>=2", "click>=8.1", "pydantic<2.0", "httpx==0.26.0"]
2315
dynamic = ["version"]
2416
license.file = "LICENSE"
2517
readme = "README.md"
26-
requires-python = ">=3.7"
18+
requires-python = ">=3.10"
2719

2820
[project.optional-dependencies]
2921
# Plotting
30-
plotting = [
31-
# make sure a python 3.9 compatible scipy and matplotlib are selected
32-
"scipy>=1.5.4",
33-
"matplotlib>=3.2.2",
34-
]
22+
plotting = ["scipy", "matplotlib"]
3523
# REST service support
3624
service = ["fastapi==0.99", "uvicorn"]
3725
# For development tests/docs
@@ -131,8 +119,6 @@ extend-select = [
131119
"I", # isort - https://docs.astral.sh/ruff/rules/#isort-i
132120
"UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
133121
]
134-
# We use pydantic, so don't upgrade to py3.10 syntax yet
135-
pyupgrade.keep-runtime-typing = true
136122
ignore = [
137123
"B008", # We use function calls in service arguments
138124
]

src/scanspec/core.py

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
from __future__ import annotations
22

3+
from collections.abc import Callable, Iterable, Iterator, Sequence
34
from dataclasses import field
4-
from typing import (
5-
Any,
6-
Callable,
7-
Dict,
8-
Generic,
9-
Iterable,
10-
Iterator,
11-
List,
12-
Optional,
13-
Sequence,
14-
Type,
15-
TypeVar,
16-
Union,
17-
)
5+
from typing import Any, Generic, Literal, TypeVar, Union
186

197
import numpy as np
208
from pydantic import BaseConfig, Extra, Field, ValidationError, create_model
219
from pydantic.error_wrappers import ErrorWrapper
22-
from typing_extensions import Literal
2310

2411
__all__ = [
2512
"if_instance_do",
@@ -43,11 +30,11 @@ class StrictConfig(BaseConfig):
4330

4431

4532
def discriminated_union_of_subclasses(
46-
super_cls: Optional[Type] = None,
33+
super_cls: type | None = None,
4734
*,
4835
discriminator: str = "type",
49-
config: Optional[Type[BaseConfig]] = None,
50-
) -> Union[Type, Callable[[Type], Type]]:
36+
config: type[BaseConfig] | None = None,
37+
) -> type | Callable[[type], type]:
5138
"""Add all subclasses of super_cls to a discriminated union.
5239
5340
For all subclasses of super_cls, add a discriminator field to identify
@@ -114,7 +101,7 @@ def calculate(self) -> int:
114101
subclasses. Defaults to None.
115102
116103
Returns:
117-
Union[Type, Callable[[Type], Type]]: A decorator that adds the necessary
104+
Type | Callable[[Type], Type]: A decorator that adds the necessary
118105
functionality to a class.
119106
"""
120107

@@ -130,12 +117,12 @@ def wrap(cls):
130117

131118

132119
def _discriminated_union_of_subclasses(
133-
super_cls: Type,
120+
super_cls: type,
134121
discriminator: str,
135-
config: Optional[Type[BaseConfig]] = None,
136-
) -> Union[Type, Callable[[Type], Type]]:
137-
super_cls._ref_classes = set()
138-
super_cls._model = None
122+
config: type[BaseConfig] | None = None,
123+
) -> type | Callable[[type], type]:
124+
super_cls._ref_classes = set() # type: ignore
125+
super_cls._model = None # type: ignore
139126

140127
def __init_subclass__(cls) -> None:
141128
# Keep track of inherting classes in super class
@@ -157,7 +144,7 @@ def __validate__(cls, v: Any) -> Any:
157144
# needs to be done once, after all subclasses have been
158145
# declared
159146
if cls._model is None:
160-
root = Union[tuple(cls._ref_classes)] # type: ignore
147+
root = Union[tuple(cls._ref_classes)] # type: ignore # noqa
161148
cls._model = create_model(
162149
super_cls.__name__,
163150
__root__=(root, Field(..., discriminator=discriminator)),
@@ -185,7 +172,7 @@ def __validate__(cls, v: Any) -> Any:
185172
return super_cls
186173

187174

188-
def if_instance_do(x: Any, cls: Type, func: Callable):
175+
def if_instance_do(x: Any, cls: type, func: Callable):
189176
"""If x is of type cls then return func(x), otherwise return NotImplemented.
190177
191178
Used as a helper when implementing operator overloading.
@@ -201,7 +188,7 @@ def if_instance_do(x: Any, cls: Type, func: Callable):
201188

202189
#: Map of axes to float ndarray of points
203190
#: E.g. {xmotor: array([0, 1, 2]), ymotor: array([2, 2, 2])}
204-
AxesPoints = Dict[Axis, np.ndarray]
191+
AxesPoints = dict[Axis, np.ndarray]
205192

206193

207194
class Frames(Generic[Axis]):
@@ -234,9 +221,9 @@ class Frames(Generic[Axis]):
234221
def __init__(
235222
self,
236223
midpoints: AxesPoints[Axis],
237-
lower: Optional[AxesPoints[Axis]] = None,
238-
upper: Optional[AxesPoints[Axis]] = None,
239-
gap: Optional[np.ndarray] = None,
224+
lower: AxesPoints[Axis] | None = None,
225+
upper: AxesPoints[Axis] | None = None,
226+
gap: np.ndarray | None = None,
240227
):
241228
#: The midpoints of scan frames for each axis
242229
self.midpoints = midpoints
@@ -253,7 +240,9 @@ def __init__(
253240
# We have a gap if upper[i] != lower[i+1] for any axes
254241
axes_gap = [
255242
np.roll(upper, 1) != lower
256-
for upper, lower in zip(self.upper.values(), self.lower.values())
243+
for upper, lower in zip(
244+
self.upper.values(), self.lower.values(), strict=False
245+
)
257246
]
258247
self.gap = np.logical_or.reduce(axes_gap)
259248
# Check all axes and ordering are the same
@@ -270,7 +259,7 @@ def __init__(
270259
lengths.add(len(self.gap))
271260
assert len(lengths) <= 1, f"Mismatching lengths {list(lengths)}"
272261

273-
def axes(self) -> List[Axis]:
262+
def axes(self) -> list[Axis]:
274263
"""The axes which will move during the scan.
275264
276265
These will be present in `midpoints`, `lower` and `upper`.
@@ -300,7 +289,7 @@ def extract_dict(ds: Iterable[AxesPoints[Axis]]) -> AxesPoints[Axis]:
300289
return {k: v[dim_indices] for k, v in d.items()}
301290
return {}
302291

303-
def extract_gap(gaps: Iterable[np.ndarray]) -> Optional[np.ndarray]:
292+
def extract_gap(gaps: Iterable[np.ndarray]) -> np.ndarray | None:
304293
for gap in gaps:
305294
if not calculate_gap:
306295
return gap[dim_indices]
@@ -371,7 +360,7 @@ def zip_gap(gaps: Sequence[np.ndarray]) -> np.ndarray:
371360
def _merge_frames(
372361
*stack: Frames[Axis],
373362
dict_merge=Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore
374-
gap_merge=Callable[[Sequence[np.ndarray]], Optional[np.ndarray]],
363+
gap_merge=Callable[[Sequence[np.ndarray]], np.ndarray | None],
375364
) -> Frames[Axis]:
376365
types = {type(fs) for fs in stack}
377366
assert len(types) == 1, f"Mismatching types for {stack}"
@@ -397,9 +386,9 @@ class SnakedFrames(Frames[Axis]):
397386
def __init__(
398387
self,
399388
midpoints: AxesPoints[Axis],
400-
lower: Optional[AxesPoints[Axis]] = None,
401-
upper: Optional[AxesPoints[Axis]] = None,
402-
gap: Optional[np.ndarray] = None,
389+
lower: AxesPoints[Axis] | None = None,
390+
upper: AxesPoints[Axis] | None = None,
391+
gap: np.ndarray | None = None,
403392
):
404393
super().__init__(midpoints, lower=lower, upper=upper, gap=gap)
405394
# Override first element of gap to be True, as subsequent runs
@@ -431,7 +420,7 @@ def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]:
431420
length = len(self)
432421
backwards = (indices // length) % 2
433422
snake_indices = np.where(backwards, (length - 1) - indices, indices) % length
434-
cls: Type[Frames[Any]]
423+
cls: type[Frames[Any]]
435424
if not calculate_gap:
436425
cls = Frames
437426
gap = self.gap[np.where(backwards, length - indices, indices) % length]
@@ -464,7 +453,7 @@ def gap_between_frames(frames1: Frames[Axis], frames2: Frames[Axis]) -> bool:
464453
return any(frames1.upper[a][-1] != frames2.lower[a][0] for a in frames1.axes())
465454

466455

467-
def squash_frames(stack: List[Frames[Axis]], check_path_changes=True) -> Frames[Axis]:
456+
def squash_frames(stack: list[Frames[Axis]], check_path_changes=True) -> Frames[Axis]:
468457
"""Squash a stack of nested Frames into a single one.
469458
470459
Args:
@@ -530,7 +519,7 @@ class Path(Generic[Axis]):
530519
"""
531520

532521
def __init__(
533-
self, stack: List[Frames[Axis]], start: int = 0, num: Optional[int] = None
522+
self, stack: list[Frames[Axis]], start: int = 0, num: int | None = None
534523
):
535524
#: The Frames stack describing the scan, from slowest to fastest moving
536525
self.stack = stack
@@ -544,7 +533,7 @@ def __init__(
544533
if num is not None and start + num < self.end_index:
545534
self.end_index = start + num
546535

547-
def consume(self, num: Optional[int] = None) -> Frames[Axis]:
536+
def consume(self, num: int | None = None) -> Frames[Axis]:
548537
"""Consume at most num frames from the Path and return as a Frames object.
549538
550539
>>> fx = SnakedFrames({"x": np.array([1, 2])})
@@ -613,18 +602,18 @@ class Midpoints(Generic[Axis]):
613602
>>> fy = Frames({"y": np.array([3, 4])})
614603
>>> mp = Midpoints([fy, fx])
615604
>>> for p in mp: print(p)
616-
{'y': 3, 'x': 1}
617-
{'y': 3, 'x': 2}
618-
{'y': 4, 'x': 2}
619-
{'y': 4, 'x': 1}
605+
{'y': np.int64(3), 'x': np.int64(1)}
606+
{'y': np.int64(3), 'x': np.int64(2)}
607+
{'y': np.int64(4), 'x': np.int64(2)}
608+
{'y': np.int64(4), 'x': np.int64(1)}
620609
"""
621610

622-
def __init__(self, stack: List[Frames[Axis]]):
611+
def __init__(self, stack: list[Frames[Axis]]):
623612
#: The stack of Frames describing the scan, from slowest to fastest moving
624613
self.stack = stack
625614

626615
@property
627-
def axes(self) -> List[Axis]:
616+
def axes(self) -> list[Axis]:
628617
"""The axes that will be present in each points dictionary."""
629618
axes = []
630619
for frames in self.stack:
@@ -635,7 +624,7 @@ def __len__(self) -> int:
635624
"""The number of dictionaries that will be produced if iterated over."""
636625
return int(np.prod([len(frames) for frames in self.stack]))
637626

638-
def __iter__(self) -> Iterator[Dict[Axis, float]]:
627+
def __iter__(self) -> Iterator[dict[Axis, float]]:
639628
"""Yield {axis: midpoint} for each frame in the scan."""
640629
path = Path(self.stack)
641630
while len(path):

0 commit comments

Comments
 (0)