From ea54e1e8d74253277279a1f32fc9adfaaf753aac Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:59:24 +0200 Subject: [PATCH 1/4] chore(tooling): add ty + coverage ratchet + docs-reference CI gate Roadmap #5/#6 tooling baseline for spotforecast2: - type checker: adopt Astral `ty` (sf2 had none); add [tool.ty]; run `ty check src/` non-blocking in CI. - coverage: add [tool.coverage] with fail_under=60 ratchet (~65% today), enforced by the existing pytest --cov run. - legacy lint: remove redundant .flake8 (not invoked; also dropped from runtime deps) + REUSE.toml entry. ([tool.ruff] already present.) - docs (#5): new "API Reference In Sync" CI job fails on quartodoc drift. Non-breaking, non-releasing (chore). Promotes to main with the next release. Co-Authored-By: Claude Opus 4.8 (1M context) --- .flake8 | 3 --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ REUSE.toml | 2 +- pyproject.toml | 18 +++++++++++++++++- uv.lock | 8 +++++--- 5 files changed, 55 insertions(+), 8 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6d0f5c9a..00000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 180 -extend-ignore = E203 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f556286..2c281468 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,11 @@ jobs: ruff check src/ tests/ continue-on-error: true + - name: Type check with ty (non-blocking) + run: | + ty check src/ + continue-on-error: true + security: name: Security Scan runs-on: ubuntu-latest @@ -148,3 +153,30 @@ jobs: sarif_file: bandit-report.sarif category: bandit continue-on-error: true + + docs-reference-sync: + name: API Reference In Sync + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Set up uv + uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5.2.2 + + - name: Regenerate the quartodoc API reference + run: | + uv run python docs/quartodoc_build.py + uv run quartodoc interlinks + + - name: Fail if committed docs/reference is stale + run: | + if ! git diff --exit-code -- docs/reference/; then + echo "::error::docs/reference is out of sync with quartodoc. Run 'uv run python docs/quartodoc_build.py && uv run quartodoc interlinks' locally and commit the result." + exit 1 + fi diff --git a/REUSE.toml b/REUSE.toml index 5a221609..37185d91 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -4,7 +4,7 @@ SPDX-PackageSupplier = "bartzbeielstein <32470350+bartzbeielstein@users.noreply. SPDX-PackageDownloadLocation = "https://github.com/sequential-parameter-optimization/spotforecast2" [[annotations]] -path = [".flake8", ".gitignore", ".python-version", ".releaserc.json", ".semantic-release-init", "mkdocs.yml", "package-lock.json", "package.json", "pyproject.toml", "uv.lock"] +path = [".gitignore", ".python-version", ".releaserc.json", ".semantic-release-init", "mkdocs.yml", "package-lock.json", "package.json", "pyproject.toml", "uv.lock"] precedence = "aggregate" SPDX-FileCopyrightText = "2026 bartzbeielstein" SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/pyproject.toml b/pyproject.toml index 91774878..24e6f980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ "astral>=3.2", "entsoe-py>=0.7.10", "feature-engine>=1.9.3", - "flake8>=7.3.0", "holidays>=0.90", "ipykernel>=7.1.0", "jinja2>=3.1", @@ -47,6 +46,7 @@ dev = [ "quartodoc>=0.7.2", "pytest>=9.0.2", "pytest-anyio>=0.0.0", + "ty>=0.0.29", ] [project.optional-dependencies] @@ -59,6 +59,7 @@ dev = [ "quartodoc>=0.7.2", "safety>=3.0.0", "bandit[sarif]>=1.8.0", + "ty>=0.0.29", ] [project.scripts] @@ -97,6 +98,21 @@ profile = "black" line-length = 88 target-version = "py313" +[tool.coverage.run] +branch = true +source = ["src/spotforecast2"] + +[tool.coverage.report] +# Ratchet: lock in the current ~65% coverage (measured locally) so it cannot +# regress; 60 leaves margin for platform variance on CI. Raise deliberately as +# coverage improves. CI runs pytest with --cov, enforcing this. +fail_under = 60 + +# Astral `ty` type checker. Run non-blocking in CI (continue-on-error) so it +# surfaces type debt without failing the build; tighten to a gate later. +[tool.ty.environment] +python-version = "3.13" + # [tool.uv.sources] # Local editable installs — use the sibling checkouts instead of PyPI: # spotforecast2-safe = { path = "../spotforecast2-safe", editable = true } diff --git a/uv.lock b/uv.lock index 256a9088..251c6df8 100644 --- a/uv.lock +++ b/uv.lock @@ -3604,13 +3604,12 @@ wheels = [ [[package]] name = "spotforecast2" -version = "5.2.0" +version = "6.0.0" source = { editable = "." } dependencies = [ { name = "astral" }, { name = "entsoe-py" }, { name = "feature-engine" }, - { name = "flake8" }, { name = "holidays" }, { name = "ipykernel" }, { name = "jinja2" }, @@ -3643,6 +3642,7 @@ dev = [ { name = "quartodoc" }, { name = "ruff" }, { name = "safety" }, + { name = "ty" }, ] [package.dev-dependencies] @@ -3651,6 +3651,7 @@ dev = [ { name = "pytest" }, { name = "pytest-anyio" }, { name = "quartodoc" }, + { name = "ty" }, ] [package.metadata] @@ -3660,7 +3661,6 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=24.1.0" }, { name = "entsoe-py", specifier = ">=0.7.10" }, { name = "feature-engine", specifier = ">=1.9.3" }, - { name = "flake8", specifier = ">=7.3.0" }, { name = "holidays", specifier = ">=0.90" }, { name = "ipykernel", specifier = ">=7.1.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, @@ -3686,6 +3686,7 @@ requires-dist = [ { name = "spotforecast2-safe", specifier = ">=19.0.0,<20" }, { name = "spotoptim", specifier = ">=0.12.9" }, { name = "tqdm", specifier = ">=4.67.2" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.29" }, ] provides-extras = ["dev"] @@ -3695,6 +3696,7 @@ dev = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-anyio", specifier = ">=0.0.0" }, { name = "quartodoc", specifier = ">=0.7.2" }, + { name = "ty", specifier = ">=0.0.29" }, ] [[package]] From 1855e30c10d6b9e20e08eb1f561ea06b420fe336 Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:13:19 +0200 Subject: [PATCH 2/4] test(cv): fix cv_block_size absent-fallback test for dataclass ConfigMulti MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spotforecast2-safe 19.1.0 made ConfigMulti a dataclass, so every field default is also a class attribute; deleting the instance attribute no longer makes hasattr(config, "cv_block_size") False, which broke test_steps_equal_predict_size_when_attr_absent. Wrap the config in a proxy that genuinely hides the attribute (mirroring an older/third-party PipelineConfig). cv_ts runtime is unaffected — it uses getattr(config, "cv_block_size", None) or predict_size, handling absent, None, and set identically. Also refresh uv.lock to safe 19.1.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_cv_block_size.py | 33 +++++++++++++++++++++++++++------ uv.lock | 6 +++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/tests/test_cv_block_size.py b/tests/test_cv_block_size.py index 4019b249..b2a58940 100644 --- a/tests/test_cv_block_size.py +++ b/tests/test_cv_block_size.py @@ -76,6 +76,27 @@ def _skl_cv( ) +class _HideAttr: + """Proxy delegating to a wrapped object but hiding one attribute. + + Recreates the "config never declared this field" path. Since + spotforecast2-safe >= 19.1 ConfigMulti is a dataclass, every field default + is also a *class* attribute, so deleting the instance attribute is no longer + enough to make ``hasattr`` / the ``getattr(..., default)`` fallback see it as + absent. This proxy makes the chosen attribute genuinely raise + ``AttributeError``, mirroring an older or third-party ``PipelineConfig``. + """ + + def __init__(self, wrapped: object, hidden: str) -> None: + self._wrapped = wrapped + self._hidden = hidden + + def __getattr__(self, name: str) -> object: + if name == self._hidden: + raise AttributeError(name) + return getattr(self._wrapped, name) + + # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @@ -126,12 +147,12 @@ class TestCvBlockSizeFallback: def test_steps_equal_predict_size_when_attr_absent(self, tmp_path, y_train): task = _make_task(tmp_path, predict_size=32) # Exercise the getattr-absent fallback in cv_ts: a config that does not - # declare cv_block_size at all must fall back to predict_size. Newer - # spotforecast2-safe ConfigMulti declares the field (default None), so - # delete it from the instance to recreate the "attribute absent" path - # the getattr fallback is written for. - if hasattr(task.config, "cv_block_size"): - delattr(task.config, "cv_block_size") + # declare cv_block_size at all must fall back to predict_size. The + # dataclass ConfigMulti (spotforecast2-safe >= 19.1) always exposes the + # field as a class attribute (default None), so deleting the instance + # attribute is not enough — wrap the config so the attribute is genuinely + # absent, mirroring an older / third-party PipelineConfig. + task.config = _HideAttr(task.config, "cv_block_size") assert not hasattr(task.config, "cv_block_size") assert task.cv_ts(y_train).steps == task.config.predict_size == 32 diff --git a/uv.lock b/uv.lock index 251c6df8..79ad22fc 100644 --- a/uv.lock +++ b/uv.lock @@ -3701,7 +3701,7 @@ dev = [ [[package]] name = "spotforecast2-safe" -version = "19.0.0" +version = "19.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astral" }, @@ -3719,9 +3719,9 @@ dependencies = [ { name = "statsmodels" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/ff/24638c47bcc50fae93efec970e0b60d9e6bc447be5888433162614139547/spotforecast2_safe-19.0.0.tar.gz", hash = "sha256:5399c94f8db4a833f05abefbbbf45d65edc594e195fe3b1f47b3833e787f57b2", size = 20621730, upload-time = "2026-06-07T19:08:50.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/22/10a45c89b8805c63eb5c0a24c6a8bc47a2eac6b5821de91daa493298edae/spotforecast2_safe-19.1.0.tar.gz", hash = "sha256:bf5ddda8990e3454a0de1040ac10a44239fbd1e4da02e2c9e2896a8cf3cd4ea4", size = 20616792, upload-time = "2026-06-07T22:20:42.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/88/a181acb1fb8d8b8b7547a703641ff17b8fb62621af0d1c0dfe1ac14af6eb/spotforecast2_safe-19.0.0-py3-none-any.whl", hash = "sha256:9bf724aea5c609341785b5cbd255d99feda56a205e5ee18566473bce5019396a", size = 20687905, upload-time = "2026-06-07T19:08:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/36917eb6493569a95d79b498853d3069303a1c79b7b879b4330f34f66730/spotforecast2_safe-19.1.0-py3-none-any.whl", hash = "sha256:4e50dc82db56bb58c64e72d29e0eeafe39161e81ecc55b8fb7ba3ec134cb1b87", size = 20685366, upload-time = "2026-06-07T22:20:39.088Z" }, ] [[package]] From 40654f4b19df647441b7822f21c8d967353d2c1e Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:04:07 +0200 Subject: [PATCH 3/4] feat(deps): support spotforecast2-safe 20.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen the spotforecast2-safe pin to >=19.0.0,<21 and lock to 20.0.0. 20.0.0 removes the spotforecast-safe-n2o1-cov-df console task (unused here) and makes ConfigEntsoe inherit ConfigMulti. The full spotforecast2 suite was run against 20.0.0 (editable overlay): 1171 passed, 0 failures — the inheritance change is transparent (the only isinstance(ConfigMulti) check tests a ConfigMulti instance) and nothing imports the removed task. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 2 +- uv.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24e6f980..61a09798 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "ruff>=0.15.6", "scikit-learn>=1.8.0", "shap>=0.49.1", - "spotforecast2-safe>=19.0.0,<20", + "spotforecast2-safe>=19.0.0,<21", "spotoptim>=0.12.9", "tqdm>=4.67.2", ] diff --git a/uv.lock b/uv.lock index 79ad22fc..946c2058 100644 --- a/uv.lock +++ b/uv.lock @@ -3683,7 +3683,7 @@ requires-dist = [ { name = "safety", marker = "extra == 'dev'", specifier = ">=3.0.0" }, { name = "scikit-learn", specifier = ">=1.8.0" }, { name = "shap", specifier = ">=0.49.1" }, - { name = "spotforecast2-safe", specifier = ">=19.0.0,<20" }, + { name = "spotforecast2-safe", specifier = ">=19.0.0,<21" }, { name = "spotoptim", specifier = ">=0.12.9" }, { name = "tqdm", specifier = ">=4.67.2" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.29" }, @@ -3701,12 +3701,11 @@ dev = [ [[package]] name = "spotforecast2-safe" -version = "19.1.0" +version = "20.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astral" }, { name = "feature-engine" }, - { name = "flake8" }, { name = "holidays" }, { name = "lightgbm" }, { name = "numba" }, @@ -3719,9 +3718,9 @@ dependencies = [ { name = "statsmodels" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/22/10a45c89b8805c63eb5c0a24c6a8bc47a2eac6b5821de91daa493298edae/spotforecast2_safe-19.1.0.tar.gz", hash = "sha256:bf5ddda8990e3454a0de1040ac10a44239fbd1e4da02e2c9e2896a8cf3cd4ea4", size = 20616792, upload-time = "2026-06-07T22:20:42.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5c/6af66d76ba594b64540fed675307cf6ada608d99850afdd808ea7d780ea2/spotforecast2_safe-20.0.0.tar.gz", hash = "sha256:2fbb176270dce6d72316a40a6e0311d39fbae89effffa34c1857a7f445aeb371", size = 20624314, upload-time = "2026-06-08T18:57:38.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/a5/36917eb6493569a95d79b498853d3069303a1c79b7b879b4330f34f66730/spotforecast2_safe-19.1.0-py3-none-any.whl", hash = "sha256:4e50dc82db56bb58c64e72d29e0eeafe39161e81ecc55b8fb7ba3ec134cb1b87", size = 20685366, upload-time = "2026-06-07T22:20:39.088Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4c/2c76a20c0083e2c70012c95fba1a6950bd24a142d2b475d159ff3de2d77e/spotforecast2_safe-20.0.0-py3-none-any.whl", hash = "sha256:2a4259fdd8078af1cf7a8cf4ef75758091b504494f999f179d3d44f08acebe1b", size = 20688771, upload-time = "2026-06-08T18:57:35.613Z" }, ] [[package]] From 9182c5ed0fb8c481dbfe07ec9c462491d48fa62c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 8 Jun 2026 19:29:42 +0000 Subject: [PATCH 4/4] chore(release): 6.1.0-rc.1 [skip ci] ## [6.1.0-rc.1](https://github.com/sequential-parameter-optimization/spotforecast2/compare/v6.0.0...v6.1.0-rc.1) (2026-06-08) ### Features * **deps:** support spotforecast2-safe 20.x ([40654f4](https://github.com/sequential-parameter-optimization/spotforecast2/commit/40654f4b19df647441b7822f21c8d967353d2c1e)) --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da56512..f7c3108d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [6.1.0-rc.1](https://github.com/sequential-parameter-optimization/spotforecast2/compare/v6.0.0...v6.1.0-rc.1) (2026-06-08) + +### Features + +* **deps:** support spotforecast2-safe 20.x ([40654f4](https://github.com/sequential-parameter-optimization/spotforecast2/commit/40654f4b19df647441b7822f21c8d967353d2c1e)) + ## [6.0.0](https://github.com/sequential-parameter-optimization/spotforecast2/compare/v5.2.0...v6.0.0) (2026-06-07) ### ⚠ BREAKING CHANGES diff --git a/pyproject.toml b/pyproject.toml index 61a09798..4c80b500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spotforecast2" -version = "6.0.0" +version = "6.1.0-rc.1" description = "Forecasting with spot" readme = "README.md" license = { text = "AGPL-3.0-or-later" }