diff --git a/.binder/environment.yml b/.binder/environment.yml index e8fdc54c6..b223d3193 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -1,6 +1,5 @@ name: parcels_binder channels: - conda-forge - - defaults dependencies: - parcels diff --git a/docs/documentation/additional_examples.rst b/docs/documentation/additional_examples.rst index 54db65047..e4c8eefd0 100644 --- a/docs/documentation/additional_examples.rst +++ b/docs/documentation/additional_examples.rst @@ -1,3 +1,6 @@ +Python Example Scripts +====================== + example_brownian.py ------------------- diff --git a/docs/examples/tutorial_delaystart.ipynb b/docs/examples/tutorial_delaystart.ipynb index 1ed477b14..456581501 100644 --- a/docs/examples/tutorial_delaystart.ipynb +++ b/docs/examples/tutorial_delaystart.ipynb @@ -76,7 +76,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The simplest way to delaye the start of a particle is to use the `time` argument for each particle\n" + "The simplest way to delay the start of a particle is to use the `time` argument for each particle\n" ] }, { diff --git a/docs/examples/tutorial_timestamps.ipynb b/docs/examples/tutorial_timestamps.ipynb index ae7c41347..6cc85f0b9 100644 --- a/docs/examples/tutorial_timestamps.ipynb +++ b/docs/examples/tutorial_timestamps.ipynb @@ -127,7 +127,7 @@ "outputs": [], "source": [ "timestamps = np.expand_dims(\n", - " np.array([np.datetime64(\"2001-%.2d-15\" % m) for m in range(1, 13)]), axis=1\n", + " np.array([np.datetime64(f\"2001-{m:02d}-15\") for m in range(1, 13)]), axis=1\n", ")" ] }, diff --git a/docs/examples/tutorial_unitconverters.ipynb b/docs/examples/tutorial_unitconverters.ipynb index d39586067..74eb85fc8 100644 --- a/docs/examples/tutorial_unitconverters.ipynb +++ b/docs/examples/tutorial_unitconverters.ipynb @@ -280,7 +280,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The units for Brownian diffusion are in $m^2/s$. If (and only if!) the diffusion fields are called `kh_zonal` and `kh_meridional`, Parcels will automatically assign the correct Unitconverter objects to these fields.\n" + "The units for Brownian diffusion are in $m^2/s$. If (and only if!) the diffusion fields are called \"Kh_zonal\" and \"Kh_meridional\", Parcels will automatically assign the correct Unitconverter objects to these fields.\n" ] }, { diff --git a/parcels/_typing.py b/parcels/_typing.py index 2e7ace119..a1e24a6e3 100644 --- a/parcels/_typing.py +++ b/parcels/_typing.py @@ -9,7 +9,8 @@ import ast import datetime import os -from typing import Callable, Literal +from collections.abc import Callable +from typing import Literal class ParcelsAST(ast.AST): diff --git a/parcels/compilation/codecompiler.py b/parcels/compilation/codecompiler.py index 794d4cfd9..4237c8752 100644 --- a/parcels/compilation/codecompiler.py +++ b/parcels/compilation/codecompiler.py @@ -107,7 +107,7 @@ def __init__(self, cppargs=None, ldargs=None, incdirs=None, libdirs=None, libs=N self._ldargs += lflags self._ldargs += ldargs if len(Lflags) > 0: - self._ldargs += ["-Wl, -rpath=%s" % (":".join(libdirs))] + self._ldargs += [f"-Wl, -rpath={':'.join(libdirs)}"] self._ldargs += arch_flag self._incdirs = incdirs self._libdirs = libdirs diff --git a/parcels/compilation/codegenerator.py b/parcels/compilation/codegenerator.py index e869bad19..e7eef09cc 100644 --- a/parcels/compilation/codegenerator.py +++ b/parcels/compilation/codegenerator.py @@ -33,7 +33,7 @@ def __getattr__(self, attr): elif isinstance(getattr(self.obj, attr), VectorField): return VectorFieldNode(getattr(self.obj, attr), ccode=f"{self.ccode}->{attr}") else: - return ConstNode(getattr(self.obj, attr), ccode="%s" % (attr)) + return ConstNode(getattr(self.obj, attr), ccode=f"{attr}") class FieldNode(IntrinsicNode): @@ -489,13 +489,13 @@ def visit_FunctionDef(self, node): c.Value("double", "time"), ] for field in self.field_args.values(): - args += [c.Pointer(c.Value("CField", "%s" % field.ccode_name))] + args += [c.Pointer(c.Value("CField", f"{field.ccode_name}"))] for field in self.vector_field_args.values(): for fcomponent in ["U", "V", "W"]: try: f = getattr(field, fcomponent) if f.ccode_name not in self.field_args: - args += [c.Pointer(c.Value("CField", "%s" % f.ccode_name))] + args += [c.Pointer(c.Value("CField", f"{f.ccode_name}"))] self.field_args[f.ccode_name] = f except: pass # field.W does not always exist @@ -528,9 +528,9 @@ def visit_Call(self, node): if isinstance(node.func, PrintNode): # Write our own Print parser because Python3-AST does not seem to have one if isinstance(node.args[0], ast.Str): - node.ccode = str(c.Statement('printf("%s\\n")' % (node.args[0].s))) + node.ccode = str(c.Statement(f'printf("{node.args[0].s}\\n")')) elif isinstance(node.args[0], ast.Name): - node.ccode = str(c.Statement('printf("%%f\\n", %s)' % (node.args[0].id))) + node.ccode = str(c.Statement(f'printf("%f\\n", {node.args[0].id})')) elif isinstance(node.args[0], ast.BinOp): if hasattr(node.args[0].right, "ccode"): args = node.args[0].right.ccode @@ -545,12 +545,12 @@ def visit_Call(self, node): args.append(a.id) else: args = [] - s = 'printf("%s\\n"' % node.args[0].left.s + s = f'printf("{node.args[0].left.s}\\n"' if isinstance(args, str): - s = s + (", %s)" % args) + s = s + f", {args})" else: for arg in args: - s = s + (", %s" % arg) + s = s + (f", {arg}") s = s + ")" node.ccode = str(c.Statement(s)) else: @@ -568,7 +568,7 @@ def visit_Call(self, node): elif isinstance(a, ParticleNode): continue elif pointer_args: - a.ccode = "&%s" % a.ccode + a.ccode = f"&{a.ccode}" ccode_args = ", ".join([a.ccode for a in node.args[pointer_args:]]) try: if isinstance(node.func, str): @@ -742,7 +742,7 @@ def visit_BoolOp(self, node): self.visit(node.op) for v in node.values: self.visit(v) - op_str = " %s " % node.op.ccode + op_str = f" {node.op.ccode} " node.ccode = op_str.join([v.ccode for v in node.values]) def visit_Eq(self, node): @@ -813,7 +813,7 @@ def visit_ConstNode(self, node): def visit_Return(self, node): self.visit(node.value) - node.ccode = c.Statement("return %s" % node.value.ccode) + node.ccode = c.Statement(f"return {node.value.ccode}") def visit_FieldEvalNode(self, node): self.visit(node.field) @@ -909,16 +909,16 @@ def visit_Print(self, node): for n in node.values: self.visit(n) if hasattr(node.values[0], "s"): - node.ccode = c.Statement('printf("%s\\n")' % (n.ccode)) + node.ccode = c.Statement(f'printf("{n.ccode}\\n")') return if hasattr(node.values[0], "s_print"): args = node.values[0].right.ccode - s = 'printf("%s\\n"' % node.values[0].left.ccode + s = f'printf("{node.values[0].left.ccode}\\n"' if isinstance(args, str): - s = s + (", %s)" % args) + s = s + f", {args})" else: for arg in args: - s = s + (", %s" % arg) + s = s + (f", {arg}") s = s + ")" node.ccode = c.Statement(s) return @@ -973,7 +973,7 @@ def generate(self, funcname, field_args, const_args, kernel_ast, c_include): c.Value("double", "dt"), ] for field, _ in field_args.items(): - args += [c.Pointer(c.Value("CField", "%s" % field))] + args += [c.Pointer(c.Value("CField", f"{field}"))] for const, _ in const_args.items(): args += [c.Value("double", const)] # are we SURE those const's are double's ? fargs_str = ", ".join(["particles->time_nextloop[pnum]"] + list(field_args.keys()) + list(const_args.keys())) diff --git a/parcels/field.py b/parcels/field.py index 84d59ff5d..7546a2783 100644 --- a/parcels/field.py +++ b/parcels/field.py @@ -2,9 +2,10 @@ import datetime import math import warnings +from collections.abc import Iterable from ctypes import POINTER, Structure, c_float, c_int, pointer from pathlib import Path -from typing import TYPE_CHECKING, Iterable, Type +from typing import TYPE_CHECKING import dask.array as da import numpy as np @@ -222,7 +223,7 @@ def __init__( stacklevel=2, ) - self.fieldset: "FieldSet" | None = None + self.fieldset: FieldSet | None = None if allow_time_extrapolation is None: self.allow_time_extrapolation = True if len(self.grid.time) == 1 else False else: @@ -299,7 +300,7 @@ def __init__( # since some datasets do not provide the deeper level of data (which is ignored by the interpolation). self.data_full_zdim = kwargs.pop("data_full_zdim", None) self.data_chunks = [] # type: ignore # the data buffer of the FileBuffer raw loaded data - shall be a list of C-contiguous arrays - self.c_data_chunks: list["PointerType" | None] = [] # C-pointers to the data_chunks array + self.c_data_chunks: list[PointerType | None] = [] # C-pointers to the data_chunks array self.nchunks: tuple[int, ...] = () self.chunk_set: bool = False self.filebuffers = [None] * 2 @@ -565,13 +566,10 @@ def from_netcdf( "time dimension in indices is not necessary anymore. It is then ignored.", FieldSetWarning, stacklevel=2 ) - if "full_load" in kwargs: # for backward compatibility with Parcels < v2.0.0 - deferred_load = not kwargs["full_load"] - - if grid.time.size <= 2 or deferred_load is False: + if grid.time.size <= 2: deferred_load = False - _field_fb_class: Type[DeferredDaskFileBuffer | DaskFileBuffer | DeferredNetcdfFileBuffer | NetcdfFileBuffer] + _field_fb_class: type[DeferredDaskFileBuffer | DaskFileBuffer | DeferredNetcdfFileBuffer | NetcdfFileBuffer] if chunksize not in [False, None]: if deferred_load: _field_fb_class = DeferredDaskFileBuffer @@ -828,11 +826,9 @@ def calc_cell_edge_sizes(self): self.cell_edge_sizes = self.grid.cell_edge_sizes else: raise ValueError( - ( - f"Field.cell_edge_sizes() not implemented for {self.grid.gtype} grids. " - "You can provide Field.grid.cell_edge_sizes yourself by in, e.g., " - "NEMO using the e1u fields etc from the mesh_mask.nc file." - ) + f"Field.cell_edge_sizes() not implemented for {self.grid.gtype} grids. " + "You can provide Field.grid.cell_edge_sizes yourself by in, e.g., " + "NEMO using the e1u fields etc from the mesh_mask.nc file." ) def cell_areas(self): diff --git a/parcels/fieldset.py b/parcels/fieldset.py index 9a5fdf3ba..c21c1f79a 100644 --- a/parcels/fieldset.py +++ b/parcels/fieldset.py @@ -1228,7 +1228,7 @@ def from_parcels( extra_fields.update({"U": uvar, "V": vvar}) for vars in extra_fields: dimensions[vars] = deepcopy(default_dims) - dimensions[vars]["depth"] = "depth%s" % vars.lower() + dimensions[vars]["depth"] = f"depth{vars.lower()}" filenames = {v: str(f"{basename}{v}.nc") for v in extra_fields.keys()} return cls.from_netcdf( filenames, @@ -1317,7 +1317,7 @@ def from_modulefile(cls, filename, modulename="create_fieldset", **kwargs): """ # check if filename exists if not os.path.exists(filename): - raise IOError(f"FieldSet module file {filename} does not exist") + raise OSError(f"FieldSet module file {filename} does not exist") # Importing the source file directly (following https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly) spec = importlib.util.spec_from_file_location(modulename, filename) @@ -1326,10 +1326,10 @@ def from_modulefile(cls, filename, modulename="create_fieldset", **kwargs): spec.loader.exec_module(fieldset_module) if not hasattr(fieldset_module, modulename): - raise IOError(f"{filename} does not contain a {modulename} function") + raise OSError(f"{filename} does not contain a {modulename} function") fieldset = getattr(fieldset_module, modulename)(**kwargs) if not isinstance(fieldset, FieldSet): - raise IOError(f"Module {filename}.{modulename} does not return a FieldSet object") + raise OSError(f"Module {filename}.{modulename} does not return a FieldSet object") return fieldset def get_fields(self): diff --git a/parcels/grid.py b/parcels/grid.py index 02763fdf9..70a277be9 100644 --- a/parcels/grid.py +++ b/parcels/grid.py @@ -92,6 +92,14 @@ def __init__( self._add_last_periodic_data_timestep = False self.depth_field = None + def __repr__(self): + with np.printoptions(threshold=5, suppress=True, linewidth=120, formatter={"float": "{: 0.2f}".format}): + return ( + f"{type(self).__name__}(" + f"lon={self.lon!r}, lat={self.lat!r}, time={self.time!r}, " + f"time_origin={self.time_origin!r}, mesh={self.mesh!r})" + ) + @staticmethod def create_grid( lon: npt.ArrayLike, @@ -352,7 +360,7 @@ def __init__(self, lon, lat, time, time_origin, mesh: Mesh): stacklevel=2, ) - def add_periodic_halo(self, zonal, meridional, halosize=5): + def add_periodic_halo(self, zonal: bool, meridional: bool, halosize: int = 5): """Add a 'halo' to the Grid, through extending the Grid (and lon/lat) similarly to the halo created for the Fields diff --git a/parcels/interaction/interactionkernel.py b/parcels/interaction/interactionkernel.py index 665be16ab..7df5e5bed 100644 --- a/parcels/interaction/interactionkernel.py +++ b/parcels/interaction/interactionkernel.py @@ -1,5 +1,4 @@ import inspect -import sys import warnings from collections import defaultdict @@ -109,10 +108,7 @@ def check_kernel_signature_on_version(self): numkernelargs = [] if self._pyfunc is not None and isinstance(self._pyfunc, list): for func in self._pyfunc: - if sys.version_info[0] < 3: - numkernelargs.append(len(inspect.getargspec(func).args)) - else: - numkernelargs.append(len(inspect.getfullargspec(func).args)) + numkernelargs.append(len(inspect.getfullargspec(func).args)) return numkernelargs def remove_lib(self): diff --git a/parcels/kernel.py b/parcels/kernel.py index 950c56c81..c20bcc264 100644 --- a/parcels/kernel.py +++ b/parcels/kernel.py @@ -127,7 +127,7 @@ def _cache_key(self): field_keys = "-".join( [f"{name}:{field.units.__class__.__name__}" for name, field in self.field_args.items()] ) - key = self.name + self.ptype._cache_key + field_keys + ("TIME:%f" % ostime()) + key = self.name + self.ptype._cache_key + field_keys + (f"TIME:{ostime():f}") return hashlib.md5(key.encode("utf-8")).hexdigest() def remove_deleted(self, pset): @@ -239,9 +239,10 @@ def __init__( numkernelargs = self.check_kernel_signature_on_version() - assert ( - numkernelargs == 3 - ), "Since Parcels v2.0, kernels do only take 3 arguments: particle, fieldset, time !! AND !! Argument order in field interpolation is time, depth, lat, lon." + if numkernelargs != 3: + raise ValueError( + "Since Parcels v2.0, kernels do only take 3 arguments: particle, fieldset, time !! AND !! Argument order in field interpolation is time, depth, lat, lon." + ) self.name = f"{ptype.name}{self.funcname}" @@ -310,7 +311,7 @@ def _cache_key(self): field_keys = "-".join( [f"{name}:{field.units.__class__.__name__}" for name, field in self.field_args.items()] ) - key = self.name + self.ptype._cache_key + field_keys + ("TIME:%f" % ostime()) + key = self.name + self.ptype._cache_key + field_keys + (f"TIME:{ostime():f}") return hashlib.md5(key.encode("utf-8")).hexdigest() def add_scipy_positionupdate_kernels(self): @@ -330,7 +331,7 @@ def Updatecoords(particle, fieldset, time): particle.depth_nextloop = particle.depth + particle_ddepth # noqa particle.time_nextloop = particle.time + particle.dt - self._pyfunc = self.__radd__(Setcoords).__add__(Updatecoords)._pyfunc + self._pyfunc = (Setcoords + self + Updatecoords)._pyfunc def check_fieldsets_in_kernels(self, pyfunc): """ @@ -396,13 +397,10 @@ def check_fieldsets_in_kernels(self, pyfunc): self.fieldset.add_constant("RK45_max_dt", 60 * 60 * 24) def check_kernel_signature_on_version(self): - numkernelargs = 0 - if self._pyfunc is not None: - if sys.version_info[0] < 3: - numkernelargs = len(inspect.getargspec(self._pyfunc).args) - else: - numkernelargs = len(inspect.getfullargspec(self._pyfunc).args) - return numkernelargs + """Returns number of arguments in a Python function.""" + if self._pyfunc is None: + return 0 + return len(inspect.getfullargspec(self._pyfunc).args) def remove_lib(self): if self._lib is not None: @@ -449,7 +447,7 @@ def get_kernel_compile_files(self): self._cache_key ) # only required here because loading is done by Kernel class instead of Compiler class dyn_dir = get_cache_dir() - basename = "%s_0" % cache_name + basename = f"{cache_name}_0" lib_path = "lib" + basename src_file_or_files = None if type(basename) in (list, dict, tuple, ndarray): diff --git a/parcels/particle.py b/parcels/particle.py index cd524275b..47ff8e082 100644 --- a/parcels/particle.py +++ b/parcels/particle.py @@ -1,5 +1,6 @@ from ctypes import c_void_p from operator import attrgetter +from typing import Literal import numpy as np @@ -27,7 +28,7 @@ class Variable: If to_write = 'once', the variable will be written as a time-independent 1D array """ - def __init__(self, name, dtype=np.float32, initial=0, to_write=True): + def __init__(self, name, dtype=np.float32, initial=0, to_write: bool | Literal["once"] = True): self.name = name self.dtype = dtype self.initial = initial @@ -39,13 +40,13 @@ def __get__(self, instance, cls): if issubclass(cls, JITParticle): return instance._cptr.__getitem__(self.name) else: - return getattr(instance, "_%s" % self.name, self.initial) + return getattr(instance, f"_{self.name}", self.initial) def __set__(self, instance, value): if isinstance(instance, JITParticle): instance._cptr.__setitem__(self.name, value) else: - setattr(instance, "_%s" % self.name, value) + setattr(instance, f"_{self.name}", value) def __repr__(self): return f"PVar<{self.name}|{self.dtype}>" diff --git a/parcels/particlefile.py b/parcels/particlefile.py index ee519b25d..db24bab1c 100644 --- a/parcels/particlefile.py +++ b/parcels/particlefile.py @@ -119,7 +119,7 @@ def __init__(self, name, particleset, outputdt=np.inf, chunks=None, create_new_z stacklevel=2, ) else: - self.fname = name if extension in [".zarr"] else "%s.zarr" % name + self.fname = name if extension in [".zarr"] else f"{name}.zarr" def _create_variables_attribute_dict(self): """Creates the dictionary with variable attributes. @@ -208,7 +208,7 @@ def write(self, pset, time, indices=None): if pset.particledata._ncount == 0: warnings.warn( - "ParticleSet is empty on writing as array at time %g" % time, + f"ParticleSet is empty on writing as array at time {time:g}", RuntimeWarning, stacklevel=2, ) diff --git a/parcels/particleset.py b/parcels/particleset.py index 6e0bd3d11..11007c93d 100644 --- a/parcels/particleset.py +++ b/parcels/particleset.py @@ -152,7 +152,7 @@ def ArrayClass_init(self, *args, **kwargs): lon = np.empty(shape=0) if lon is None else convert_to_flat_array(lon) lat = np.empty(shape=0) if lat is None else convert_to_flat_array(lat) - if isinstance(pid_orig, (type(None), type(False))): + if isinstance(pid_orig, (type(None), bool)): pid_orig = np.arange(lon.size) if depth is None: diff --git a/parcels/rng.py b/parcels/rng.py index 602e5c874..bac7fbce8 100644 --- a/parcels/rng.py +++ b/parcels/rng.py @@ -95,7 +95,7 @@ def remove_lib(self): def compile(self, compiler=None): if self.src_file is None or self.lib_file is None or self.log_file is None: - basename = "parcels_random_%s" % uuid.uuid4() + basename = f"parcels_random_{uuid.uuid4()}" lib_filename = "lib" + basename basepath = os.path.join(get_cache_dir(), f"{basename}") libpath = os.path.join(get_cache_dir(), f"{lib_filename}") diff --git a/parcels/tools/_helpers.py b/parcels/tools/_helpers.py index ffe583157..00893c040 100644 --- a/parcels/tools/_helpers.py +++ b/parcels/tools/_helpers.py @@ -2,7 +2,7 @@ import functools import warnings -from typing import Callable +from collections.abc import Callable PACKAGE = "Parcels" diff --git a/parcels/tools/converters.py b/parcels/tools/converters.py index de6705fac..3911bd676 100644 --- a/parcels/tools/converters.py +++ b/parcels/tools/converters.py @@ -131,7 +131,7 @@ def fulltime(self, time): raise RuntimeError(f"Calendar {self.calendar} not implemented in TimeConverter") def __repr__(self): - return "%s" % self.time_origin + return f"{self.time_origin}" def __eq__(self, other): other = other.time_origin if isinstance(other, TimeConverter) else other @@ -213,10 +213,10 @@ def to_source(self, value, x, y, z): return value * 1000.0 * 1.852 * 60.0 * cos(y * pi / 180) def ccode_to_target(self, x, y, z): - return "(1.0 / (1000. * 1.852 * 60. * cos(%s * M_PI / 180)))" % y + return f"(1.0 / (1000. * 1.852 * 60. * cos({y} * M_PI / 180)))" def ccode_to_source(self, x, y, z): - return "(1000. * 1.852 * 60. * cos(%s * M_PI / 180))" % y + return f"(1000. * 1.852 * 60. * cos({y} * M_PI / 180))" class GeographicSquare(UnitConverter): @@ -253,10 +253,10 @@ def to_source(self, value, x, y, z): return value * pow(1000.0 * 1.852 * 60.0 * cos(y * pi / 180), 2) def ccode_to_target(self, x, y, z): - return "pow(1.0 / (1000. * 1.852 * 60. * cos(%s * M_PI / 180)), 2)" % y + return f"pow(1.0 / (1000. * 1.852 * 60. * cos({y} * M_PI / 180)), 2)" def ccode_to_source(self, x, y, z): - return "pow((1000. * 1.852 * 60. * cos(%s * M_PI / 180)), 2)" % y + return f"pow((1000. * 1.852 * 60. * cos({y} * M_PI / 180)), 2)" unitconverters_map = { diff --git a/parcels/tools/exampledata_utils.py b/parcels/tools/exampledata_utils.py index 07e59e3a8..a57b2b4c3 100644 --- a/parcels/tools/exampledata_utils.py +++ b/parcels/tools/exampledata_utils.py @@ -1,7 +1,6 @@ import os from datetime import datetime, timedelta from pathlib import Path -from typing import List from urllib.request import urlretrieve import platformdirs @@ -96,7 +95,7 @@ def get_data_home(data_home=None): return data_home -def list_example_datasets() -> List[str]: +def list_example_datasets() -> list[str]: """List the available example datasets. Use :func:`download_example_dataset` to download one of the datasets. diff --git a/parcels/tools/global_statics.py b/parcels/tools/global_statics.py index 896d3195f..f575d4561 100644 --- a/parcels/tools/global_statics.py +++ b/parcels/tools/global_statics.py @@ -34,6 +34,6 @@ def get_package_dir(): def get_cache_dir(): - directory = os.path.join(gettempdir(), "parcels-%s" % getuid()) + directory = os.path.join(gettempdir(), f"parcels-{getuid()}") Path(directory).mkdir(exist_ok=True) return directory diff --git a/parcels/tools/interpolation_utils.py b/parcels/tools/interpolation_utils.py index 273dbbec8..f7455c6ec 100644 --- a/parcels/tools/interpolation_utils.py +++ b/parcels/tools/interpolation_utils.py @@ -1,4 +1,5 @@ -from typing import Callable, Literal +from collections.abc import Callable +from typing import Literal import numpy as np diff --git a/parcels/tools/timer.py b/parcels/tools/timer.py index 8896aa42a..311fd7a90 100644 --- a/parcels/tools/timer.py +++ b/parcels/tools/timer.py @@ -47,7 +47,7 @@ def print_tree_sequential(self, step=0, root_time=0, parent_time=0): print(" " * (step + 1), end="") if step > 0: print("(%3d%%) " % round(time / parent_time * 100), end="") - t_str = "%1.3e s" % time if root_time < 300 else datetime.timedelta(seconds=time) + t_str = f"{time:1.3e} s" if root_time < 300 else datetime.timedelta(seconds=time) print(f"Timer {(self._name).ljust(20 - 2*step + 7*(step == 0))}: {t_str}") for child in self._children: child.print_tree_sequential(step + 1, root_time, time) diff --git a/pyproject.toml b/pyproject.toml index 134a6f8ff..0d5941351 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ select = [ "F", # pyflakes "I", # isort "B", # Bugbear - # "UP", # pyupgrade + "UP", # pyupgrade "LOG", # logging "ICN", # import conventions "G", # logging-format diff --git a/tests/test_advection.py b/tests/test_advection.py index 0ba7b5639..efc53e5b4 100644 --- a/tests/test_advection.py +++ b/tests/test_advection.py @@ -580,7 +580,7 @@ def test_uniform_analytical(mode, u, v, w, direction, tmpdir): pset.execute(AdvectionAnalytical, runtime=4, dt=direction, output_file=outfile) assert np.abs(pset.lon - x0 - pset.time * u) < 1e-6 assert np.abs(pset.lat - y0 - pset.time * v) < 1e-6 - if w: + if w is not None: assert np.abs(pset.depth - z0 - pset.time * w) < 1e-4 ds = xr.open_zarr(outfile_path) diff --git a/tests/test_kernel_execution.py b/tests/test_kernel_execution.py index a16702c55..3b003f4c5 100644 --- a/tests/test_kernel_execution.py +++ b/tests/test_kernel_execution.py @@ -420,3 +420,25 @@ def nudge_kernel(particle, fieldset, time): "Parcels uses function name to determine kernel support. " "Aliasing ParcelsRandom to another name is not supported." ) + + +def test_outdated_kernel(fieldset): + """ + Make sure that if users try using a kernel from pre Parcels 2.0 they get an error. + + Prevents users from copy-pasting old kernels that are no longer supported. + """ + pset = ParticleSet(fieldset, pclass=JITParticle, lon=[0.5], lat=[0.5]) + + def outdated_kernel(particle, fieldset, time, dt): + particle.lon += 0.1 + + with pytest.raises(ValueError) as e: + pset.Kernel(outdated_kernel) + + assert "Since Parcels v2.0" in str(e.value) + + with pytest.raises(ValueError) as e: + pset.execute(outdated_kernel, endtime=1.0, dt=1.0) + + assert "Since Parcels v2.0" in str(e.value) diff --git a/tests/test_kernel_language.py b/tests/test_kernel_language.py index 19ca2289a..ec3a20da0 100644 --- a/tests/test_kernel_language.py +++ b/tests/test_kernel_language.py @@ -277,7 +277,7 @@ def kernel(particle, fieldset, time): def kernel2(particle, fieldset, time): tmp = 3 - print("%f" % (tmp)) + print(f"{tmp:f}") pset.execute(kernel2, endtime=2.0, dt=1.0, verbose_progress=False) out, err = capfd.readouterr() diff --git a/tests/test_reprs.py b/tests/test_reprs.py new file mode 100644 index 000000000..d072ded9f --- /dev/null +++ b/tests/test_reprs.py @@ -0,0 +1,37 @@ +from typing import Any + +import numpy as np + +from parcels import Grid, TimeConverter +from parcels.grid import RectilinearGrid + + +def validate_simple_repr(class_: type, kwargs: dict[str, Any]): + """Test that the repr of an object contains all the arguments. This only works for objects where the repr matches the calling signature.""" + obj = class_(**kwargs) + obj_repr = repr(obj) + + for param in kwargs.keys(): + assert param in obj_repr + # skip `assert repr(value) in obj_repr` as this is not always true if init does processing on the value + assert class_.__name__ in obj_repr + + +def test_grid_repr(): + """Test arguments are in the repr of a Grid object""" + kwargs = dict( + lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, time_origin=TimeConverter(), mesh="spherical" + ) + validate_simple_repr(Grid, kwargs) + + +def test_rectilineargrid_repr(): + """ + Test arguments are in the repr of a RectilinearGrid object. + + Mainly to test inherited repr is correct. + """ + kwargs = dict( + lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, time_origin=TimeConverter(), mesh="spherical" + ) + validate_simple_repr(RectilinearGrid, kwargs)