From fb83b919ee7664bebd5bf08368beeb0109e44812 Mon Sep 17 00:00:00 2001 From: igerber Date: Tue, 7 Apr 2026 17:14:40 -0400 Subject: [PATCH 1/3] =?UTF-8?q?Add=20Python=203.14=20support:=20upgrade=20?= =?UTF-8?q?PyO3=200.22=E2=86=920.28,=20update=20CI=20and=20publish=20matri?= =?UTF-8?q?ces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade PyO3 from 0.22 to 0.28 and rust-numpy from 0.22 to 0.28 to enable Python 3.14 compatibility. PyO3 0.22 cannot compile against 3.14 headers, and since maturin is the build backend, pip install fails entirely without this upgrade (the runtime fallback never gets a chance to activate). The migration is mechanical: the code already uses the modern Bound API, so only 14 to_pyarray_bound→to_pyarray renames were needed. ndarray bumped from 0.16 to 0.17 as required by numpy 0.28. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/publish.yml | 6 +++--- .github/workflows/rust-test.yml | 4 ++-- pyproject.toml | 3 ++- rust/Cargo.toml | 8 ++++---- rust/src/bootstrap.rs | 2 +- rust/src/linalg.rs | 10 +++++----- rust/src/trop.rs | 6 +++--- rust/src/weights.rs | 10 +++++----- 8 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 39bc164f..e79e3b5b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,7 +36,7 @@ jobs: - name: Build wheels run: | expected=0 - for pyver in 39 310 311 312 313; do + for pyver in 39 310 311 312 313 314; do pybin="/opt/python/cp${pyver}-cp${pyver}/bin/python" if [ ! -f "$pybin" ]; then echo "ERROR: Expected Python interpreter not found: $pybin" @@ -65,7 +65,7 @@ jobs: runs-on: macos-14 strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v4 @@ -95,7 +95,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index d22c1d85..60c76e26 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -81,7 +81,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-24.04-arm] - python-version: ['3.11', '3.13'] + python-version: ['3.11', '3.13', '3.14'] steps: - uses: actions/checkout@v4 @@ -184,7 +184,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.13' + python-version: '3.14' - name: Install dependencies # Keep in sync with pyproject.toml [project.dependencies] and [project.optional-dependencies.dev] diff --git a/pyproject.toml b/pyproject.toml index 192d0a96..38ac4704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "3.0.0" description = "Difference-in-Differences causal inference with sklearn-like API. Callaway-Sant'Anna, Synthetic DiD, Honest DiD, event studies, parallel trends." readme = "README.md" license = "MIT" -requires-python = ">=3.9,<3.14" +requires-python = ">=3.9,<3.15" authors = [ {name = "diff-diff contributors"} ] @@ -39,6 +39,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 26ee6ace..b82b2ebd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,10 +20,10 @@ accelerate = ["ndarray/blas", "dep:blas-src", "blas-src/accelerate"] openblas = ["ndarray/blas"] [dependencies] -# PyO3 0.22 supports Python 3.8-3.13 -pyo3 = "0.22" -numpy = "0.22" -ndarray = { version = "0.16", features = ["rayon"] } +# PyO3 0.28 supports Python 3.9-3.14 +pyo3 = "0.28" +numpy = "0.28" +ndarray = { version = "0.17", features = ["rayon"] } rand = "0.8" rand_xoshiro = "0.6" rayon = "1.8" diff --git a/rust/src/bootstrap.rs b/rust/src/bootstrap.rs index e6539286..71d5093f 100644 --- a/rust/src/bootstrap.rs +++ b/rust/src/bootstrap.rs @@ -48,7 +48,7 @@ pub fn generate_bootstrap_weights_batch<'py>( } }; - Ok(weights.to_pyarray_bound(py)) + Ok(weights.to_pyarray(py)) } /// Generate Rademacher weights: ±1 with equal probability. diff --git a/rust/src/linalg.rs b/rust/src/linalg.rs index 1260fa6b..8d4c5854 100644 --- a/rust/src/linalg.rs +++ b/rust/src/linalg.rs @@ -144,20 +144,20 @@ pub fn solve_ols<'py>( // Rank-deficient: cannot compute valid vcov, return NaN matrix let mut nan_vcov = Array2::::zeros((k, k)); nan_vcov.fill(f64::NAN); - Some(nan_vcov.to_pyarray_bound(py)) + Some(nan_vcov.to_pyarray(py)) } else { // Full rank: compute robust vcov normally let cluster_arr = cluster_ids.as_ref().map(|c| c.as_array().to_owned()); let vcov_arr = compute_robust_vcov_internal(&x_arr, &residuals.view(), cluster_arr.as_ref(), n, k)?; - Some(vcov_arr.to_pyarray_bound(py)) + Some(vcov_arr.to_pyarray(py)) } } else { None }; Ok(( - coefficients.to_pyarray_bound(py), - residuals.to_pyarray_bound(py), + coefficients.to_pyarray(py), + residuals.to_pyarray(py), vcov, )) } @@ -186,7 +186,7 @@ pub fn compute_robust_vcov<'py>( let n = x_arr.nrows(); let k = x_arr.ncols(); let vcov = compute_robust_vcov_internal(&x_arr, &residuals_arr, cluster_arr.as_ref(), n, k)?; - Ok(vcov.to_pyarray_bound(py)) + Ok(vcov.to_pyarray(py)) } /// Internal implementation of robust variance-covariance computation. diff --git a/rust/src/trop.rs b/rust/src/trop.rs index 605303a7..4257d4a8 100644 --- a/rust/src/trop.rs +++ b/rust/src/trop.rs @@ -44,7 +44,7 @@ pub fn compute_unit_distance_matrix<'py>( let dist_matrix = compute_unit_distance_matrix_internal(&y_arr, &d_arr); - Ok(dist_matrix.to_pyarray_bound(py)) + Ok(dist_matrix.to_pyarray(py)) } /// Internal implementation of unit distance matrix computation. @@ -1098,7 +1098,7 @@ pub fn bootstrap_trop_variance<'py>( }; let estimates_arr = Array1::from_vec(bootstrap_estimates); - Ok((estimates_arr.to_pyarray_bound(py), se)) + Ok((estimates_arr.to_pyarray(py), se)) } // ============================================================================ @@ -1838,7 +1838,7 @@ pub fn bootstrap_trop_variance_global<'py>( }; let estimates_arr = Array1::from_vec(bootstrap_estimates); - Ok((estimates_arr.to_pyarray_bound(py), se)) + Ok((estimates_arr.to_pyarray(py), se)) } #[cfg(test)] diff --git a/rust/src/weights.rs b/rust/src/weights.rs index 001b9b23..00c95b01 100644 --- a/rust/src/weights.rs +++ b/rust/src/weights.rs @@ -54,7 +54,7 @@ pub fn compute_synthetic_weights<'py>( let weights = compute_synthetic_weights_internal(&y_control_arr, &y_treated_arr, lambda_reg, max_iter, tol)?; - Ok(weights.to_pyarray_bound(py)) + Ok(weights.to_pyarray(py)) } /// Internal implementation of synthetic weight computation. @@ -137,7 +137,7 @@ pub fn project_simplex<'py>( ) -> PyResult>> { let v_arr = v.as_array(); let result = project_simplex_internal(&v_arr); - Ok(result.to_pyarray_bound(py)) + Ok(result.to_pyarray(py)) } /// Internal implementation of simplex projection. @@ -607,7 +607,7 @@ pub fn sc_weight_fw<'py>( min_decrease, max_iter, ); - Ok(result.to_pyarray_bound(py)) + Ok(result.to_pyarray(py)) } /// Compute SDID time weights via Frank-Wolfe optimization. @@ -637,7 +637,7 @@ pub fn compute_time_weights<'py>( let y_post = y_post_control.as_array(); let result = compute_time_weights_internal(&y_pre, &y_post, zeta_lambda, intercept, min_decrease, max_iter_pre_sparsify, max_iter); - Ok(result.to_pyarray_bound(py)) + Ok(result.to_pyarray(py)) } pub(crate) fn compute_time_weights_internal( @@ -720,7 +720,7 @@ pub fn compute_sdid_unit_weights<'py>( &y_pre, &y_tr_mean, zeta_omega, intercept, min_decrease, max_iter_pre_sparsify, max_iter, ); - Ok(result.to_pyarray_bound(py)) + Ok(result.to_pyarray(py)) } pub(crate) fn compute_sdid_unit_weights_internal( From e07b0c7f71f7c0f25d236249e24f9382672b479d Mon Sep 17 00:00:00 2001 From: igerber Date: Tue, 7 Apr 2026 17:29:00 -0400 Subject: [PATCH 2/3] Address review: add rust-version MSRV, update README support matrix - Add rust-version = "1.83" to Cargo.toml (PyO3 0.28 MSRV) so source builds fail early with an actionable error on older toolchains - Update README Requirements from "Python 3.9 - 3.13" to "3.9 - 3.14" Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- rust/Cargo.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 42cc866c..63793bd3 100644 --- a/README.md +++ b/README.md @@ -2769,7 +2769,7 @@ Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, ## Requirements -- Python 3.9 - 3.13 +- Python 3.9 - 3.14 - numpy >= 1.20 - pandas >= 1.3 - scipy >= 1.7 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b82b2ebd..5a428030 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,6 +2,7 @@ name = "diff_diff_rust" version = "3.0.0" edition = "2021" +rust-version = "1.83" description = "Rust backend for diff-diff DiD library" license = "MIT" From 30faedf4020727e7bca02b6453569c390492095e Mon Sep 17 00:00:00 2001 From: igerber Date: Tue, 7 Apr 2026 17:39:32 -0400 Subject: [PATCH 3/3] Bump rust-version to 1.84 to match faer 0.24 MSRV faer 0.24 requires Rust 1.84, which is higher than PyO3 0.28's 1.83 floor. Declare the actual minimum so source builds fail early. Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5a428030..67426d09 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,7 +2,7 @@ name = "diff_diff_rust" version = "3.0.0" edition = "2021" -rust-version = "1.83" +rust-version = "1.84" description = "Rust backend for diff-diff DiD library" license = "MIT"