diff --git a/dascore/compat.py b/dascore/compat.py index 49a033b77..4edbea0dd 100644 --- a/dascore/compat.py +++ b/dascore/compat.py @@ -9,6 +9,7 @@ import importlib from contextlib import suppress +from typing import TypeGuard import numpy as np from h5py import Dataset as H5Dataset @@ -94,7 +95,7 @@ def array(array): return _make_immutable(out) -def is_array(maybe_array): +def is_array(maybe_array) -> TypeGuard[np.ndarray]: """ Determine if an object is a numpy array. """ diff --git a/dascore/core/attrs.py b/dascore/core/attrs.py index ded934207..f80d9a7a3 100644 --- a/dascore/core/attrs.py +++ b/dascore/core/attrs.py @@ -136,8 +136,8 @@ def from_dict( return attr_map if attr_map is None: out = {} - elif hasattr(attr_map, "model_dump"): - out = attr_map.model_dump() + elif callable(model_dump := getattr(attr_map, "model_dump", None)): + out = model_dump() else: out = attr_map if isinstance(out, Mapping): diff --git a/dascore/core/coordmanager.py b/dascore/core/coordmanager.py index 4c43ac22d..a3de3797c 100644 --- a/dascore/core/coordmanager.py +++ b/dascore/core/coordmanager.py @@ -45,7 +45,7 @@ from collections.abc import Mapping, Sequence from itertools import zip_longest from types import EllipsisType -from typing import Annotated +from typing import Annotated, Any import numpy as np from pydantic import field_validator, model_validator @@ -300,9 +300,8 @@ def _divide_kwargs(kwargs): # update based on keywords for item, value in coord_updates.items(): coord_name, attr = item.split("_") - new = list(out[coord_name]) - new[1] = new[1].update(**{attr: value}) - out[coord_name] = tuple(new) + coord_dims, coord = out[coord_name] + out[coord_name] = (coord_dims, coord.update(**{attr: value})) dims = tuple(x for x in dims if x not in coord_to_drop) return get_coord_manager(out, dims=dims) @@ -1000,7 +999,7 @@ def _get_coord_dims_tuple(self): dim_map = self.dim_map return tuple((name, *dim_map[name]) for name in self.coord_map) - def _get_indexer(self, ind: int | None = None, value=None): + def _get_indexer(self, ind: int, value=None): """ Get an indexer for the appropriate data shape. @@ -1008,7 +1007,7 @@ def _get_indexer(self, ind: int | None = None, value=None): ind is a list of indices to substitute in values. """ - out = [slice(None, None) for _ in self.shape] + out: list[Any] = [slice(None, None) for _ in self.shape] out[ind] = value return tuple(out) diff --git a/dascore/core/coords.py b/dascore/core/coords.py index 45964d5de..e34d98e79 100644 --- a/dascore/core/coords.py +++ b/dascore/core/coords.py @@ -18,7 +18,7 @@ from functools import cache from operator import gt, lt from types import EllipsisType -from typing import Any, Literal +from typing import Any, Literal, overload import numpy as np import pandas as pd @@ -481,9 +481,15 @@ def valid_non_coord(coord1, coord2): coord2, slice2 = other.order(intersection) return coord1, coord2, slice1, slice2 + @overload + def __getitem__(self, item: int | np.integer) -> Any: ... + + @overload + def __getitem__(self, item: slice | np.ndarray) -> Self: ... + @abc.abstractmethod - def __getitem__(self, item) -> Self: - """Should implement slicing and return new instance.""" + def __getitem__(self, item): + """Index the coord; slices return a new coord, int indices a value.""" @cached_method def __len__(self): @@ -1139,17 +1145,17 @@ def reduce_coord(self, dim_reduce="empty"): new_coord = get_coord(shape=(1,), units=self.units, dtype=self.dtype) elif dim_reduce == "squeeze": return None - elif (func := _AGG_FUNCS.get(dim_reduce)) or callable(dim_reduce): - func = dim_reduce if callable(dim_reduce) else func + else: + func = dim_reduce if callable(dim_reduce) else _AGG_FUNCS.get(dim_reduce) + if func is None: + msg = "dim_reduce must be 'empty', 'squeeze' or valid aggregator." + raise ParameterError(msg) coord_data = self.data if dtype_time_like(coord_data): result = _reduce_time_like(func, coord_data) else: result = func(self.data) new_coord = self.update(data=result) - else: - msg = "dim_reduce must be 'empty', 'squeeze' or valid aggregator." - raise ParameterError(msg) return new_coord @@ -2258,6 +2264,7 @@ def select( kept.append(sub) if not kept: return self.empty(), slice(0, 0) + assert hi is not None # kept is non-empty, so the loop set hi new = self._rebuild(kept) start = None if lo == 0 else lo stop = None if hi >= len(self) else hi diff --git a/dascore/core/spool.py b/dascore/core/spool.py index 9c3f1695c..a63e7f86b 100644 --- a/dascore/core/spool.py +++ b/dascore/core/spool.py @@ -539,6 +539,7 @@ def __getitem__(self, item) -> PatchType | BaseSpool: def __iter__(self): # The catalog snapshots the relation once and skips patches which # cannot be resolved (see #583). + assert self._catalog is not None # __init__ always sets the catalog yield from self._catalog # --- selection and presentation specs ------------------------------- diff --git a/dascore/io/index/backend.py b/dascore/io/index/backend.py index e1874e991..9745e5752 100644 --- a/dascore/io/index/backend.py +++ b/dascore/io/index/backend.py @@ -886,7 +886,7 @@ def _flatten( if kinds == {"str"}: # flat-contract convention: missing strings are "" series = series.fillna("") - new_columns[name] = series + new_columns[str(name)] = series if cols_to_drop: out = out.drop(columns=cols_to_drop) # flat-contract names for source columns; path_attrs goes private diff --git a/dascore/io/index/indexer.py b/dascore/io/index/indexer.py index 74dc484e5..b8364580d 100644 --- a/dascore/io/index/indexer.py +++ b/dascore/io/index/indexer.py @@ -127,7 +127,7 @@ class DBDirectoryIndexer: def __init__( self, - path: str | Path, + path: str | Path | UPath, index_path: str | Path | None = None, ): path = UPath(path).absolute() if isinstance(path, UPath) else Path(path) diff --git a/dascore/io/index/planned.py b/dascore/io/index/planned.py index b7579791e..d9148c0e9 100644 --- a/dascore/io/index/planned.py +++ b/dascore/io/index/planned.py @@ -125,7 +125,9 @@ def _coord_record_from_row( units = None length = None if step is not None: - length = int(round((hi - lo) / step)) + 1 + # lo, hi, and step always share a time kind (or are all floats), but + # ty unions the branch types and rejects the mixed combinations. + length = int(round((hi - lo) / step)) + 1 # ty: ignore[unsupported-operator] key = row.get(f"_{name}_def_key") fingerprint = None if isinstance(key, str) and key.startswith("fp:"): diff --git a/dascore/io/segy/core.py b/dascore/io/segy/core.py index 8259dacac..d2cc0fbba 100644 --- a/dascore/io/segy/core.py +++ b/dascore/io/segy/core.py @@ -41,10 +41,10 @@ def read(self, path: LocalPath, time=None, channel=None, **kwargs): be implemented as well. """ segyio = optional_import(self._package_name) - path = str(path) - with segyio.open(path, ignore_geometry=True) as fi: + path_str = str(path) + with segyio.open(path_str, ignore_geometry=True) as fi: coords = _get_coords(fi) - attrs = _get_attrs(fi, coords, path, self, include_source=True) + attrs = _get_attrs(fi, coords, path_str, self, include_source=True) data, coords = _get_filtered_data_and_coords( fi, coords, time=time, channel=channel ) @@ -61,10 +61,10 @@ def scan(self, path: LocalPath, **kwargs) -> list[ScanPayload]: Returns lightweight scan metadata without loading the data array. """ segyio = optional_import(self._package_name) - path = str(path) - with segyio.open(path, ignore_geometry=True) as fi: + path_str = str(path) + with segyio.open(path_str, ignore_geometry=True) as fi: coords = _get_coords(fi) - attrs = _get_attrs(fi, coords, path, self) + attrs = _get_attrs(fi, coords, path_str, self) dtype = str(fi.dtype) return [ { diff --git a/dascore/io/sintela/utils.py b/dascore/io/sintela/utils.py index 28c1d3fdf..fea20e8e1 100644 --- a/dascore/io/sintela/utils.py +++ b/dascore/io/sintela/utils.py @@ -103,7 +103,9 @@ def _read_base_header(fid): """Return the first 3 elements of the sintela header.""" data = fid.read(base_header_dtypes.itemsize) array = np.frombuffer(data, dtype=base_header_dtypes, count=1) - out = {x: y for x, y in zip(array.dtype.names, array[0])} + names = array.dtype.names + assert names is not None # structured dtypes always have field names + out = {x: y for x, y in zip(names, array[0])} return out @@ -130,7 +132,9 @@ def _read_remaining_header(fid, base): dtype = _HEADER_DTYPES[version] data = fid.read(dtype.itemsize) buf = np.frombuffer(data, dtype=dtype, count=1) - header = {x: y for x, y in zip(buf.dtype.names, buf[0])} + names = buf.dtype.names + assert names is not None # structured dtypes always have field names + header = {x: y for x, y in zip(names, buf[0])} assert version == "3", "only 3 support for now," header["num_packets"] = _get_number_of_packets(fid, header, header_size) header["dtype"] = " BaseCoord: +) -> np.ndarray: """ Get an array associated with patch data or a coordinate. diff --git a/dascore/proc/filter.py b/dascore/proc/filter.py index 871998908..85b3878cf 100644 --- a/dascore/proc/filter.py +++ b/dascore/proc/filter.py @@ -596,7 +596,7 @@ def _maybe_transform_units(filt, dft_patch, freq_dims): filt = filt * dc.get_quantity(units) if not isinstance(filt, dc.units.Quantity): return filt - array, units = filt.magnitude, filt.units + array, units = np.asarray(filt.magnitude), filt.units coord_unit_1 = dft_patch.get_coord(freq_dims[-1]).units coord_unit_2 = dft_patch.get_coord(freq_dims[-2]).units if not (coord_unit_1 and coord_unit_2): diff --git a/dascore/proc/mute.py b/dascore/proc/mute.py index 6aaf8fa93..04bbbd221 100644 --- a/dascore/proc/mute.py +++ b/dascore/proc/mute.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping, Sized -from typing import ClassVar +from typing import Any, ClassVar import numpy as np from numpy.linalg import norm @@ -137,7 +137,7 @@ def from_params(cls, vals, dims, axes, patch, relative): def _apply_mask(self, array: NDArray, patch: dc.Patch, fill_value) -> NDArray: coord = patch.get_coord(self.dims[0]) _, c_index = coord.select(self.lims, relative=self.relative) - index = [slice(None)] * array.ndim + index: list[Any] = [slice(None)] * array.ndim index[self.axes[0]] = c_index array[tuple(index)] = fill_value return array diff --git a/dascore/transform/fbe.py b/dascore/transform/fbe.py index 401bfbd34..2b9bd9598 100644 --- a/dascore/transform/fbe.py +++ b/dascore/transform/fbe.py @@ -8,7 +8,6 @@ from dascore.units import get_filter_units from dascore.utils.misc import check_filter_kwargs, check_filter_range from dascore.utils.patch import get_dim_sampling_rate, patch_function -from dascore.utils.time import to_float @patch_function() @@ -84,7 +83,7 @@ def fbe( check_filter_range(nyquist, low, high, filt_min, filt_max) if step is None: - step = to_float(1 / sample_rate) + step = 1 / sample_rate patch = patch.pass_filter(**kwargs) diff --git a/dascore/transform/fourier.py b/dascore/transform/fourier.py index d8a8a7448..3c4579d32 100644 --- a/dascore/transform/fourier.py +++ b/dascore/transform/fourier.py @@ -282,11 +282,11 @@ def dft( >>> # calculate a power spectral density along time >>> psd = patch.dft(dim="time", real=True, output="PSD") """ - output = output.upper() - if output not in DFT_OUTPUT_TYPES: + output_type = output.upper() + if output_type not in DFT_OUTPUT_TYPES: msg = f"Unknown output={output!r}. Expected one of: {DFT_OUTPUT_TYPES}." raise ValueError(msg) - if output == "FFT" and db: + if output_type == "FFT" and db: msg = "db=True is only supported for output='AS', 'PS', or 'PSD'." raise ParameterError(msg) @@ -318,11 +318,13 @@ def dft( shift_slice = slice(None) if real is None else slice(None, -1) data = nft.fftshift(fft_data, axes=axes[shift_slice]) # get attributes - attrs = _get_dft_attrs(patch, dims, new_coords, pad=pad, output=output) + attrs = _get_dft_attrs(patch, dims, new_coords, pad=pad, output=output_type) patch_out = patch.new(data=data, coords=new_coords, attrs=attrs) - if output != "FFT": - patch_out = _convert_dft_spectral_amplitudes(patch_out, output, dims, real, db) + if output_type != "FFT": + patch_out = _convert_dft_spectral_amplitudes( + patch_out, output_type, dims, real, db + ) return patch_out diff --git a/dascore/units.py b/dascore/units.py index c041bdbcb..e6ae5d006 100644 --- a/dascore/units.py +++ b/dascore/units.py @@ -20,7 +20,6 @@ from dascore.utils.misc import _reinit_after_fork, iterate, unbyte from dascore.utils.time import dtype_time_like, is_datetime64, is_timedelta64, to_float -str_or_none = TypeVar("str_or_none", None, str) numeric = TypeVar("numeric", np.ndarray, int, float) @@ -114,7 +113,9 @@ def _str_to_quant(qunat_str): return ureg.Quantity(qunat_str) -def get_quantity(value: str_or_none) -> Quantity | None: +def get_quantity( + value: str | Quantity | Unit | np.datetime64 | np.timedelta64 | None, +) -> Quantity | None: """ Convert a value to a pint quantity. @@ -147,8 +148,9 @@ def get_quantity(value: str_or_none) -> Quantity | None: def get_factor_and_unit( - value: str_or_none, simplify: bool = False -) -> tuple[float, str_or_none]: + value: str | Quantity | Unit | np.datetime64 | np.timedelta64 | None, + simplify: bool = False, +) -> tuple[float, str | None]: """Convert a mixed unit/scaling factor to scale_factor and unit str.""" quant = get_quantity(value) if quant is None: @@ -171,7 +173,7 @@ def _get_conversion_factors(from_quant, to_quant) -> tuple[float, float, float]: def convert_units( - data: numeric, + data: numeric | Quantity, to_units: None | str | Quantity, from_units: None | str | Quantity = None, ) -> numeric: @@ -194,7 +196,7 @@ def convert_units( [time]) """ if isinstance(data, Quantity): # an existing quantity - from_units, data = data.units, data.magnitude + return convert_units(data.magnitude, to_units, data.units) to_units, from_units = get_quantity(to_units), get_quantity(from_units) if from_units is None: return data @@ -205,7 +207,9 @@ def convert_units( mult1, add, mult2 = _get_conversion_factors(from_units, to_units) except DimensionalityError as e: raise UnitError(str(e)) - return (data * mult1 + add) * mult2 + # ty cannot resolve `*` on the `numeric & ~Quantity` intersection left + # by the isinstance early return above. + return (data * mult1 + add) * mult2 # ty: ignore[unsupported-operator] def assert_dtype_compatible_with_units(dtype, quantity) -> Quantity: @@ -225,7 +229,7 @@ def assert_dtype_compatible_with_units(dtype, quantity) -> Quantity: return quant -def invert_quantity(unit: pint.Unit | str) -> pint.Unit | None: +def invert_quantity(unit: pint.Unit | str | Quantity | None) -> Quantity | None: """Invert a unit.""" # just get magnitude for isnull test to avoid warning of casting # quantity to array. @@ -233,6 +237,8 @@ def invert_quantity(unit: pint.Unit | str) -> pint.Unit | None: if pd.isnull(unit_test): return None quant = get_quantity(unit) + if quant is None: + return None return 1 / quant diff --git a/dascore/utils/chunk_plan.py b/dascore/utils/chunk_plan.py index ea4acdf01..dfc3f27b0 100644 --- a/dascore/utils/chunk_plan.py +++ b/dascore/utils/chunk_plan.py @@ -484,6 +484,7 @@ def build_chunk_plan( ) raise ParameterError(msg) if not merge_mode: + assert value is not None # pd.isnull(None) is True, so merge_mode covers it zero = to_timedelta64(0) if is_timedelta64(value) else 0 if value <= zero: msg = "Chunk value must be greater than 0." diff --git a/dascore/utils/jit.py b/dascore/utils/jit.py index b47a5f24c..dd8bef580 100644 --- a/dascore/utils/jit.py +++ b/dascore/utils/jit.py @@ -102,7 +102,9 @@ def decorated(*args, **kwargs): out_func = decorated else: out_func = numba.jit(**compiler_kwargs)(func) - out_func.func = func # make original func accessible via .func + # Make the original func accessible via .func; function objects accept + # new attributes even though their declared type does not. + out_func.func = func # ty: ignore[invalid-assignment] return out_func return _wrapper diff --git a/dascore/utils/mapping.py b/dascore/utils/mapping.py index 356b09fa8..170e9f46b 100644 --- a/dascore/utils/mapping.py +++ b/dascore/utils/mapping.py @@ -30,7 +30,7 @@ class FrozenDict(ABCMap[K, V]): """ def __init__(self, *args, **kwargs): - self._dict: dict[K, V] = dict(*args, **kwargs) + self._dict = dict(*args, **kwargs) self._hash = None def __getitem__(self, key: K) -> V: diff --git a/dascore/utils/misc.py b/dascore/utils/misc.py index 29909349a..b8a474380 100644 --- a/dascore/utils/misc.py +++ b/dascore/utils/misc.py @@ -51,7 +51,7 @@ def register_func(list_or_dict: list | dict, key=None): def wrapper(func): name = key or func.__name__ - if hasattr(list_or_dict, "append"): + if isinstance(list_or_dict, list): list_or_dict.append(name) else: list_or_dict[name] = func @@ -752,7 +752,7 @@ def _spool_map(spool, func, size=None, client=None, progress=True, **kwargs): # Now things get interesting. We need to split the spool here # so that patches don't get serialized. if size is None: - size = len(spool) / os.cpu_count() + size = len(spool) / (os.cpu_count() or 1) spools = list(spool.split(size=size)) # this is a hack to get the progress bar to work. Essentially, we just # add a secret flag to all but one spool so that progress bar is only diff --git a/dascore/utils/models.py b/dascore/utils/models.py index 58e431858..ee0458e26 100644 --- a/dascore/utils/models.py +++ b/dascore/utils/models.py @@ -36,8 +36,11 @@ PlainSerializer(to_str, when_used="json"), # getting undefined name ] +# The validator may preserve non-numpy array-likes (see compat.array), but +# ndarray is deliberately the single static face of array values; a structural +# protocol is not worth the complexity it spreads through every signature. ArrayLike = Annotated[ - object, + np.ndarray, PlainValidator(array), ] @@ -75,8 +78,8 @@ def sensible_model_equals( self: BaseModel | Mapping, other: BaseModel | Mapping ) -> bool: """Custom equality to not compare private attrs and handle numpy arrays.""" - d1 = self.model_dump() if hasattr(self, "model_dump") else self - d2 = other.model_dump() if hasattr(other, "model_dump") else other + d1 = self.model_dump() if isinstance(self, BaseModel) else self + d2 = other.model_dump() if isinstance(other, BaseModel) else other if not set(d1) == set(d2): # different keys, not equal return False for name in set(x for x in d1 if not x.startswith("_")): diff --git a/dascore/utils/moving.py b/dascore/utils/moving.py index 006a8f117..281084598 100644 --- a/dascore/utils/moving.py +++ b/dascore/utils/moving.py @@ -65,7 +65,9 @@ def _apply_scipy_operation( ddof: int = 0, ) -> np.ndarray: """Apply scipy operation with proper handling.""" - _, func_name = OPERATION_REGISTRY[operation]["scipy"] + spec = OPERATION_REGISTRY[operation]["scipy"] + assert spec is not None # callers check the registry before dispatching here + _, func_name = spec func = _get_engine_function("scipy", operation) scipy_kwargs = {"mode": mode, "cval": cval, "origin": origin} @@ -201,9 +203,11 @@ def _get_available_engines() -> tuple[str, ...]: return tuple(["scipy", *bottle_list]) -def _get_engine_function(engine: str, func_name: str) -> Callable | None: +def _get_engine_function(engine: str, func_name: str) -> Callable: """Get and cache engine function.""" - module_name, func_name = OPERATION_REGISTRY[func_name][engine] + spec = OPERATION_REGISTRY[func_name][engine] + assert spec is not None # callers check the registry before dispatching here + module_name, func_name = spec mod = _get_module(module_name) return getattr(mod, func_name) diff --git a/dascore/utils/patch.py b/dascore/utils/patch.py index 66c021224..a886dc95c 100644 --- a/dascore/utils/patch.py +++ b/dascore/utils/patch.py @@ -340,9 +340,9 @@ def patches_to_df( A dataframe with the attrs of each patch converted to a columns plus a field called 'patch' which contains a reference to the patches. """ - # Handle spool case - if hasattr(patches, "get_contents"): - df = patches.get_contents() + # Handle spool case (or anything else exposing spool-style get_contents) + if callable(get_contents := getattr(patches, "get_contents", None)): + df = get_contents() # get_contents() carries only metadata; embed the patches so the # flat-dump path can serve them (the "patch" column is the point). if "patch" not in df.columns: diff --git a/dascore/utils/patch_assembly.py b/dascore/utils/patch_assembly.py index 94b8814b8..3b94d0a07 100644 --- a/dascore/utils/patch_assembly.py +++ b/dascore/utils/patch_assembly.py @@ -191,6 +191,7 @@ def _merge_patches_streaming(self, joined, df_dict_list, merge_dim, samples): axis = patch.get_axis(merge_dim) elif patch.dims != dims: patch = patch.transpose(*dims) + assert axis is not None # set on the first pass through the loop data = patch.data if buffer is None: shape = list(data.shape) diff --git a/dascore/utils/paths.py b/dascore/utils/paths.py index fe7393dff..50b84c695 100644 --- a/dascore/utils/paths.py +++ b/dascore/utils/paths.py @@ -7,6 +7,8 @@ from pathlib import Path from urllib.parse import unquote +from typing_extensions import TypeIs + from dascore.compat import UPath from dascore.exceptions import InvalidSpoolError @@ -23,7 +25,7 @@ _MEMORY_SCHEMES = ("memorypatch://", "memory://") -def is_pathlike(resource) -> bool: +def is_pathlike(resource) -> TypeIs[str | Path | UPath]: """Return True if resource is supported path-like input.""" return isinstance(resource, str | Path | UPath) diff --git a/dascore/utils/pd.py b/dascore/utils/pd.py index bac6d5b59..239a93b88 100644 --- a/dascore/utils/pd.py +++ b/dascore/utils/pd.py @@ -4,7 +4,7 @@ import fnmatch from collections import defaultdict -from collections.abc import Collection, Mapping, Sequence +from collections.abc import Collection, Generator, Mapping, Sequence from functools import cache import numpy as np @@ -344,7 +344,7 @@ def get_interval_columns(df, name): return start, stop, step -def yield_range_tuple_from_kwargs(df, kwargs) -> tuple[str, slice]: +def yield_range_tuple_from_kwargs(df, kwargs) -> Generator[tuple[str, tuple]]: """ For each slice keyword, yield the name and a tuple of (start, stop). diff --git a/dascore/utils/progress.py b/dascore/utils/progress.py index 44014a99f..09488a82c 100644 --- a/dascore/utils/progress.py +++ b/dascore/utils/progress.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from collections.abc import Generator, Sized +from collections.abc import Iterable from contextlib import suppress import rich.progress as prog @@ -38,7 +38,7 @@ def get_progress_instance(progress: PROGRESS_LEVELS | Progress = "standard"): def track( - sequence: Sized | Generator, + sequence: Iterable, description: str, progress: PROGRESS_LEVELS | Progress = "standard", length: int | None = None, @@ -66,6 +66,8 @@ def track( guess_len = length if length is not None else 0 with suppress(TypeError, ValueError): length = len(sequence) if not guess_len else guess_len + if length is None: # unsized iterable with no length given; no progress bar + length = 0 if length < min_length: length = 0 # This is a dirty hack to allow debugging while running tests. diff --git a/pyproject.toml b/pyproject.toml index fd7d1ddbe..ee3344715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,10 +260,9 @@ line-ending = "lf" include = ["dascore"] # tests/ still has many diagnostics; expand scope later. # Rules with large pre-existing error counts, ignored until incrementally -# burned down. Counts at time of adoption: invalid-argument-type 210, -# unresolved-attribute 163, invalid-return-type 76, no-matching-overload 42, -# not-subscriptable 36, invalid-method-override 35, invalid-assignment 21, -# unsupported-operator 17, call-non-callable 11, not-iterable 10. +# burned down. Counts as of 2026-08-01: invalid-argument-type 179, +# unresolved-attribute 138, invalid-return-type 72, invalid-method-override 35, +# not-subscriptable 19, no-matching-overload 12. [tool.ty.rules] invalid-argument-type = "ignore" unresolved-attribute = "ignore" @@ -271,10 +270,6 @@ invalid-return-type = "ignore" no-matching-overload = "ignore" not-subscriptable = "ignore" invalid-method-override = "ignore" -invalid-assignment = "ignore" -unsupported-operator = "ignore" -call-non-callable = "ignore" -not-iterable = "ignore" # These files lazily import optional or untyped modules (xarray, numba, # h5py.h5r) that are not installed in the pre-commit hook's environment. diff --git a/tests/test_units.py b/tests/test_units.py index 0ef471279..43325e458 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -75,6 +75,10 @@ def test_invert(self): assert unit == reverted assert invert_quantity(None) is None + def test_invert_empty_string(self): + """An empty unit string has no quantity to invert; return None.""" + assert invert_quantity("") is None + class TestGetQuantStr: """Ensure units can be validated.""" diff --git a/tests/test_utils/test_progress.py b/tests/test_utils/test_progress.py index 1ab1e9f5d..efabbaa73 100644 --- a/tests/test_utils/test_progress.py +++ b/tests/test_utils/test_progress.py @@ -19,6 +19,11 @@ def test_progressbar_shows(self): for _ in track([1, 2, 3], "testing_tracker"): pass + def test_unsized_iterable(self): + """Unsized iterables without a length just skip the progress bar.""" + with config_context(debug=False): + assert list(track(iter([1, 2, 3]), "unsized_tracker")) == [1, 2, 3] + def test_get_basic_progress(self): """Ensure we can return a basic progress bar.""" pbar = get_progress_instance("basic")