From bf21ef6ffb7bcfdd96965b5b0254d68c7ebd2335 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 May 2026 15:32:19 -0600 Subject: [PATCH 1/3] make compatible with pandas 3.0, tests to confirm compatibility with pandas 3.0 and 2.x --- pyproject.toml | 11 ++++++++--- src/dispatch/model.py | 31 +++++++++++++++++++++++-------- tests/helper_test.py | 6 +++--- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf43c625..c217b2d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "numba >= 0.65.0,< 0.66", "numexpr >= 2.8, < 2.14.2", "numpy >= 1.18.5,< 3", - "pandas >= 1.4,< 3.0", + "pandas >= 1.4,< 4.0", "pandera >= 0.20.1, < 0.31", "pyarrow>=7, <25", ] @@ -176,7 +176,7 @@ pydocstyle.convention = "google" "tests/engine_test.py" = ["E501"] [tool.tox] -env_list = [ "linters", "docs", "prep", "3.13", "3.14", "coverage" ] +env_list = [ "linters", "docs", "prep", "3.13", "3.14", "3.14-pandas2", "coverage" ] [tool.tox.env.prep] skip_install = true @@ -188,11 +188,16 @@ allowlist_externals = [ "bash", "ruff", "prek" ] runner = "uv-venv-runner" extras = [ "dev", "viz" ] commands = [ + [ "python", "-c", 'import importlib.metadata as m; [print(f"{d.name}=={d.version}") for d in sorted(m.distributions(), key=lambda x: (x.name or "").lower())]' ], [ "coverage", "run", "--source={envsitepackagesdir}/dispatch", "-m", "pytest", "--doctest-modules", "{envsitepackagesdir}/dispatch", "tests" ], ] +[tool.tox.env."3.14-pandas2"] +description = "Run tests on Python 3.14 against the pandas <3 line to verify back-compat." +deps = [ "pandas<3.0" ] + [tool.tox.env.coverage] -depends = [ "3.13", "3.14" ] +depends = [ "3.13", "3.14", "3.14-pandas2" ] skip_install = true deps = "coverage" commands = [ diff --git a/src/dispatch/model.py b/src/dispatch/model.py index 50cff4e5..27fea8be 100644 --- a/src/dispatch/model.py +++ b/src/dispatch/model.py @@ -10,6 +10,12 @@ import numpy as np import pandas as pd +from dispatch import __version__ +from dispatch.constants import COLOR_MAP, MTDF, PLOT_MAP +from dispatch.engine import dispatch_engine, dispatch_engine_auto +from dispatch.helpers import dispatch_key, zero_profiles_outside_operating_dates +from dispatch.metadata import LOAD_PROFILE_SCHEMA, Validator + try: import plotly.express as px from plotly.graph_objects import Figure @@ -21,6 +27,12 @@ Figure = Any PLOTLY_INSTALLED = False +# pandas 3.0 removed the ``dropna`` argument from ``DataFrame.stack`` because the +# new implementation never introduces NA rows. To keep one code path that works +# under both pandas <3 (old default stack behavior) and pandas >=3 (new default), +# we branch on the major version. +_PANDAS_MAJOR_VERSION = int(pd.__version__.split(".", 1)[0]) + __all__ = ["DispatchModel"] try: @@ -41,12 +53,6 @@ def to_file(self, *args, **kwargs) -> None: raise NotImplementedError("datazip is required for this functionality") -from dispatch import __version__ -from dispatch.constants import COLOR_MAP, MTDF, PLOT_MAP -from dispatch.engine import dispatch_engine, dispatch_engine_auto -from dispatch.helpers import dispatch_key, zero_profiles_outside_operating_dates -from dispatch.metadata import LOAD_PROFILE_SCHEMA, Validator - LOGGER = logging.getLogger(__name__) @@ -991,9 +997,18 @@ def strict_grouper( dropna = False if col_name is None: return out + if _PANDAS_MAJOR_VERSION >= 3: + # pandas >=3 dropped the ``dropna`` kwarg and never introduces NA + # rows in stack. Replicate the legacy ``dropna=True`` behavior by + # dropping after the fact; ``dropna=False`` is already the new + # default so it needs no extra work. + stacked = out.stack(level=out.columns.names) + if dropna: + stacked = stacked.dropna() + else: + stacked = out.stack(level=out.columns.names, dropna=dropna) return ( - out.stack(level=out.columns.names, dropna=dropna) - .reorder_levels(order=[*out.columns.names, "datetime"]) + stacked.reorder_levels(order=[*out.columns.names, "datetime"]) .to_frame(name=col_name) .sort_index() ) diff --git a/tests/helper_test.py b/tests/helper_test.py index ecf26734..ba165178 100644 --- a/tests/helper_test.py +++ b/tests/helper_test.py @@ -41,19 +41,19 @@ def zero_profiles_outside_operating_dates_slow( dt_idx = pd.concat( [profiles.index.to_series()] * profiles.shape[1], axis=1, - ).to_numpy(dtype=np.datetime64) + ).to_numpy(dtype="datetime64[ns]") return pd.DataFrame( ( ( dt_idx <= retirement_date.fillna(profiles.index.max()).to_numpy( - dtype=np.datetime64 + dtype="datetime64[ns]" ) ) & ( dt_idx >= operating_date.fillna(profiles.index.min()).to_numpy( - dtype=np.datetime64 + dtype="datetime64[ns]" ) ) ) From aed94d9e3302e534ecb3cfb361d907baadce3ad0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 May 2026 15:54:49 -0600 Subject: [PATCH 2/3] switch to hatch --- pyproject.toml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c217b2d9..b4a15380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=64", "setuptools_scm>=8"] -build-backend = "setuptools.build_meta" +requires = ["hatchling>=1.27", "hatch-vcs>=0.4"] +build-backend = "hatchling.build" [project] name = "rmi.dispatch" @@ -53,11 +53,17 @@ viz = [ "kaleido>0.2,<2", ] -[tool.setuptools.dynamic] -version = {attr = "dispatch._version.__version__"} +[tool.hatch.version] +source = "vcs" -[tool.setuptools_scm] -version_file = "src/dispatch/_version.py" +[tool.hatch.build.hooks.vcs] +version-file = "src/dispatch/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/dispatch"] + +[tool.hatch.build.targets.sdist] +include = ["src/dispatch", "README.rst", "LICENSE.txt"] [tool.uv] constraint-dependencies = ["kaleido!=0.2.1.post1"] From da0bbcdc860d42b26e847118835d0ba94fa720f1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 May 2026 16:07:48 -0600 Subject: [PATCH 3/3] change all package upper version limits to the next major version, unless major version is currently 0 --- pyproject.toml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b4a15380..76397407 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,36 +20,36 @@ classifiers = [ ] requires-python = ">=3.12,<3.15" dependencies = [ - "bottleneck >= 1.6.0,< 1.6.1", - "numba >= 0.65.0,< 0.66", - "numexpr >= 2.8, < 2.14.2", - "numpy >= 1.18.5,< 3", - "pandas >= 1.4,< 4.0", - "pandera >= 0.20.1, < 0.31", - "pyarrow>=7, <25", + "bottleneck>=1.6.0,<2", + "numba>=0.65.0,<0.66", + "numexpr>=2.8,<3", + "numpy>=1.18.5,<3", + "pandas>= 1.4,<4", + "pandera>=0.20.1,<0.31", + "pyarrow>=7,<25", ] [project.optional-dependencies] dev = [ - "coverage>=5.3,<7.15", # Lets us track what code is being tested - "pytest>=6.2,<9.1", # test framework + "coverage>=5.3,<8", # Lets us track what code is being tested + "pytest>=6.2,<10", # test framework "datazip", ] doc = [ - "doc8>=2.0.0,<2.1", + "doc8>=2.0.0,<3", "furo>=2025.12.19", - "rstcheck[sphinx,toml]>=5.0,<6.3", - "sphinx>=4,!=5.1.0,<9.1.1", - "sphinx-autoapi>=1.8,<3.9", - "sphinx-issues>=1.2,<6.1", - "sphinx-autodoc-typehints>1.19,<3.11.0", - "sphinxcontrib-mermaid>0.7,<2.1.0", + "rstcheck[sphinx,toml]>=5.0,<7", + "sphinx>=4,!=5.1.0,<10", + "sphinx-autoapi>=1.8,<4", + "sphinx-issues>=1.2,<7", + "sphinx-autodoc-typehints>1.19,<4", + "sphinxcontrib-mermaid>0.7,<3", ] qa = [] tooling = [] tests = [] viz = [ - "plotly>5.10,<6.8", + "plotly>5.10,<7", "kaleido>0.2,<2", ]