diff --git a/genno/core/attrseries.py b/genno/core/attrseries.py index 541a4846..7ece6097 100644 --- a/genno/core/attrseries.py +++ b/genno/core/attrseries.py @@ -186,7 +186,7 @@ def bfill(self, dim: Hashable, limit: Optional[int] = None): # if needed use _maybe_groupby() return self._replace( self.unstack(dim) - .fillna(method="bfill", axis=1, limit=limit) + .bfill(axis=1, limit=limit) .stack() .reorder_levels(self.dims), ) @@ -271,7 +271,7 @@ def ffill(self, dim: Hashable, limit: Optional[int] = None): # if needed use _maybe_groupby() return self._replace( self.unstack(dim) - .fillna(method="ffill", axis=1, limit=limit) + .ffill(axis=1, limit=limit) .stack() .reorder_levels(self.dims), ) diff --git a/genno/tests/compat/test_pyam.py b/genno/tests/compat/test_pyam.py index febe3014..72cd1dd4 100644 --- a/genno/tests/compat/test_pyam.py +++ b/genno/tests/compat/test_pyam.py @@ -14,6 +14,11 @@ # Skip this entire file if pyam is not installed pyam = pytest.importorskip("pyam", reason="pyam-iamc not installed") +# Warning emitted by pandas ≥ 2.1.0 with pyam 1.9.0 +pytestmark = pytest.mark.filterwarnings( + "ignore:.*unique with argument that is not.*:FutureWarning:pyam.core" +) + @pytest.fixture(scope="session") def scenario(): diff --git a/genno/util.py b/genno/util.py index 32dcb76c..72a06a35 100644 --- a/genno/util.py +++ b/genno/util.py @@ -3,6 +3,7 @@ from inspect import Parameter, signature from typing import Callable, Iterable, Mapping, MutableMapping, Tuple, Type, Union +import numpy as np import pandas as pd import pint from dask.core import literal @@ -70,6 +71,16 @@ def filter_concat_args(args): yield arg +def _invalid(unit: str, exc: Exception) -> Exception: + """Helper method to return an intelligible exception from :func:`parse_units`.""" + chars = "".join(filter("-?$".__contains__, unit)) + msg = f"unit {unit!r} cannot be parsed; contains invalid character(s) {chars!r}" + # Use the original class of `exc`, mapped in some cases + cls_map: Mapping[Type[Exception], Type[Exception]] = {TypeError: ValueError} + return_cls = cls_map.get(type(exc), type(exc)) + return return_cls(msg) + + def parse_units(data: Iterable, registry=None) -> pint.Unit: """Return a :class:`pint.Unit` for an iterable of strings. @@ -95,6 +106,12 @@ def parse_units(data: Iterable, registry=None) -> pint.Unit: """ registry = registry or pint.get_application_registry() + # Ensure a type that is accepted by pd.unique() + if isinstance(data, str): + data = np.array([data]) + elif not isinstance(data, (np.ndarray, pd.Index, pd.Series)): + data = np.array(data) + unit = pd.unique(data) if len(unit) > 1: @@ -106,15 +123,6 @@ def parse_units(data: Iterable, registry=None) -> pint.Unit: # `units_series` is length 0 → no data → dimensionless unit = registry.dimensionless - def invalid(unit: str, exc: Exception) -> Exception: - """Helper method to return an intelligible exception.""" - chars = "".join(filter("-?$".__contains__, unit)) - msg = f"unit {unit!r} cannot be parsed; contains invalid character(s) {chars!r}" - # Use the original class of `exc`, mapped in some cases - cls_map: Mapping[Type[Exception], Type[Exception]] = {TypeError: ValueError} - return_cls = cls_map.get(type(exc), type(exc)) - return return_cls(msg) - # Parse units try: return registry.Unit(unit) @@ -139,12 +147,12 @@ def invalid(unit: str, exc: Exception) -> Exception: return registry.Unit(unit) except PintError as e: # registry.define() failed somehow - raise invalid(unit, e) + raise _invalid(unit, e) except (AttributeError, TypeError) + PintError as e: # type: ignore [misc] # Unit contains a character like '-' that throws off pint # NB this 'except' clause must be *after* UndefinedUnitError, since that is a # subclass of AttributeError. - raise invalid(unit, e) + raise _invalid(unit, e) def partial_split(func: Callable, kwargs: Mapping) -> Tuple[Callable, MutableMapping]: