Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions .github/workflows/run_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,35 @@ jobs:
# into silence. Without this step a partial install silently shrinks the suite
# and CI still reports green. Guarded by
# tests/test_falsification_extras_actually_installed.py.
# Also asserts the resolver did NOT land in the floor's major: the floor step below
# covers that end, so this leg must cover the other. Read from pyproject through the
# one reader, not a literal. A deliberate false red: if every release above the
# floor's major were yanked, this fails loudly rather than silently testing 1.x twice.
- name: Verify optional extras are installed
run: poetry run python -c "import views_frames, importlib.metadata as m; v = m.version('views-frames'); print('resolved views-frames', v); assert v.split('.')[0] != '1', 'the resolver picked the floor major; the 2.x end of the range would go untested'"
run: poetry run python -c "import views_frames, importlib.metadata as m; from tests.test_falsification_extras_actually_installed import _views_frames_specifier; from packaging.specifiers import SpecifierSet; v = m.version('views-frames'); spec = SpecifierSet(_views_frames_specifier()); floor = min(str(s.version) for s in spec if s.operator == '>='); print('resolved views-frames', v, 'declared', spec, 'floor', floor); assert spec.contains(v), 'resolved outside the declared range'; assert v.split('.')[0] != floor.split('.')[0], 'the resolver picked the floor major; the other end of the range would go untested'"

- name: Run tests
run: |
set -e
poetry run pytest tests/

# The `frames` extra admits two views-frames majors (>=1.10.2,<3; #91). The step
# above runs on whatever the resolver picks (asserted to be the 2.x end in the
# verify step), so the FLOOR would be untested. This reads the floor from
# pyproject.toml (one source, no literal to drift), pins it, and runs the whole
# suite on it. Nothing after this step touches Python, so nothing is restored;
# above runs on whatever the resolver picks (asserted not to be the floor's major
# in the verify step), so the FLOOR would be untested. This reads the floor through
# the ONE reader the guards use (no second parser, no literal to drift), pins it
# with `--no-deps` so pip cannot move other pins, proves the environment consistent
# and the module importable, and runs the whole suite on it. Nothing after this
# step touches Python, so nothing is restored;
# a later Python-touching step must reinstall the extras first (the held workflow
# text forces that review). Guarded by the held workflow text.
- name: Run the suite on the views-frames floor
run: |
set -e
FLOOR=$(poetry run python -c "import tomllib, re; d = tomllib.load(open('pyproject.toml', 'rb')); p = d.get('project', {}); e = [x for x in p.get('dependencies', []) + sum(p.get('optional-dependencies', {}).values(), []) if x.startswith('views-frames')]; spec = re.sub(r'^views-frames(\[[^\]]*\])?', '', e[0]) if e else d['tool']['poetry']['dependencies']['views-frames']['version']; print([s for s in spec.replace(' ', '').split(',') if s.startswith('>=')][0][2:])")
FLOOR=$(poetry run python -c "from tests.test_falsification_extras_actually_installed import _views_frames_specifier; from packaging.specifiers import SpecifierSet; print(min(str(s.version) for s in SpecifierSet(_views_frames_specifier()) if s.operator == '>='))")
echo "views-frames floor from pyproject.toml: $FLOOR"
poetry run pip install --quiet "views-frames==$FLOOR"
poetry run python -c "import importlib.metadata as m, sys; assert m.version('views-frames') == sys.argv[1], m.version('views-frames')" "$FLOOR"
poetry run pip install --quiet "views-frames==$FLOOR" --no-deps
poetry run pip check
poetry run python -c "import views_frames, importlib.metadata as m, sys; from views_frames import FrameMetadata; assert m.version('views-frames') == sys.argv[1], m.version('views-frames')" "$FLOOR"
poetry run pytest tests/ -q

# Structural documentation checks (CIC references, cross-ADR integrity, unfilled
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ provided they were announced here.

## [Unreleased]

_Nothing yet._

---

## [2.1.0] — 2026-09-19

One constraint changes: the `frames` extra no longer caps views-frames below 2.0.0. The
rest of the release is the guards and CI legs that make that measured, and a sixth item
on the ADR-022 §7 release checklist so a stale cap is noticed at a release rather than by
a consumer. Nothing this package emits changes. Issue #91, reported by views-pipeline-core's
3.3.0 range review.

### Changed

- **The `frames` extra no longer excludes views-frames 2.x** (`>=1.10.2,<3`, was `<2`).
Expand All @@ -35,6 +47,40 @@ provided they were announced here.
resolved version, asserting the resolved one is the 2.x end. Nothing changes in what
this package emits; MINOR under ADR-022 §5.

### Release checklist (ADR-022 §7)

- [x] **Does this release do anything rule 2 governs — remove an `__all__` symbol, remove
a supported config key, narrow an accepted input, or change a raised exception type?
If so, did a `DeprecationWarning` ship at least one release ago?** No. One dependency
constraint widens; no symbol, key, input or exception type changes.
- [x] **Does this release make previously-accepted input fail? If so, are the release
notes explicit, and have known consumers been notified?** No input changes. The one
consumer that requests the `frames` extra, views-reporting, was notified **before the
tag** on 2026-09-19 (views-platform/views-reporting#289, comment 5738670537); the
comment records what their own views-frames bound was when observed. views-pipeline-core
filed #91.
- [x] **Does the version bump match the change class (rule 5)?** `2.0.0` → `2.1.0`,
MINOR. §1 does not count dependency ranges as public API; widening one removes an
exclusion and breaks nothing that resolved before.
- [x] **Do the release notes list every breaking change with its migration?** There is
no breaking change.
- [x] **Does `MetricFrame`'s format or axis vocabulary change? If so, has it been agreed
with views-reporting and views-pipeline-core?** No. Measured: a frame saved under
views-frames 1.10.2 loads under 2.0.0 and the reverse, identically; CI runs the whole
suite on both ends of the range.
- [x] **Has any dependency this package caps published a release outside the declared
range since the last release? If so, is the cap measured and deliberate, or stale?**
This is the release that answers "stale" for views-frames and fixes it. Working the
item honestly for the rest: **numpy `<2.0.0` is stale.** Measured 2026-09-19 with
`pip install --dry-run --only-binary=:all:`: on Python 3.13 and 3.14, both inside our
declared `>=3.11,<3.15`, no numpy wheel satisfies `<2.0.0`, so this package cannot be
installed from wheels there (3.11 and 3.12 resolve). scipy 1.18 (2026-06-19) requires
numpy ≥ 2, so under our cap the resolver silently freezes scipy at 1.17.x. Not fixed in
this release — lifting the cap means measuring numpy 2.x — and tracked as #93 with
register C-41 re-tiered; until then, Python 3.13/3.14 users need a numpy 1.26 built from
source. `python <3.15`: 3.15.0 final is due 2026-10-01; #93 covers it. Dev group:
`pandas <3` has releases above it; dev-only, no consumer effect.

---

## [2.0.0] — 2026-09-18
Expand Down
2 changes: 1 addition & 1 deletion documentation/ADRs/022_evolution_and_stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ This policy is checked at two points:
- [ ] Does the version bump match the change class (rule 5)?
- [ ] Do the release notes list every breaking change with its migration?
- [ ] Does `MetricFrame`'s format or axis vocabulary change? If so, has it been agreed with views-reporting and views-pipeline-core?
- [ ] Has any dependency this package caps (`numpy`, `scipy`, `views-frames`) published a release outside the declared range since the last release? If so, is the cap measured and deliberate, or stale? *(Added 2026-09-19, #91: the `frames` extra's `<2` cap outlived views-frames 2.0.0 by a month and was noticed by a consumer, not by a release of ours.)*
- [ ] For every capped entry in `pyproject.toml` — `python` and the dev group included, not a fixed list — has the dependency published a release outside the declared range since the last release, or a release that requires something another of our caps excludes? If so, is the cap measured and deliberate, or stale? *(Added 2026-09-19, #91: the `frames` extra's `<2` cap outlived views-frames 2.0.0 by a month and was noticed by a consumer, not by a release of ours.)*

## Consequences

Expand Down
4 changes: 2 additions & 2 deletions documentation/CICs/MetricFrame.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Status:** Active
**Owner:** Evaluation Core
**Last reviewed:** 2026-08-02
**Last reviewed:** 2026-09-19
**Related ADRs:** views-frames ADR-020 (contract home), ADR-041 (Output Schema), ADR-013 (Observability), ADR-015 (Degenerate/Empty Results), ADR-011 (Topology)

---
Expand Down Expand Up @@ -42,7 +42,7 @@ It is a string-keyed value object — **not** a spatiotemporal `(time, unit)` fr
- **`identifiers`**: dict containing exactly the keys in `AXES`, each a 1-D length-`N` array of strings.
- **`metadata`**: optional `MetricFrameMetadata`; defaults to an empty one.
- **Assumes** the caller has already decided what belongs in the frame. Construction is a structural gate, not a semantic one.
- **Requires** the optional `views-frames` dependency (`pip install views-evaluation[frames]`), any version in `>=1.10.2,<3` — both majors measured 2026-09-19 (#91). The module is import-gated in `views_evaluation/__init__.py` on `find_spec`, so the core API stays importable without it (ADR-011 minimal core). This package's only runtime import from views-frames is the `FrameMetadata` dataclass (six keyword fields, `to_dict`, `from_dict`); its second dependency is the envelope contract `views_frames.conformance.assert_frame_envelope`, called by the tests, whose body is unchanged between the majors although views-frames moved its `CONFORMANCE_FLOOR` to 2.0.0. `tests/test_falsification_extras_actually_installed.py::TestViewsFramesRange` pins the range and the one-name import surface, `tests/test_metric_frame.py::TestViewsFramesSurface` pins `FrameMetadata`'s behaviour on the installed major, and CI runs the whole suite on the range's floor as well as on the resolved version.
- **Requires** the optional `views-frames` dependency (`pip install views-evaluation[frames]`), any version in the range `pyproject.toml` declares (guarded by `TestViewsFramesRange`; measured on both ends, see CHANGELOG `[2.1.0]`). The module is import-gated in `views_evaluation/__init__.py` on `find_spec`, so the core API stays importable without it (ADR-011 minimal core). This package's only runtime import from views-frames is the `FrameMetadata` dataclass (six keyword fields, `to_dict`, `from_dict`); its second dependency is the envelope contract `views_frames.conformance.assert_frame_envelope`, called by the tests. `TestViewsFramesRange` pins the range and the one-name import surface, `tests/test_metric_frame.py::TestViewsFramesSurface` pins `FrameMetadata`'s behaviour on the installed major, and CI runs the whole suite on the range's floor as well as on the resolved version.

---

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "views_evaluation"
version = "2.0.0"
version = "2.1.0"
description = ""
authors = [
"Xiaolong Sun <xiaolong.sun@pcr.uu.se>",
Expand All @@ -20,6 +20,7 @@ frames = ["views-frames"]

[tool.poetry.group.dev.dependencies]
pytest = "^9.0.3"
packaging = ">=22" # Dev-only: the dependency-range guards and CI floor step parse specifiers with it; declared, not borrowed from pytest
ruff = "*"
properscoring = "^0.1.0" # Dev-only: used as test oracle for CRPS parity verification
scikit-learn = "^1.6.0" # Dev-only: test oracle for AP/MTD parity verification (left runtime 2026-09-18, C-05)
Expand Down
Loading
Loading