Skip to content

Commit

Permalink
refactor type hinting and improve code clarity with TYPE_CHECKING imp…
Browse files Browse the repository at this point in the history
…orts
  • Loading branch information
fbriol committed Feb 4, 2025
1 parent 50f36e7 commit 5c2b1bc
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 47 deletions.
14 changes: 9 additions & 5 deletions src/python/pyfes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@
"""Tidal model prediction library."""
from __future__ import annotations

from typing import TYPE_CHECKING

from . import core
from .astronomic_angle import AstronomicAngle
from .config import load as load_config
from .core import Constituent, Formulae, Quality, constituents
from .leap_seconds import get_leap_seconds
from .typing import VectorDateTime64, VectorFloat64, VectorInt8
from .version import __version__
from .wave_table import WaveDict, WaveTable

if TYPE_CHECKING:
from .typing import VectorDateTime64, VectorFloat64, VectorInt8

__all__ = [
'__version__',
'constituents',
'AstronomicAngle',
'Constituent',
'Formulae',
'Quality',
'load_config',
'WaveDict',
'WaveTable',
'__version__',
'constituents',
'load_config',
]


Expand Down Expand Up @@ -138,7 +142,7 @@ def evaluate_tide(
tidal model used is a Cartesian grid.
"""
return core.evaluate_tide(
tidal_model, # type: ignore
tidal_model, # type: ignore[arg-type]
date,
get_leap_seconds(date),
longitude,
Expand Down
37 changes: 22 additions & 15 deletions src/python/pyfes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"""
from __future__ import annotations

from typing import Any, NamedTuple, Union
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, NamedTuple, Union
import dataclasses
import enum
import os
Expand All @@ -30,7 +29,11 @@
AbstractTidalModelComplex128,
Constituent,
)
from .typing import Matrix, Vector

if TYPE_CHECKING:
from collections.abc import Callable

from .typing import Matrix, Vector

#: Alias for a tidal type.
TidalModel = Union[AbstractTidalModelComplex64, AbstractTidalModelComplex128]
Expand All @@ -41,6 +44,9 @@
#: Maximum number of nested environment variables.
MAX_INTERPOLATION_DEPTH = 10

#: Number of dimensions expected in the wave data.
WAVE_DIMENSIONS = 2

#: Alias to LPG classes known to this software.
LGPModel = type[core.tidal_model.LGP1Complex64
| core.tidal_model.LGP1Complex128
Expand Down Expand Up @@ -150,7 +156,7 @@ def load_cartesian_model(
Returns:
A tuple containing the longitude, latitude and tidal model.
"""
with netCDF4.Dataset(path) as ds: # type: ignore
with netCDF4.Dataset(path) as ds:
lon: Vector = ds.variables[lon_name][:]
lat: Vector = ds.variables[lat_name][:]
amp: Matrix = numpy.ma.filled(ds.variables[amp_name][:], numpy.nan)
Expand Down Expand Up @@ -251,9 +257,9 @@ class GridProperties(NamedTuple):
#: Longitude axis.
lon: Vector
#: Shape of the tidal model.
shape: tuple[int, int]
shape: tuple[int, ...]

def __ne__(self, other: Any) -> bool:
def __ne__(self, other: object) -> bool:
if not isinstance(other, GridProperties):
return NotImplemented
return (self.dtype != other.dtype and self.shape != other.shape
Expand Down Expand Up @@ -287,7 +293,7 @@ class TidalModelInstance(NamedTuple):
self.phase,
)

if len(wave.shape) != 2:
if wave.ndim != WAVE_DIMENSIONS:
raise ValueError(f'defined constituent {constituent!r} has '
f'invalid shape: {wave.shape!r}.')

Expand Down Expand Up @@ -406,7 +412,7 @@ def _lgp_class(self, dtype: numpy.dtype) -> LGPModel:

def load(self) -> TidalModel:
"""Load the tidal model defined by the configuration."""
with netCDF4.Dataset(self.path, 'r') as ds: # type: ignore
with netCDF4.Dataset(self.path, 'r') as ds:
lon: Vector = ds.variables[self.longitude][:]
lat: Vector = ds.variables[self.latitude][:]
triangles: Matrix = ds.variables[self.triangle][:]
Expand Down Expand Up @@ -514,20 +520,21 @@ def load(path: str | os.PathLike) -> dict[str, TidalModel]:
raise ValueError(f'Configuration file {path!r} is empty.')

key: str
for key in user_settings:
for key, settings in user_settings.items():
if key not in ['tide', 'radial']:
raise ValueError(f'Configuration file {path!r} is invalid. '
f'Expected "tide" or "radial" section.')
try:
models[key] = _load_model(user_settings[key], tidal_type=key)
models[key] = _load_model(settings, tidal_type=key)
except TypeError as err:
if 'unexpected keyword argument' in str(err):
msg = str(err)
key = msg.split('unexpected keyword argument ')[1]
key = key.replace("'", '')
unknown_key = msg.split('unexpected keyword argument ')[1]
unknown_key = unknown_key.replace("'", '')
section: str = msg.split('.__init__', maxsplit=1)[0].lower()
raise ValueError(f'Configuration file {path!r} is invalid. '
f'Unknown keyword: {key!r} in section '
f'{section!r}.') from err
raise ValueError(
f'Configuration file {path!r} is invalid. '
f'Unknown keyword: {unknown_key!r} in section '
f'{section!r}.') from err
raise err from None
return models
2 changes: 1 addition & 1 deletion src/python/pyfes/console_script/fes_convert_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def parse_args():
def main():
"""Main function."""
args = parse_args()
waves = dict()
waves = {}

properties = None
for path in args.mesh:
Expand Down
4 changes: 2 additions & 2 deletions src/python/pyfes/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import datetime
from . import constituents, datemanip, mesh, tidal_model

__all__ = [
"AbstractTidalModelComplex128",
"AbstractTidalModelComplex64",
"AbstractTidalModelComplex128",
"Accelerator",
"AstronomicAngle",
"Axis",
Expand All @@ -17,9 +17,9 @@ __all__ = [
"TideType",
"Wave",
"WaveTable",
"evaluate_tide",
"constituents",
"datemanip",
"evaluate_tide",
"mesh",
"tidal_model",
]
Expand Down
30 changes: 20 additions & 10 deletions src/python/pyfes/leap_seconds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""
from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING
import datetime
import functools
import pathlib
Expand All @@ -21,12 +21,15 @@

import numpy

from .typing import (
NDArrayStructured,
VectorDateTime64,
VectorInt64,
VectorUInt16,
)
if TYPE_CHECKING:
from collections.abc import Callable

from .typing import (
NDArrayStructured,
VectorDateTime64,
VectorInt64,
VectorUInt16,
)

#: URL of the IERS leap second file.
LEAP_SECOND_URL = 'https://data.iana.org/time-zones/data/leap-seconds.list'
Expand All @@ -43,6 +46,9 @@
'Nov', 'Dec'
]

#: Standard response for successful HTTP requests.
HTTP_OK = 200


def _build_urlopener() -> urllib.request.OpenerDirector:
"""Helper for building a `urllib.request.build_opener` which handles
Expand All @@ -64,7 +70,7 @@ def _download_leap_second_file() -> None:
})

response = urlopener.open(req, timeout=None)
if response.status != 200:
if response.status != HTTP_OK:
raise RuntimeError(
f'Failed to download leap second file: {response.status} '
f'{response.reason}')
Expand Down Expand Up @@ -118,13 +124,17 @@ def _load_leap_second_file() -> NDArrayStructured:
# If the leap second file has expired, try to download a new one.
warnings.warn(
f'Leap second file {LEAP_SECOND_FILE.name} has expired. '
'Downloading a new version.', UserWarning)
'Downloading a new version.',
UserWarning,
stacklevel=2)
try:
expires, lines = _read_leap_second_file(download=True)
except RuntimeError:
warnings.warn(
'Failed to download new leap second file. '
'Using expired file.', UserWarning)
'Using expired file.',
UserWarning,
stacklevel=2)

# Number of seconds elapsed between 1900-01-01T00:00:00:00+0000 and
# 1970-01-01T00:00:00:00+0000
Expand Down
6 changes: 4 additions & 2 deletions src/python/pyfes/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@
MatrixComplex128 = Matrix[numpy.complex128]
NDArrayStructured = numpy.ndarray[Any, numpy.dtype[numpy.void]]
else:
ScalarType = TypeVar('ScalarType', bound=numpy.generic, covariant=True)
DType = GenericAlias(numpy.dtype, (ScalarType, ))
ScalarType_co = TypeVar('ScalarType_co',
bound=numpy.generic,
covariant=True)
DType = GenericAlias(numpy.dtype, (ScalarType_co, ))

Vector = GenericAlias(numpy.ndarray, (Any, DType))
Matrix = GenericAlias(numpy.ndarray, (Any, DType))
Expand Down
27 changes: 15 additions & 12 deletions src/python/pyfes/wave_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,32 @@
"""Properties of tidal constituents."""
from __future__ import annotations

from typing import TYPE_CHECKING
import datetime

import numpy

from . import core
from .core import Formulae
from .typing import (
VectorComplex128,
VectorDateTime64,
VectorFloat64,
VectorUInt16,
)

if TYPE_CHECKING:
from .typing import (
VectorComplex128,
VectorDateTime64,
VectorFloat64,
VectorUInt16,
)

#: Maximum number of tidal constituents to display in the representation
MAX_CONSTITUENTS = 9


class WaveTable(core.WaveTable):
"""Properties of tidal constituents."""

def __repr__(self) -> str:
constituents: list[str] = self.keys()
if len(constituents) > 9:
if len(constituents) > MAX_CONSTITUENTS:
constituents = constituents[:4] + ['...'] + constituents[-4:]

return '{}.{}({})'.format(self.__class__.__module__,
Expand Down Expand Up @@ -57,7 +63,7 @@ def compute_nodal_modulations(
return super().compute_nodal_modulations(datetime64, leap_seconds)
# The method throws an error if the dates are not datetime64
return super().compute_nodal_modulations(
dates, # type: ignore
dates, # type: ignore[arg-type]
leap_seconds,
formulae)

Expand Down Expand Up @@ -152,10 +158,7 @@ def harmonic_analysis( # type: ignore[override]
:py:meth:`WaveTable.harmonic_analysis`
"""
analysis: VectorComplex128 = super().harmonic_analysis(h, f, vu)
return {
constituent: coefficient
for constituent, coefficient in zip(self.keys(), analysis)
}
return dict(zip(self.keys(), analysis))

def tide_from_tide_series(
self,
Expand Down

0 comments on commit 5c2b1bc

Please sign in to comment.