From acecddae70b9f7e823f50d328439dd9b1c5b83b0 Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 11:02:56 -0600 Subject: [PATCH 1/7] Remove ParameterSet.py (entirely commented-out dead code) --- pyPRMS/parameters/ParameterSet.py | 96 ------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 pyPRMS/parameters/ParameterSet.py diff --git a/pyPRMS/parameters/ParameterSet.py b/pyPRMS/parameters/ParameterSet.py deleted file mode 100644 index 61e89d1..0000000 --- a/pyPRMS/parameters/ParameterSet.py +++ /dev/null @@ -1,96 +0,0 @@ - -# class ParameterSet(object): - # def degenerate_parameters(self): - # """Print parameters that have fewer dimensions than specified in the master parameters.""" - # - # result = [] - # # TODO: 20230726 PAN - This is not needed with the current parameter code - # if self.__master_params is not None: - # for kk, vv in self.parameters.items(): - # try: - # if set(vv.dimensions.keys()) != set(self.__master_params[kk].dimensions.keys()): - # if not (set(self.__master_params[kk].dimensions.keys()).issubset(set(HRU_DIMS)) and - # set(vv.dimensions.keys()).issubset(HRU_DIMS)): - # result.append(kk) - # if self.verbose: - # print(f'Parameter, {kk}, is degenerate') - # print(' parameter: ', list(vv.dimensions.keys())) - # print(' master: ', list(self.__master_params[kk].dimensions.keys())) - # except ValueError: - # if self.verbose: - # print(f'ERROR: Parameter, {kk}, is not a valid PRMS parameter') - # return result - - # def expand_parameter(self, name: str): - # """Expand an existing parameter. - # - # Expand (e.g. reshape) a parameter, broadcasting existing value(s) into - # new shape specified by master parameters. The hru_deplcrv parameter has - # special handling to also update the snarea_curve parameter. - # - # :param name: name of parameter - # """ - # # TODO: 20230726 PAN - This is not needed with the current parameter code - # if self.__master_params is not None: - # # 1) make sure parameter exists - # if self.__master_params.exists(name): - # # 2) get dimensions from master parameters - # new_dims = self.__master_params.parameters[name].dimensions.copy() - # - # # The new_dims copy is no longer of type Dimensions, instead it - # # is an OrderedDict - # # 3) get dimension sizes from global dimensions object - # for kk, vv in new_dims.items(): - # vv.size = self.__dimensions[kk].size - # - # if self.verbose and set(new_dims.keys()) == set(self.__parameters[name].dimensions.keys()): - # print(f'Parameter, {name}, already has the maximum number of dimensions') - # print(' current: ', list(self.__parameters[name].dimensions.keys())) - # print(' requested: ', list(new_dims.keys())) - # - # # TODO: Write special case where hru_deplcrv is dimensioned nhru, but - # # the number of snarea_curve entries is less than nhru * 11. - # else: - # # 4) call reshape for the parameter - # self.__parameters[name].reshape(new_dims) - # - # if name == 'hru_deplcrv': - # # hru_deplcrv needs special handling - # # 2) get current value of hru_deplcrv, this is the snow_index to use - # # 3) replace broadcast original value with np.arange(1:nhru) - # orig_index = self.__parameters[name].data[0] - 1 - # new_indices = np.arange(1, new_dims['nhru'].size + 1) - # self.__parameters['hru_deplcrv'].data = new_indices - # - # # 5) get snarea_curve associated with original hru_deplcrv value - # curr_snarea_curve = self.__parameters['snarea_curve'].data.reshape((-1, 11))[orig_index, :] - # - # # 6) replace current snarea_curve values with broadcast of select snarea_curve*nhru - # new_snarea_curve = np.broadcast_to(curr_snarea_curve, (new_dims['nhru'].size, 11)) - # # 7) reset snarea_curve dimension size to nhru*11 - # self.__parameters['snarea_curve'].dimensions['ndeplval'].size = new_dims['nhru'].size * 11 - # self.__parameters['snarea_curve'].data = new_snarea_curve.flatten(order='C') - # - # if self.verbose: - # print('hru_deplcrv and snarea_curve have been expanded/updated') - - # def remove_by_global_id(self, hrus: Optional[List] = None, - # segs: Optional[List] = None): - # """Removes data-by-id (nhm_seg, nhm_id) from all parameters. - # - # :param hrus: List of HRU IDs to remove - # :param segs: List of segment IDs to remove - # """ - # self.__parameters.remove_by_global_id(hrus=hrus, segs=segs) - # - # # Adjust the global dimensions - # if segs is not None: - # self.__dimensions['nsegment'].size -= len(segs) - # - # if hrus is not None: - # self.__dimensions['nhru'].size -= len(hrus) - # - # if self.__dimensions.exists('nssr'): - # self.__dimensions['nssr'].size -= len(hrus) - # if self.__dimensions.exists('ngw'): - # self.__dimensions['ngw'].size -= len(hrus) From 0f5511a87b0a825df059a71f3b940a7977573167 Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 11:09:00 -0600 Subject: [PATCH 2/7] Modernize type hints across parameters directory - Add 'from __future__ import annotations' to all parameter files - Replace typing.Dict, List, Set, Optional, Union with built-in generics and X | Y syntax - Simplify super() calls to Python 3 style - Remove commented-out Rich console setup code - Fix verbose parameter types: Optional[bool] -> bool (None was never a meaningful value) --- pyPRMS/parameters/ParamDb.py | 7 +-- pyPRMS/parameters/Parameter.py | 43 +++++++-------- pyPRMS/parameters/ParameterFile.py | 21 +++----- pyPRMS/parameters/ParameterNetCDF.py | 6 +-- pyPRMS/parameters/Parameters.py | 81 ++++++++++++++-------------- 5 files changed, 75 insertions(+), 83 deletions(-) diff --git a/pyPRMS/parameters/ParamDb.py b/pyPRMS/parameters/ParamDb.py index 791d42a..76d780a 100644 --- a/pyPRMS/parameters/ParamDb.py +++ b/pyPRMS/parameters/ParamDb.py @@ -1,7 +1,8 @@ +from __future__ import annotations import os import pandas as pd # type: ignore -from typing import cast, Optional +from typing import cast from ..constants import PRMS_VERSION from ..Exceptions_custom import ParameterNotValidError @@ -17,7 +18,7 @@ class ParamDb(Parameters): def __init__(self, paramdb_dir: str, metadata, - verbose: Optional[bool] = False): + verbose: bool = False): """Initialize ParamDb object. This object handles the monolithic parameter database. @@ -26,7 +27,7 @@ def __init__(self, paramdb_dir: str, :param verbose: Output additional debug information """ - super(ParamDb, self).__init__(metadata=metadata, verbose=verbose) + super().__init__(metadata=metadata, verbose=verbose) global con con = get_console_instance() diff --git a/pyPRMS/parameters/Parameter.py b/pyPRMS/parameters/Parameter.py index 2a267cc..592b457 100644 --- a/pyPRMS/parameters/Parameter.py +++ b/pyPRMS/parameters/Parameter.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import functools import numpy as np import numpy.typing as npt import pandas as pd # type: ignore -from typing import Any, cast, Dict, List, NamedTuple, Optional, Union +from typing import Any, cast, NamedTuple import xml.etree.ElementTree as xmlET from ..base.console import get_console_instance @@ -10,15 +12,10 @@ from ..dimensions.Dimensions import ParamDimensions from ..Exceptions_custom import FixedDimensionError -# from rich.console import Console -# from rich import pretty -# -# pretty.install() -# con = Console(force_jupyter=False) con = None -ParamDataRawType = Union[npt.NDArray, np.int32, np.float32, np.float64, np.str_] -ParamDataType = Union[npt.NDArray, np.int32, np.float32, np.float64, np.str_, int, float, str] +ParamDataRawType = npt.NDArray | np.int32 | np.float32 | np.float64 | np.str_ +ParamDataType = npt.NDArray | np.int32 | np.float32 | np.float64 | np.str_ | int | float | str class Outliers(NamedTuple): @@ -29,10 +26,10 @@ class Outliers(NamedTuple): class Stats(NamedTuple): name: str - min: Optional[npt.DTypeLike] - max: Optional[npt.DTypeLike] - mean: Optional[npt.DTypeLike] - median: Optional[npt.DTypeLike] + min: npt.DTypeLike | None + max: npt.DTypeLike | None + mean: npt.DTypeLike | None + median: npt.DTypeLike | None class Parameter(object): @@ -44,10 +41,10 @@ class Parameter(object): # Container for a single parameter def __init__(self, name: str, - meta: Optional[Dict] = None, + meta: dict | None = None, global_dims=None, - strict: Optional[bool] = True, - verbose: Optional[bool] = False): + strict: bool = True, + verbose: bool = False): """ Initialize a parameter object. @@ -86,7 +83,7 @@ def __init__(self, name: str, # The meta must be supplied as an adhoc dictionary self.meta = meta - self.__data: Optional[ParamDataRawType] = None + self.__data: ParamDataRawType | None = None self.__modified = False def __str__(self) -> str: @@ -245,7 +242,7 @@ def dimensions(self) -> ParamDimensions: return self.__dimensions @property - def index_map(self) -> Union[Dict[Any, int], None]: + def index_map(self) -> dict[Any, int] | None: """Returns an ordered dictionary which maps data values of a 1D array to index positions. @@ -275,7 +272,7 @@ def modified(self) -> bool: return self.__modified @property - def modules(self) -> List[str]: + def modules(self) -> list[str]: """Returns the names of the PRMS modules that require the parameter. :returns: names of PRMS modules that require the parameter @@ -422,7 +419,7 @@ def outliers(self) -> Outliers: return Outliers(self.__name, values_under, values_over) - def remove_by_index(self, dim_name: str, indices: List[int]): + def remove_by_index(self, dim_name: str, indices: list[int]): """Remove columns (nhru or nsegment) from data array given a list of indices. :param dim_name: Name of dimension to reduce @@ -522,7 +519,7 @@ def subset_by_index(self, dim_name: str, indices): assert self.data_raw is not None # Needed so mypy doesn't fail on next line self.dimensions[dim_name].size = self.data_raw.shape[dim_idx] - def tolist(self) -> List[Union[int, float, str]]: + def tolist(self) -> list[int | float | str]: """Returns the parameter data as a list. :returns: Parameter data @@ -572,14 +569,14 @@ def tostructure(self) -> dict: 'data': self.tolist()} return param - def unique(self) -> Optional[npt.NDArray]: + def unique(self) -> npt.NDArray | None: """Create array of unique values from the parameter data. :returns: Array of unique values """ return np.unique(self.data_raw) - def update_element(self, index: int, value: Union[int, float, List[int], List[float]]): + def update_element(self, index: int, value: int | float | list[int] | list[float]): """Update single value or row of values (e.g. nhru by nmonths) for a given local zero-based index in the parameter data array. @@ -634,7 +631,7 @@ def update_element(self, index: int, value: Union[int, float, List[int], List[fl self.__data[index] = value # type: ignore self.__modified = True - def _value_index_1d(self, value: Union[int, float, str]) -> npt.NDArray: + def _value_index_1d(self, value: int | float | str) -> npt.NDArray: """Given a scalar value return the indices where there is a match. :param value: The value to find in the parameter data array diff --git a/pyPRMS/parameters/ParameterFile.py b/pyPRMS/parameters/ParameterFile.py index 2bba119..bf0aea6 100644 --- a/pyPRMS/parameters/ParameterFile.py +++ b/pyPRMS/parameters/ParameterFile.py @@ -1,7 +1,7 @@ -# from typing import Any, Union, Dict, List, OrderedDict as OrderedDictType, Set +from __future__ import annotations + import numpy as np -from typing import List, Optional, Set from ..Exceptions_custom import ParameterExistsError, ParameterNotValidError from .Parameters import Parameters @@ -9,11 +9,6 @@ from ..prms_helpers import get_file_iter from ..base.console import get_console_instance -# from rich.console import Console -# from rich import pretty -# -# pretty.install() -# con = Console() con = None class ParameterFile(Parameters): @@ -22,7 +17,7 @@ class ParameterFile(Parameters): def __init__(self, filename: str, metadata, - verbose: Optional[bool] = False): + verbose: bool = False): # verify: Optional[bool] = True): """Create the ParameterFile object. @@ -31,7 +26,7 @@ def __init__(self, filename: str, :param verify: whether to load the master parameters (default=True) """ - super(ParameterFile, self).__init__(metadata=metadata, verbose=verbose) + super().__init__(metadata=metadata, verbose=verbose) global con con = get_console_instance() @@ -40,7 +35,7 @@ def __init__(self, filename: str, # self.__header = None self.__isloaded = False - self.__updated_parameters: Set[str] = set() + self.__updated_parameters: set[str] = set() self.__verbose = verbose self.filename = filename @@ -62,12 +57,12 @@ def filename(self, name: str): self.__isloaded = False self.__filename = name - self.__header: List[str] = [] # Initialize the list of file headers + self.__header: list[str] = [] # Initialize the list of file headers self._read() @property - def headers(self) -> List[str]: + def headers(self) -> list[str]: """Get the headers from the parameter file. :returns: list of headers from parameter file @@ -76,7 +71,7 @@ def headers(self) -> List[str]: return self.__header @property - def updated_parameters(self) -> Set[str]: + def updated_parameters(self) -> set[str]: """Get list of parameters that had more than one entry in the parameter file. :returns: list of parameters diff --git a/pyPRMS/parameters/ParameterNetCDF.py b/pyPRMS/parameters/ParameterNetCDF.py index 41202d2..dfb7e5e 100644 --- a/pyPRMS/parameters/ParameterNetCDF.py +++ b/pyPRMS/parameters/ParameterNetCDF.py @@ -1,6 +1,6 @@ +from __future__ import annotations import xarray as xr -from typing import Optional from .Parameters import Parameters from ..base.console import get_console_instance @@ -16,7 +16,7 @@ class ParameterNetCDF(Parameters): def __init__(self, filename: str, metadata, - verbose: Optional[bool] = False): + verbose: bool = False): """Initialize ParamDb object. :param filename: Path the ParamDb netcdf file @@ -24,7 +24,7 @@ def __init__(self, :param verify: Verify parameters against master list """ - super(ParameterNetCDF, self).__init__(metadata=metadata, verbose=verbose) + super().__init__(metadata=metadata, verbose=verbose) global con con = get_console_instance() diff --git a/pyPRMS/parameters/Parameters.py b/pyPRMS/parameters/Parameters.py index 20a66fb..48b2def 100644 --- a/pyPRMS/parameters/Parameters.py +++ b/pyPRMS/parameters/Parameters.py @@ -1,3 +1,4 @@ +from __future__ import annotations import cartopy.crs as ccrs # type: ignore import gc @@ -13,11 +14,11 @@ import xml.etree.ElementTree as xmlET from collections import defaultdict -from collections.abc import KeysView +from collections.abc import KeysView, Sequence from functools import cached_property from packaging.version import Version from pathlib import Path -from typing import Any, Literal, Optional, Sequence, Union, Dict, List, Set, Tuple +from typing import Any, Literal, cast from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER # type: ignore from ..control.Control import Control @@ -30,8 +31,6 @@ NEW_PTYPE_TO_DTYPE, PRMS_VERSION, PRMS6_DEV_VERSION, PTYPE_TO_PRMS_TYPE, NHM_DATATYPES, PARAMETERS_XML, VAR_DELIM) -# from rich.console import Console -# from rich import pretty from rich.table import Table import os @@ -53,7 +52,7 @@ class Parameters(object): # Create date: 2017-05-01 def __init__(self, metadata: MetaDataType, - verbose: Optional[bool] = False): + verbose: bool = False): """Initialize the Parameters object. Create an ordered dictionary to contain pyPRMS.Parameter objects @@ -67,16 +66,16 @@ def __init__(self, metadata: MetaDataType, # con.print('Parameters: Console info: {}'.format(con)) self.__dimensions = Dimensions(metadata=metadata, verbose=verbose) - self.__parameters: Dict[str, Parameter] = dict() + self.__parameters: dict[str, Parameter] = dict() self.verbose = verbose - self.__control: Optional[Control] = None + self.__control: Control | None = None self.__hru_poly = None - self.__hru_shape_key: Optional[str] = None + self.__hru_shape_key: str | None = None self.__seg_poly = None - self.__seg_shape_key: Optional[str] = None - self.__seg_to_hru: Dict[int, List[int]] = dict() - self.__hru_to_seg: Dict[int, int] = dict() + self.__seg_shape_key: str | None = None + self.__seg_to_hru: dict[int, list[int]] = dict() + self.__hru_to_seg: dict[int, int] = dict() self.metadata = metadata['parameters'] self.prms_version = Version(metadata['info']['version']) @@ -116,7 +115,7 @@ def __str__(self) -> str: return outstr @property - def control(self) -> Optional[Control]: + def control(self) -> Control | None: """Get Control object. :returns: Control object @@ -169,7 +168,7 @@ def dimensions(self) -> Dimensions: return self.__dimensions @cached_property - def hru_to_seg(self) -> Dict[int, int]: + def hru_to_seg(self) -> dict[int, int]: """Returns an ordered dictionary mapping NHM HRU IDs to HRU NHM segment IDs. :returns: dictionary mapping nhm_id to hru_segment_nhm @@ -194,7 +193,7 @@ def missing_params(self) -> Set: return pset.difference(set(self.parameters.keys())) @property - def parameters(self) -> Dict[str, Parameter]: + def parameters(self) -> dict[str, Parameter]: """Returns an ordered dictionary of parameter objects. :returns: dictionary of Parameter objects @@ -203,7 +202,7 @@ def parameters(self) -> Dict[str, Parameter]: return self.__parameters @property - def poi_to_seg(self) -> Dict[str, int]: + def poi_to_seg(self) -> dict[str, int]: """Returns a dictionary mapping poi_id to local poi_seg. :returns: dictionary mapping poi_id to local poi_seg""" @@ -213,7 +212,7 @@ def poi_to_seg(self) -> Dict[str, int]: return dict(np.rec.fromarrays([gage_ids, gage_segments]).tolist()) # type: ignore @property - def poi_to_seg0(self) -> Dict[str, int]: + def poi_to_seg0(self) -> dict[str, int]: """Returns a dictionary mapping poi_id to local, zero-based poi_seg. :returns: dictionary mapping poi_id to local, zero-based poi_seg""" @@ -222,7 +221,7 @@ def poi_to_seg0(self) -> Dict[str, int]: return dict(np.rec.fromarrays([gage_ids, gage_segments]).tolist()) # type: ignore @cached_property - def seg_to_hru(self) -> Dict[int, List[int]]: + def seg_to_hru(self) -> dict[int, list[int]]: """Returns a dictionary mapping HRU global segment IDs to global HRU IDs. Segment keys equal to zero are for non-routed HRUs. Segment keys greater than @@ -369,7 +368,7 @@ def add_missing_parameters(self): if self.verbose: # pragma: no cover con.print(f'[bold]{cparam}[/] [gold3] parameter added with default value[/]') - def add_poi(self, addl_gages: Dict[str, int]): + def add_poi(self, addl_gages: dict[str, int]): """Add user-specified points of interest (POIs) to the model. :param addl_gages: Dictionary of user-specified POIs with POI ID as key and segment index as value @@ -636,7 +635,7 @@ def get_dataframe(self, name: str) -> pd.DataFrame: return param_data def get_subset(self, name: str, - global_ids: List[int]) -> ParamDataRawType: + global_ids: list[int]) -> ParamDataRawType: """Returns a subset for a parameter based on the global_ids (e.g. nhm_id, nhm_seg). :param name: Name of the parameter @@ -646,7 +645,7 @@ def get_subset(self, name: str, param = self.get(name) dim_set = set(param.dimensions.keys()).intersection({'nhru', 'nssr', 'ngw', 'nsegment', 'ndeplval'}) - id_index_map: Union[Dict[Any, int], None] = {} + id_index_map: dict[Any, int] | None = {} cdim = dim_set.pop() if cdim in ['nhru', 'nssr', 'ngw', 'ndeplval']: @@ -696,7 +695,7 @@ def get_subset(self, name: str, return np.take(param.data_raw, nhm_idx0, axis=0) # axis: 0 rows, 1 columns # return param.data_raw[tuple(nhm_idx0), ] - def outlier_ids(self, name: str) -> List[int]: + def outlier_ids(self, name: str) -> list[int]: """Returns list of HRU or segment IDs of invalid parameter values :param name: Name of the parameter @@ -781,9 +780,9 @@ def parameters_info(self) -> pd.DataFrame: return pd.DataFrame.from_records(out_list, columns=col_names) def plot(self, name: str, - output_dir: Optional[str] = None, - limits: Optional[Union[LimitOptions, List[float], Tuple[float, float]]] = 'absolute', - mask_defaults: Optional[str] = None, + output_dir: str | None = None, + limits: LimitOptions | list[float] | tuple[float, float] | None = 'absolute', + mask_defaults: str | None = None, **kwargs): # pragma: no cover """Plot a parameter. @@ -1024,7 +1023,7 @@ def plot(self, name: str, else: print('Non-plottable parameter') - def poi_upstream_hrus(self, poi: Union[str, List[str], KeysView]) -> Dict[str, List[int]]: + def poi_upstream_hrus(self, poi: str | list[str] | KeysView) -> dict[str, list[int]]: """Returns a dictionary of POI to upstream global HRU IDs. :param poi: POI ID or list of POI IDs @@ -1054,7 +1053,7 @@ def poi_upstream_hrus(self, poi: Union[str, List[str], KeysView]) -> Dict[str, L return poi_hrus - def poi_upstream_segments(self, poi: Union[str, List[str], KeysView]) -> Dict[str, List[int]]: + def poi_upstream_segments(self, poi: str | list[str] | KeysView) -> dict[str, list[int]]: """Returns a dictionary of POI to upstream global segment IDs. :param poi: POI ID or list of POI IDs @@ -1083,7 +1082,7 @@ def poi_upstream_segments(self, poi: Union[str, List[str], KeysView]) -> Dict[st return poi_segs - def remove(self, name: Union[str, Sequence[str], Set[str]]): + def remove(self, name: str | Sequence[str] | set[str]): """Delete one or more parameters if they exist. :param name: parameter or list of parameters to remove @@ -1099,7 +1098,7 @@ def remove(self, name: Union[str, Sequence[str], Set[str]]): if self.verbose: # pragma: no cover con.print(f'[bold]{cparam}[/] [gold3]parameter removed[/]') - def remove_poi(self, poi: Union[str, List[str]]): + def remove_poi(self, poi: str | list[str]): """Remove POIs by gage_id. :param poi: POI id to remove @@ -1142,7 +1141,7 @@ def remove_poi(self, poi: Union[str, List[str]]): self.dimensions.get('npoigages').size -= len(poi_del_indices) self.dimensions.get('nobs').size -= len(poi_del_indices) - def segment_upstream_hrus(self, segs: Union[int, List[int], KeysView, npt.NDArray]) -> Dict[int, List[int]]: + def segment_upstream_hrus(self, segs: int | list[int] | KeysView | npt.NDArray) -> dict[int, list[int]]: """Returns a dictionary of segment to upstream global HRU IDs. :param segs: Global segment ID or list of global segment IDs @@ -1172,7 +1171,7 @@ def segment_upstream_hrus(self, segs: Union[int, List[int], KeysView, npt.NDArra return seg_hrus - def segment_upstream_segments(self, segs: Union[int, List[int], KeysView, npt.NDArray]) -> Dict[int, List[int]]: + def segment_upstream_segments(self, segs: int | list[int] | KeysView | npt.NDArray) -> dict[int, list[int]]: """Returns a dictionary of global segment IDs to upstream global segment IDs. :param segs: global segment IDs or list of global segment IDs @@ -1202,8 +1201,8 @@ def segment_upstream_segments(self, segs: Union[int, List[int], KeysView, npt.ND return us_segs def shapefile_hrus(self, filename: str, - layer_name: Optional[str] = None, - shape_key: Optional[str] = None): # pragma: no cover + layer_name: str | None = None, + shape_key: str | None = None): # pragma: no cover """Read a shapefile or geodatabase that corresponds to HRUs. :param filename: name of shapefile or geodatabase @@ -1222,8 +1221,8 @@ def shapefile_hrus(self, filename: str, self.__hru_shape_key = shape_key def shapefile_segments(self, filename: str, - layer_name: Optional[str] = None, - shape_key: Optional[str] = None): # pragma: no cover + layer_name: str | None = None, + shape_key: str | None = None): # pragma: no cover """Read a shapefile or geodatabase that corresponds to stream segments. :param filename: name of shapefile or geodatabase @@ -1242,7 +1241,7 @@ def shapefile_segments(self, filename: str, self.__seg_shape_key = shape_key def stream_network(self, tosegment: str = 'tosegment_nhm', - seg_id: str = 'nhm_seg') -> Union[nx.DiGraph, None]: + seg_id: str = 'nhm_seg') -> nx.DiGraph | None: """Create Directed, Acyclic Graph (DAG) of stream network. :param tosegment: name of parameter to use for HRU tosegment @@ -1266,7 +1265,7 @@ def stream_network(self, tosegment: str = 'tosegment_nhm', def update_element(self, name: str, id1: int, - value: Union[int, float, List[int], List[float]]): + value: int | float | list[int] | list[float]): """Update single value or row of values (e.g. nhru by nmonths) for a given nhm_id, nhm_seg, or 0 (for scalars). @@ -1334,8 +1333,8 @@ def write_paramdb(self, output_dir: str): with open(f'{output_dir}/{xx.name}.csv', 'w') as ff: ff.write(xx.toparamdb()) - def write_parameter_file(self, filename: Union[str, Path], - header: Optional[List[str]] = None): + def write_parameter_file(self, filename: str | Path, + header: list[str] | None = None): """Write a PRMS parameter file. :param filename: name of parameter file @@ -1442,7 +1441,7 @@ def write_parameter_file(self, filename: Union[str, Path], outfile.close() - def write_parameter_netcdf(self, filename: Union[str, Path]): + def write_parameter_netcdf(self, filename: str | Path): """Write parameters to a netcdf format file. :param filename: full path for output file @@ -1641,7 +1640,7 @@ def _trim_req_params(self, param_set: Set) -> Set: return param_set def _upstream_hrus(self, streamnet: nx.DiGraph, - dsmost_seg: List[int]) -> List[int]: + dsmost_seg: list[int]) -> list[int]: """Get list of HRUs that contribute to the given stream segments. :param streamnet: Directed, Acyclic Graph (DAG) of stream network @@ -1673,7 +1672,7 @@ def _upstream_hrus(self, streamnet: nx.DiGraph, return final_hru_list def _upstream_segments(self, streamnet: nx.DiGraph, - dsmost_seg: List[int]) -> List[int]: + dsmost_seg: list[int]) -> list[int]: """Get list of segments that contribute to the given stream segments. :param streamnet: Directed, Acyclic Graph (DAG) of stream network From ab26b42e1bc14c71977492c42ba37d0c24d4bb70 Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 11:12:01 -0600 Subject: [PATCH 3/7] Replace __getattr__ with explicit collection interface in Parameters - Remove __getattr__ delegation to internal __parameters dict - Add __contains__ for 'name in params' syntax - Add __iter__ for 'for name in params:' iteration - Add __len__ for len(params) - Update exists() to use __contains__ - Improves IDE support, prevents accidental dict mutation, and produces clear error messages on typos --- pyPRMS/parameters/Parameters.py | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pyPRMS/parameters/Parameters.py b/pyPRMS/parameters/Parameters.py index 48b2def..15da1ba 100644 --- a/pyPRMS/parameters/Parameters.py +++ b/pyPRMS/parameters/Parameters.py @@ -79,25 +79,37 @@ def __init__(self, metadata: MetaDataType, self.metadata = metadata['parameters'] self.prms_version = Version(metadata['info']['version']) - def __getattr__(self, name: str): - """Not sure what to write yet. + def __contains__(self, name: str) -> bool: + """Check if a parameter exists. - :param name: Name of the attribute + :param name: Name of the parameter + :returns: True if parameter exists, otherwise False """ - - # Undefined attributes will look up the given parameter - # return self.get(item) - # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html - if name == "__setstate__": - raise AttributeError(name) - return getattr(self.__parameters, name) + return name in self.__parameters def __getitem__(self, item): - """Not sure what to write yet. + """Get a parameter by name. + + :param item: Name of the parameter + :returns: Parameter object """ return self.get(item) + def __iter__(self): + """Iterate over parameter names. + + :returns: Iterator over parameter names + """ + return iter(self.__parameters) + + def __len__(self) -> int: + """Return number of parameters. + + :returns: Number of parameters + """ + return len(self.__parameters) + def __str__(self) -> str: """Pretty-print string representation of the Parameters object. @@ -571,7 +583,7 @@ def exists(self, name: str) -> bool: :returns: True if parameter exists, otherwise False """ - return name in self.parameters.keys() + return name in self def get(self, name: str) -> Parameter: """Returns a parameter object. From 418c29f9e2c0a19650c311fb71af983ec4549315 Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 11:57:09 -0600 Subject: [PATCH 4/7] Rename __parameters_to_dict to _parameters_to_dict in MetaData - Change from double-underscore (name-mangled private) to single-underscore (internal but accessible) since ParamDb already accesses it externally - Eliminates fragile name-mangled access pattern (mobj._MetaData__parameters_to_dict) in ParamDb.py - Method is now accessed as mobj._parameters_to_dict --- pyPRMS/metadata/metadata.py | 8 ++++---- pyPRMS/parameters/ParamDb.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyPRMS/metadata/metadata.py b/pyPRMS/metadata/metadata.py index a0fb444..e095115 100644 --- a/pyPRMS/metadata/metadata.py +++ b/pyPRMS/metadata/metadata.py @@ -43,7 +43,7 @@ def __init__(self, version: str | Version = PRMS_VERSION, fcn_map = {'control': self.__control_to_dict, 'dimensions': self.__dimensions_to_dict, - 'parameters': self.__parameters_to_dict, + 'parameters': self._parameters_to_dict, 'variables': self.__variables_to_dict, 'data_file': self.__data_file_to_dict, 'cbh': self.__cbh_to_dict} @@ -250,9 +250,9 @@ def __control_to_dict(self, xml_root: xmlET.Element, return meta_dict - def __parameters_to_dict(self, xml_root: xmlET.Element, - meta_type: str, - req_version: Version) -> dict: + def _parameters_to_dict(self, xml_root: xmlET.Element, + meta_type: str, + req_version: Version) -> dict: """Convert parameter metadata to dictionary. :param xml_root: XML root element diff --git a/pyPRMS/parameters/ParamDb.py b/pyPRMS/parameters/ParamDb.py index 76d780a..a9c3b53 100644 --- a/pyPRMS/parameters/ParamDb.py +++ b/pyPRMS/parameters/ParamDb.py @@ -60,9 +60,9 @@ def _read(self): # Create a MetaData object to use its parameter parsing function mobj = MetaData() - pvt_meta = mobj._MetaData__parameters_to_dict(xml_root=params_root, - meta_type='parameters', - req_version=PRMS_VERSION) + pvt_meta = mobj._parameters_to_dict(xml_root=params_root, + meta_type='parameters', + req_version=PRMS_VERSION) # Populate parameterSet with all available parameter names for param in params_root.findall('parameter'): From 003368777d704bdf132e1410026baa027245170e Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 12:05:55 -0600 Subject: [PATCH 5/7] Remove commented-out dead code from parameter files - Parameter.py: remove commented-out reshape method (~45 lines), size property, old namedtuple comments - Parameters.py: remove commented-out debug prints, old return statements, and alternative implementations - ParameterFile.py: remove commented-out verify parameter, old attribute initializations, and debug print --- pyPRMS/parameters/Parameter.py | 59 ------------------------------ pyPRMS/parameters/ParameterFile.py | 6 --- pyPRMS/parameters/Parameters.py | 15 -------- 3 files changed, 80 deletions(-) diff --git a/pyPRMS/parameters/Parameter.py b/pyPRMS/parameters/Parameter.py index 592b457..9648469 100644 --- a/pyPRMS/parameters/Parameter.py +++ b/pyPRMS/parameters/Parameter.py @@ -298,16 +298,6 @@ def ndim(self) -> int: else: return self.__dimensions.ndim - # @property - # def size(self) -> int: - # """Return the total size of the parameter for the defined dimensions. - # - # :returns total size of parameter dimensions""" - # arr_shp = [dd.size for dd in self.dimensions.dimensions.values()] - # - # # Compute the total size of the parameter - # return functools.reduce(lambda x, y: x * y, arr_shp) - @property def xml(self) -> xmlET.Element: """Return the xml metadata for the parameter as an xml Element. @@ -352,7 +342,6 @@ def check_values(self) -> bool: :returns: true when all values are within the valid min/max range for the parameter """ - # if self.__data is not None: minval = self.meta.get('minimum', None) maxval = self.meta.get('maximum', None) @@ -406,7 +395,6 @@ def outliers(self) -> Outliers: :returns: NamedTuple containing count of values less than and values greater than valid range """ - # Outliers = namedtuple('Outliers', ['name', 'under', 'over']) values_under = 0 values_over = 0 @@ -438,58 +426,11 @@ def remove_by_index(self, dim_name: str, indices: list[int]): else: raise TypeError('Parameter data is not initialized') - # def reshape(self, new_dims: Dict): - # """Reshape a parameter, broadcasting existing values as necessary. - # - # :param new_dims: Dimension names and sizes that will be used to reshape the parameter data - # """ - # - # if self.__data is None: - # # Reshape has no meaning if there is no data to reshape - # return - # - # if self.dimensions.ndim == 1: - # if 'one' in self.dimensions.keys(): - # # Reshaping from a scalar to a 1D or 2D array - # # print('Scalar to 1D or 2D') - # new_sizes = [vv.size for vv in new_dims.values()] - # tmp_data = np.broadcast_to(self.__data, new_sizes) - # - # # Remove the original dimension - # self.dimensions.remove('one') - # - # # Add the new ones - # for kk, vv in new_dims.items(): - # self.dimensions.add(kk, vv.size) - # - # self.__data = tmp_data - # elif set(self.dimensions.keys()).issubset(set(new_dims.keys())): - # # Reschaping a 1D to a 2D - # if len(new_dims) == 1: - # print('ERROR: Cannot reshape from 1D array to 1D array') - # else: - # # print('1D array to 2D array') - # new_sizes = [vv.size for vv in new_dims.values()] - # try: - # tmp_data = np.broadcast_to(self.__data, new_sizes) - # except ValueError: - # # operands could not be broadcast together with remapped shapes - # tmp_data = np.broadcast_to(self.__data, new_sizes[::-1]).T - # - # old_dim = list(self.dimensions.keys())[0] - # self.dimensions.remove(old_dim) - # - # for kk, vv in new_dims.items(): - # self.dimensions.add(kk, vv.size) - # - # self.__data = tmp_data - def stats(self) -> Stats: """Returns basic statistics on parameter values. :returns: None (for strings or no data) or NamedTuple containing min, max, mean, and median of parameter values """ - # Stats = namedtuple('Stats', ['name', 'min', 'max', 'mean', 'median']) try: return Stats(self.__name, np.min(self.data_raw), np.max(self.data_raw), diff --git a/pyPRMS/parameters/ParameterFile.py b/pyPRMS/parameters/ParameterFile.py index bf0aea6..b0a67bb 100644 --- a/pyPRMS/parameters/ParameterFile.py +++ b/pyPRMS/parameters/ParameterFile.py @@ -18,7 +18,6 @@ class ParameterFile(Parameters): def __init__(self, filename: str, metadata, verbose: bool = False): - # verify: Optional[bool] = True): """Create the ParameterFile object. :param filename: name of parameter file @@ -31,9 +30,6 @@ def __init__(self, filename: str, global con con = get_console_instance() - # self.__filename = None - # self.__header = None - self.__isloaded = False self.__updated_parameters: set[str] = set() self.__verbose = verbose @@ -116,8 +112,6 @@ def _read(self): if line == VAR_DELIM: continue varname = line.split(' ')[0] - # if self.__verbose: # pragma: no cover - # print(f'{varname=}') # Add the parameter try: diff --git a/pyPRMS/parameters/Parameters.py b/pyPRMS/parameters/Parameters.py index 15da1ba..dcdad51 100644 --- a/pyPRMS/parameters/Parameters.py +++ b/pyPRMS/parameters/Parameters.py @@ -440,7 +440,6 @@ def adjust_bounded_parameters(self): cmeta = cparam.meta if cmeta.get('maximum') in list(self.dimensions.keys()): - # if isinstance(cmeta.get('maximum'), str): try: cmeta['maximum'] = self.dimensions.get(cmeta.get('maximum')).size @@ -484,7 +483,6 @@ def check(self): # pragma: no cover """Check all parameter variables for proper array size. """ - # for pp in self.__parameters.values(): for pk in sorted(list(self.parameters.keys())): pp = self.get(pk) @@ -505,8 +503,6 @@ def check(self): # pragma: no cover con.print(f' [dark_orange]WARNING[/]: Value(s) (range: {pp_stats.min}, {pp_stats.max}) outside ' + f'the valid range of ({valid_min}, {valid_max}); ' + f'under/over=({pp_outliers.under}, {pp_outliers.over})') - # print(f' WARNING: Value(s) (range: {pp.data.min()}, {pp.data.max()}) outside ' + - # f'the valid range of ({pp.minimum}, {pp.maximum})') elif valid_min == 'bounded': # TODO: Handling bounded parameters needs improvement con.print(f' [dark_orange]WARNING[/]: Bounded parameter value(s) ' @@ -592,7 +588,6 @@ def get(self, name: str) -> Parameter: :returns: Parameter object """ - # Return the given parameter if self.exists(name): return self.__parameters[name] @@ -673,7 +668,6 @@ def get_subset(self, name: str, if param.dimensions.ndim == 2: return np.take(param.data_raw, nhm_idx0, axis=0) # axis: 0 rows, 1 columns - # return param.data_raw[tuple(nhm_idx0), :] else: if name in ['hru_deplcrv', 'snarea_curve']: init_data = np.take(self.get('hru_deplcrv').data_raw, nhm_idx0, axis=0) @@ -687,7 +681,6 @@ def get_subset(self, name: str, # Create new hru_deplcrv and renumber res = np.array([uniq_dict[xx] for xx in init_data]) - # return np.array([uniq_dict[xx] for xx in init_data]) case 'snarea_curve': uniq_deplcrv0 = [xx - 1 for xx in uniq_deplcrv] res = param.data_raw.reshape((-1, 11))[tuple(uniq_deplcrv0), :].reshape((-1)) @@ -705,7 +698,6 @@ def get_subset(self, name: str, else: # All other 1D arrays return np.take(param.data_raw, nhm_idx0, axis=0) # axis: 0 rows, 1 columns - # return param.data_raw[tuple(nhm_idx0), ] def outlier_ids(self, name: str) -> list[int]: """Returns list of HRU or segment IDs of invalid parameter values @@ -870,7 +862,6 @@ def plot(self, name: str, # Takes care of multipolygons that are in the NHM geodatabase/shapefile geoms_exploded = self.__hru_poly.explode(index_parts=True).reset_index(level=1, drop=True) - # print('Writing first plot') df_mrg = geoms_exploded.merge(param_data, left_on=self.__hru_shape_key, right_index=True, how='left') @@ -945,7 +936,6 @@ def plot(self, name: str, for tt in range(1, 12): # Months 2 through 12 - # print(f' Index: {tt}') param_data = self.get_dataframe(name).iloc[:, tt].to_frame(name=name) if mask_defaults is not None: @@ -1134,9 +1124,6 @@ def remove_poi(self, poi: str | list[str]): poi_parameters = ['poi_gage_id', 'poi_gage_segment', 'poi_type'] - # print(f'POIs to delete: {poi}') - # print(f'Current POIs: {poi_ids}') - # print(f'Size of poi_del_indices: {poi_del_indices.size}') if len(poi_del_indices) > 0: if self.get('poi_gage_id').dimensions.get('npoigages').size == len(poi_del_indices): # We're trying to remove all the POIs @@ -1505,9 +1492,7 @@ def write_parameter_netcdf(self, filename: str | Path): else: # String parameter # Get the maximum string length in the array of data - # print('String parameter: {}'.format(vv.name)) str_size = len(max(vv.data, key=len)) - # print('size: {}'.format(str_size)) # Create a dimension for the string length nc_hdl.createDimension(vv.name + '_nchars', str_size) From 1819096286a9aa7666323a53ea09e7e5486e60ba Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 12:14:03 -0600 Subject: [PATCH 6/7] Minor improvements to Parameter.py - Add __repr__ method for debugging - Add -> bool return type annotation to is_scalar property - Replace isinstance(indices, type(dict().values())) with isinstance(indices, ValuesView) from collections.abc --- pyPRMS/parameters/Parameter.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyPRMS/parameters/Parameter.py b/pyPRMS/parameters/Parameter.py index 9648469..7c96bb0 100644 --- a/pyPRMS/parameters/Parameter.py +++ b/pyPRMS/parameters/Parameter.py @@ -4,6 +4,7 @@ import numpy as np import numpy.typing as npt import pandas as pd # type: ignore +from collections.abc import ValuesView from typing import Any, cast, NamedTuple import xml.etree.ElementTree as xmlET @@ -86,6 +87,13 @@ def __init__(self, name: str, self.__data: ParamDataRawType | None = None self.__modified = False + def __repr__(self) -> str: + """String representation of the Parameter object. + + :return: string with parameter name and dimensions + """ + return f"Parameter(name='{self.name}')" + def __str__(self) -> str: """Pretty-print string representation of the parameter information. @@ -257,7 +265,7 @@ def index_map(self) -> dict[Any, int] | None: return None @property - def is_scalar(self): + def is_scalar(self) -> bool: try: return 'one' in self.meta['dimensions'] except KeyError: @@ -413,7 +421,7 @@ def remove_by_index(self, dim_name: str, indices: list[int]): :param dim_name: Name of dimension to reduce :param indices: List of indices to remove""" - if isinstance(indices, type(dict().values())): + if isinstance(indices, ValuesView): indices = list(indices) if self.__data is not None: @@ -445,7 +453,7 @@ def subset_by_index(self, dim_name: str, indices): :param dim_name: name of dimension :param indices: local indices of HRUs or segments to extract""" - if isinstance(indices, type(dict().values())): + if isinstance(indices, ValuesView): indices = list(indices) if self.dimensions[dim_name].is_fixed: From 2e6c91ca5971cec17753e7ab13d57f323e465d20 Mon Sep 17 00:00:00 2001 From: Parker Norton Date: Mon, 11 May 2026 12:17:21 -0600 Subject: [PATCH 7/7] Protect parameters property with MappingProxyType - Return a read-only MappingProxyType view from the parameters property instead of the raw internal dict - Prevents external code from bypassing add()/remove() by directly mutating the dict - All read operations (keys, values, items, [], in, iteration) continue to work unchanged --- pyPRMS/parameters/Parameters.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyPRMS/parameters/Parameters.py b/pyPRMS/parameters/Parameters.py index dcdad51..f9859b2 100644 --- a/pyPRMS/parameters/Parameters.py +++ b/pyPRMS/parameters/Parameters.py @@ -18,6 +18,7 @@ from functools import cached_property from packaging.version import Version from pathlib import Path +from types import MappingProxyType from typing import Any, Literal, cast from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER # type: ignore @@ -205,13 +206,13 @@ def missing_params(self) -> Set: return pset.difference(set(self.parameters.keys())) @property - def parameters(self) -> dict[str, Parameter]: - """Returns an ordered dictionary of parameter objects. + def parameters(self) -> MappingProxyType[str, Parameter]: + """Returns a read-only view of parameter objects. - :returns: dictionary of Parameter objects + :returns: Read-only mapping of parameter names to Parameter objects """ - return self.__parameters + return MappingProxyType(self.__parameters) @property def poi_to_seg(self) -> dict[str, int]: