From 01be7305e58bd082ada39ccfb229f70cad39e5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 21 Nov 2024 12:45:47 +0100 Subject: [PATCH] DEP: drop support for Python 3.9 --- .github/workflows/ci.yml | 4 +- nonos/_readers/binary.py | 16 ++++---- nonos/_types.py | 29 +++----------- nonos/api/_angle_parsing.py | 14 +++---- nonos/api/analysis.py | 76 ++++++++++++++++++------------------- nonos/api/satellite.py | 26 ++++++------- nonos/loaders.py | 13 ++----- nonos/logging.py | 3 +- nonos/main.py | 17 +++------ nonos/parsing.py | 16 ++++---- pyproject.toml | 13 +++---- 11 files changed, 96 insertions(+), 131 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e45ff7b6..75863957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,6 @@ jobs: os: - ubuntu-latest python-version: - - '3.9' - '3.10' - '3.11' - '3.12' @@ -30,7 +29,7 @@ jobs: venv-loc: [bin] include: - os: ubuntu-20.04 - python-version: '3.9' + python-version: '3.10' venv-loc: bin deps: minimal install-args: --resolution=lowest-direct @@ -97,7 +96,6 @@ jobs: strategy: matrix: python-version: - - '3.9' - '3.10' - '3.11' - '3.12' diff --git a/nonos/_readers/binary.py b/nonos/_readers/binary.py index 9ea3258c..bfe94147 100644 --- a/nonos/_readers/binary.py +++ b/nonos/_readers/binary.py @@ -9,7 +9,7 @@ import sys from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional, Union, final +from typing import final import numpy as np @@ -31,12 +31,12 @@ class VTKReader(ReaderMixin): @staticmethod def parse_output_number_and_filename( - file_or_number: Union[PathT, int], + file_or_number: PathT | int, *, directory: PathT, prefix: str, # noqa: ARG004 ) -> tuple[int, Path]: - if isinstance(file_or_number, (str, Path)): + if isinstance(file_or_number, str | Path): file = Path(file_or_number) if (m := re.search(r"\d+", file.name)) is None: raise ValueError( @@ -157,7 +157,7 @@ def read(file, /, **meta) -> BinData: n2 = int(slist[2]) n3 = int(slist[3]) - z: Union[np.ndarray, np.memmap] + z: np.ndarray | np.memmap if V["geometry"] is Geometry.CARTESIAN: s = fid.readline() # X_COORDINATES NX float @@ -476,7 +476,7 @@ def read(file, /, **meta) -> BinData: class _FargoReader(ReaderMixin, ABC): @staticmethod def parse_output_number_and_filename( - file_or_number: Union[PathT, int], + file_or_number: PathT | int, *, directory: PathT, prefix: str, # noqa ARG004 @@ -536,7 +536,7 @@ def read( output_number, directory = _FargoReader._get_output_number_and_dir_from(file) default_fluid = "gas" - fluid_option: Optional[str] = meta.get("fluid", default_fluid) + fluid_option: str | None = meta.get("fluid", default_fluid) if fluid_option is None: fluid = default_fluid else: @@ -655,13 +655,13 @@ class NPYReader(ReaderMixin): @staticmethod def parse_output_number_and_filename( - file_or_number: Union[PathT, int], + file_or_number: PathT | int, *, directory: PathT, prefix: str, ) -> tuple[int, Path]: directory = Path(directory).resolve() - if isinstance(file_or_number, (str, Path)): + if isinstance(file_or_number, str | Path): file = Path(file_or_number) if (match := NPYReader._filename_re.fullmatch(file.name)) is None: raise ValueError(f"Filename {file.name!r} is not recognized") diff --git a/nonos/_types.py b/nonos/_types.py index dfe458a9..179a7a1b 100644 --- a/nonos/_types.py +++ b/nonos/_types.py @@ -16,15 +16,10 @@ from dataclasses import dataclass from enum import Enum, auto from pathlib import Path -from typing import Any, Protocol, Union, final +from typing import Any, Protocol, TypeAlias, final import numpy as np -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - if sys.version_info >= (3, 11): from enum import StrEnum from typing import assert_never @@ -34,7 +29,7 @@ from nonos._backports import StrEnum -PathT: TypeAlias = Union[str, Path] +PathT: TypeAlias = str | Path StrDict: TypeAlias = dict[str, Any] FloatArray: TypeAlias = "np.ndarray[Any, np.dtype[np.float32 | np.float64]]" @@ -53,10 +48,8 @@ class FrameType(Enum): @final -@dataclass(frozen=True, eq=False) +@dataclass(frozen=True, eq=False, slots=True) class BinData: - # TODO: use slots=True in @dataclass when Python 3.9 is dropped - __slots__ = ["data", "geometry", "x1", "x2", "x3"] data: StrDict geometry: Geometry x1: FloatArray @@ -71,10 +64,8 @@ def default_init(cls): @final -@dataclass(frozen=True, eq=False) +@dataclass(frozen=True, eq=False, slots=True) class OrbitalElements: - # TODO: use slots=True in @dataclass when Python 3.9 is dropped - __slots__ = ["i", "e", "a"] i: FloatArray e: FloatArray a: FloatArray @@ -148,16 +139,8 @@ def get_rotational_rate(self) -> FloatArray: @final -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class IniData: - # TODO: use slots=True in @dataclass when Python 3.9 is dropped - __slots__ = [ - "file", - "frame", - "rotational_rate", - "output_time_interval", - "meta", - ] file: Path frame: FrameType rotational_rate: float @@ -168,7 +151,7 @@ class IniData: class BinReader(Protocol): @staticmethod def parse_output_number_and_filename( - file_or_number: Union[PathT, int], + file_or_number: PathT | int, *, directory: PathT, prefix: str, diff --git a/nonos/api/_angle_parsing.py b/nonos/api/_angle_parsing.py index 204aa157..9e980567 100644 --- a/nonos/api/_angle_parsing.py +++ b/nonos/api/_angle_parsing.py @@ -1,7 +1,7 @@ import warnings from collections.abc import Callable from math import isclose -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: # note that these are documented as deprecated, but the replacement @@ -10,15 +10,15 @@ from mypy_extensions import DefaultArg, DefaultNamedArg find_phip_T = Callable[ - [DefaultArg(Optional[int]), DefaultNamedArg(Optional[str], "planet_file")], + [DefaultArg(int | None), DefaultNamedArg(str | None, "planet_file")], float, ] def _parse_planet_file( *, - planet_file: Optional[str] = None, - planet_number: Optional[int] = None, + planet_file: str | None = None, + planet_number: int | None = None, ) -> str: if planet_number is not None and planet_file is not None: raise TypeError( @@ -33,9 +33,9 @@ def _parse_planet_file( def _parse_rotation_angle( *, - rotate_by: Optional[float], - rotate_with: Optional[str], - planet_number_argument: tuple[str, Optional[int]], + rotate_by: float | None, + rotate_with: str | None, + planet_number_argument: tuple[str, int | None], find_phip: "find_phip_T", stacklevel: int, ) -> float: diff --git a/nonos/api/analysis.py b/nonos/api/analysis.py index 20167dca..d15409ae 100644 --- a/nonos/api/analysis.py +++ b/nonos/api/analysis.py @@ -6,7 +6,7 @@ from functools import cached_property from pathlib import Path from shutil import copyfile -from typing import TYPE_CHECKING, Any, Optional, Union, overload +from typing import TYPE_CHECKING, Any, overload import numpy as np from matplotlib.scale import SymmetricalLogTransform @@ -295,7 +295,7 @@ def native_from_wanted( self, _wanted_x1: str, _wanted_x2: None, / ) -> tuple[tuple[str], str]: ... - def native_from_wanted(self, _wanted_x1: str, _wanted_x2: Optional[str] = None, /): + def native_from_wanted(self, _wanted_x1: str, _wanted_x2: str | None = None, /): if self.geometry == "cartesian": conversion = { "x": "x", @@ -440,11 +440,11 @@ def __init__( on: int, operation: str, *, - inifile: Optional[PathT] = None, - code: Union[str, Recipe, None] = None, - directory: Optional[PathT] = None, - rotate_by: Optional[float] = None, - rotate_with: Optional[str] = None, + inifile: PathT | None = None, + code: str | Recipe | None = None, + directory: PathT | None = None, + rotate_by: float | None = None, + rotate_with: str | None = None, rotate_grid: int = -1, # deprecated ) -> None: self.field = field @@ -502,9 +502,9 @@ def shape(self) -> tuple[int, int, int]: def map( self, *wanted, - rotate_by: Optional[float] = None, - rotate_with: Optional[str] = None, - planet_corotation: Optional[int] = None, # deprecated + rotate_by: float | None = None, + rotate_with: str | None = None, + planet_corotation: int | None = None, # deprecated ) -> Plotable: rotate_by = _parse_rotation_angle( rotate_by=rotate_by, @@ -610,7 +610,7 @@ def rotate_axes(arr, shift: int): def save( self, - directory: Optional[PathT] = None, + directory: PathT | None = None, header_only: bool = False, ) -> Path: if directory is None: @@ -676,8 +676,8 @@ def find_iphi(self, phi=0): def _load_planet( self, *, - planet_number: Optional[int] = None, - planet_file: Optional[str] = None, + planet_number: int | None = None, + planet_file: str | None = None, ) -> PlanetData: planet_file = _parse_planet_file( planet_number=planet_number, planet_file=planet_file @@ -692,9 +692,9 @@ def _get_ind_output_number(self, time) -> int: def find_rp( self, - planet_number: Optional[int] = None, + planet_number: int | None = None, *, - planet_file: Optional[str] = None, + planet_file: str | None = None, ) -> float: pd = self._load_planet(planet_number=planet_number, planet_file=planet_file) ind_on = self._get_ind_output_number(pd.t) @@ -702,9 +702,9 @@ def find_rp( def find_rhill( self, - planet_number: Optional[int] = None, + planet_number: int | None = None, *, - planet_file: Optional[str] = None, + planet_file: str | None = None, ) -> float: ini = self._loader.load_ini_file() pd = self._load_planet(planet_number=planet_number, planet_file=planet_file) @@ -714,9 +714,9 @@ def find_rhill( def find_phip( self, - planet_number: Optional[int] = None, + planet_number: int | None = None, *, - planet_file: Optional[str] = None, + planet_file: str | None = None, ) -> float: pd = self._load_planet(planet_number=planet_number, planet_file=planet_file) ind_on = self._get_ind_output_number(pd.t) @@ -727,7 +727,7 @@ def _parse_operation_name( *, prefix: str, default_suffix: str, - operation_name: Optional[str], + operation_name: str | None, ) -> str: if operation_name == "": raise ValueError("operation_name cannot be empty") @@ -1232,9 +1232,9 @@ def azimuthal_at_phi(self, phi=0.0, *, operation_name=None) -> "GasField": def azimuthal_at_planet( self, - planet_number: Optional[int] = None, + planet_number: int | None = None, *, - planet_file: Optional[str] = None, + planet_file: str | None = None, operation_name=None, ) -> "GasField": planet_file = _parse_planet_file( @@ -1308,9 +1308,9 @@ def azimuthal_average(self, *, operation_name=None) -> "GasField": def remove_planet_hill_band( self, - planet_number: Optional[int] = None, + planet_number: int | None = None, *, - planet_file: Optional[str] = None, + planet_file: str | None = None, operation_name=None, ) -> "GasField": planet_file = _parse_planet_file( @@ -1513,10 +1513,10 @@ def diff(self, on_2) -> "GasField": def rotate( self, - planet_corotation: Optional[int] = None, + planet_corotation: int | None = None, *, - rotate_with: Optional[str] = None, - rotate_by: Optional[float] = None, + rotate_with: str | None = None, + rotate_by: float | None = None, ) -> "GasField": rotate_by = _parse_rotation_angle( rotate_by=rotate_by, @@ -1602,17 +1602,17 @@ class GasDataSet: def __init__( self, - input_dataset: Union[int, PathT], + input_dataset: int | PathT, /, *, - inifile: Optional[PathT] = None, - code: Union[str, Recipe, None] = None, - geometry: Optional[str] = None, - directory: Optional[PathT] = None, - fluid: Optional[str] = None, - operation: Optional[str] = None, + inifile: PathT | None = None, + code: str | Recipe | None = None, + geometry: str | None = None, + directory: PathT | None = None, + fluid: str | None = None, + operation: str | None = None, ) -> None: - if isinstance(input_dataset, (str, Path)): + if isinstance(input_dataset, str | Path): input_dataset = Path(input_dataset) directory_from_input = input_dataset.parent if directory is None: @@ -1722,9 +1722,9 @@ def from_npy( cls, on: int, *, - inifile: Optional[PathT] = None, - code: Union[str, Recipe, None] = None, - directory: Optional[PathT] = None, + inifile: PathT | None = None, + code: str | Recipe | None = None, + directory: PathT | None = None, operation: str, ) -> "GasDataSet": warnings.warn( diff --git a/nonos/api/satellite.py b/nonos/api/satellite.py index ce46454c..e4b4584b 100644 --- a/nonos/api/satellite.py +++ b/nonos/api/satellite.py @@ -18,10 +18,10 @@ def file_analysis( filename: "PathT", *, - inifile: Optional[str] = None, - code: Optional[str] = None, + inifile: str | None = None, + code: str | None = None, directory: Optional["PathT"] = None, - norb: Optional[int] = None, + norb: int | None = None, ) -> "FloatArray": from scipy.ndimage import uniform_filter1d @@ -86,10 +86,10 @@ def __init__( ly: GasField, field: GasField, *, - xmin: Optional[float] = None, - xmax: Optional[float] = None, - ymin: Optional[float] = None, - ymax: Optional[float] = None, + xmin: float | None = None, + xmax: float | None = None, + ymin: float | None = None, + ymax: float | None = None, size_interpolated: int = 1000, niter_lic: int = 6, kernel_length: int = 101, @@ -126,13 +126,13 @@ def plot( fig: "Figure", ax: "Axes", *, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: float | None = None, + vmax: float | None = None, alpha: float = 0.45, log: bool = False, cmap=None, - title: Optional[str] = None, - density_streamlines: Optional[float] = None, + title: str | None = None, + density_streamlines: float | None = None, color_streamlines: str = "black", ): dict_background = {} @@ -222,8 +222,8 @@ def from_data( coords: Coordinates, on: int, operation: str, - inifile: Optional[str] = None, - code: Optional[str] = None, + inifile: str | None = None, + code: str | None = None, directory: str = "", rotate_grid: int = -1, ): # pragma: no cover diff --git a/nonos/loaders.py b/nonos/loaders.py index 9fe09cc0..1aea1b5f 100644 --- a/nonos/loaders.py +++ b/nonos/loaders.py @@ -34,7 +34,7 @@ class Recipe(StrEnum): @final -@dataclass(frozen=True, eq=True) +@dataclass(frozen=True, eq=True, slots=True) class Loader: r""" A composable data loader interface. @@ -59,13 +59,6 @@ class Loader: FileNotFoundError: if `parameter_file` doesn't exist or is a directory. """ - # TODO: use slots=True in @dataclass when Python 3.9 is dropped - __slots__ = [ - "parameter_file", - "binary_reader", - "planet_reader", - "ini_reader", - ] parameter_file: Path binary_reader: type["BinReader"] planet_reader: type["PlanetReader"] @@ -91,7 +84,7 @@ def load_ini_file(self) -> "IniData": def loader_from( *, - code: Optional[str] = None, + code: str | None = None, parameter_file: Optional["PathT"] = None, directory: Optional["PathT"] = None, ) -> Loader: @@ -219,7 +212,7 @@ def _ingredients_from(recipe: Recipe, /) -> Ingredients: def recipe_from( *, - code: Optional[str] = None, + code: str | None = None, parameter_file: Optional["PathT"] = None, directory: Optional["PathT"] = None, ) -> Recipe: diff --git a/nonos/logging.py b/nonos/logging.py index 74cbbb75..ebec2170 100644 --- a/nonos/logging.py +++ b/nonos/logging.py @@ -1,12 +1,11 @@ import sys -from typing import Union from loguru import logger from rich import print as rprint from rich.logging import RichHandler -def configure_logger(level: Union[int, str] = 30, **kwargs) -> None: +def configure_logger(level: int | str = 30, **kwargs) -> None: logger.remove() # remove pre-existing handler logger.add( RichHandler( diff --git a/nonos/main.py b/nonos/main.py index f28f7480..e08aacd0 100644 --- a/nonos/main.py +++ b/nonos/main.py @@ -14,12 +14,11 @@ from importlib.metadata import version from multiprocessing import Pool from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any import cblind # noqa import inifix import numpy as np -from packaging.version import Version from nonos.api import GasDataSet from nonos.api._angle_parsing import _parse_planet_file @@ -48,7 +47,6 @@ NONOS_VERSION = version("nonos") -INIFIX_GE_5_0 = Version(version("inifix")) >= Version("5.0.0") def get_non_interactive_figure(fmt: str) -> "Figure": @@ -86,7 +84,7 @@ def process_field( geometry, diff, log, - planet_file: Optional[str], + planet_file: str | None, extent, vmin, vmax, @@ -421,7 +419,7 @@ def get_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[list[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: parser = get_parser() clargs = vars(parser.parse_args(argv)) @@ -470,12 +468,7 @@ def main(argv: Optional[list[str]] = None) -> int: conf_repr[key] = args[key] print(f"# Generated with nonos {NONOS_VERSION}") s = inifix.dumps(conf_repr) - if INIFIX_GE_5_0: - print(inifix.format_string(s)) # type: ignore [attr-defined] - else: - from inifix.format import iniformat - - print(iniformat(s)) + print(inifix.format_string(s)) return 0 try: @@ -542,7 +535,7 @@ def mytrack(iterable, *args, **kwargs): def mytrack(iterable, *args, **kwargs): # noqa: ARG001 return iterable - planet_file: Optional[str] + planet_file: str | None if not is_set(args["corotate"]): planet_file = None else: diff --git a/nonos/parsing.py b/nonos/parsing.py index aea44921..0e59651c 100644 --- a/nonos/parsing.py +++ b/nonos/parsing.py @@ -1,4 +1,4 @@ -from typing import Any, Literal, Optional, TypeVar, Union, overload +from typing import Any, Literal, TypeVar, overload import numpy as np @@ -11,7 +11,7 @@ def is_set(x: Any) -> bool: T2 = TypeVar("T2") -def userval_or_default(userval: T1, /, *, default: T2) -> Union[T1, T2]: +def userval_or_default(userval: T1, /, *, default: T2) -> T1 | T2: # it'd be nice to avoid a Union as a return type, however it's not clear # how to express what this function does in typing language. # In practice this is used in places where it's very hard to constrain T1, so @@ -23,8 +23,8 @@ def userval_or_default(userval: T1, /, *, default: T2) -> Union[T1, T2]: def parse_output_number_range( - on: Union[list[int], int, Literal["unset"], None], - maxval: Optional[int] = None, + on: list[int] | int | Literal["unset"] | None, + maxval: int | None = None, ) -> list[int]: if not is_set(on): if maxval is None: @@ -56,7 +56,7 @@ def parse_output_number_range( return ret -def parse_range(extent, dim: int) -> tuple[Optional[float], ...]: +def parse_range(extent, dim: int) -> tuple[float | None, ...]: if not is_set(extent): return (None,) * 2 * dim @@ -69,7 +69,7 @@ def parse_range(extent, dim: int) -> tuple[Optional[float], ...]: @overload def range_converter( - extent: tuple[Optional[float], Optional[float]], + extent: tuple[float | None, float | None], abscissa: np.ndarray, ordinate: np.ndarray, ) -> tuple[float, float]: ... @@ -77,7 +77,7 @@ def range_converter( @overload def range_converter( - extent: tuple[Optional[float], Optional[float], Optional[float], Optional[float]], + extent: tuple[float | None, float | None, float | None, float | None], abscissa: np.ndarray, ordinate: np.ndarray, ) -> tuple[float, float, float, float]: ... @@ -100,7 +100,7 @@ def range_converter(extent, abscissa, ordinate): raise TypeError(f"Expected extent to be of length 2 or 4, got {len(extent)=}") -def parse_image_format(s: Optional[str]) -> str: +def parse_image_format(s: str | None) -> str: from matplotlib.backend_bases import FigureCanvasBase if not is_set(s): diff --git a/pyproject.toml b/pyproject.toml index 2c25ac0b..085a690b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,18 +16,17 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Typing :: Typed", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "cblind>=2.3.0", - "inifix>=3.0.0", + "inifix>=5.0.0", "lick>=0.5.1", "loguru>=0.5.3", "matplotlib>=3.5.0", - "numpy>=1.19.3", - "packaging>=20.0", + "numpy>=1.21.3", "rich>=10.13.0", - "scipy>=1.6.1", + "scipy>=1.7.3", "typing_extensions >= 4.4.0 ; python_version < '3.12'", ] @@ -89,10 +88,10 @@ filterwarnings = [ ] [tool.mypy] -# python_version = "3.9" # this can be uncommented (and updated) when Python 3.9 is dropped +python_version = "3.10" show_error_codes = true warn_unused_configs = true -# warn_unused_ignores = true # this can be uncommented (and updated) when Python 3.9 is dropped +warn_unused_ignores = true warn_unreachable = true show_error_context = true disallow_untyped_defs = false # TODO: add missing annotations and switch this option to true