Repository navigation
Conversation
…package linopy reads math-spec natively, so the eager lane in `lpspec/linopy/` is a second, smaller implementation of something linopy owns. It goes, and what took its place as the differential oracle is `linopy.Model.from_spec`. The `[linopy]` extra now pins a rev rather than master: the oracle and this package must lower with the same math-spec, and a moving branch would swap the oracle's language under a suite that pins its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmAEXeq2gC4eFbr7Yeur74
19 of 33 tasks
FBumann
added a commit
that referenced
this pull request
Sep 25, 2026
> **Prompt:** And prepare a todo list for me the same way. Also start with 0.1.0rc1 > [!NOTE] > The following content was generated by AI. This is the release PR for `0.1.0rc1`, the release candidate for the first PyPI upload. It is stacked on #1751. It stays a draft until PyPI can take the package. One direct reference is left, the `linopy` extra, and #1755 removes it (step 1). ## The first PyPI release, step by step ### 1. What blocks the upload PyPI refuses a distribution that names a git URL in its metadata. - [x] mathspec is on PyPI, and `pyproject.toml` depends on `mathspec>=0.1.0` (#1754). The parity job reads the corpus tag from that floor. - [x] Merge #1755. It takes linopy out of the package and keeps it as the test oracle: the `linopy @ git+…@master` reference moves to the `dev` group, which PyPI never sees. It also deletes `allow-direct-references`, so hatchling refuses a direct reference at build time, before PyPI would. ### 2. One-time setup **PyPI** - [x] On pypi.org, go to Account → Publishing → "Add a new pending publisher" → GitHub, and fill in: - PyPI project name: `specsolve` - Owner: `fluxopt` - Repository name: `specsolve` - Workflow name: `release.yaml` - Environment name: `pypi` **GitHub**, under fluxopt/specsolve Settings - [x] Environments: create or edit `pypi`. Under "Deployment branches and tags", choose "Selected branches and tags" and add `main`. Add the people who may approve a release as required reviewers. - [x] After #1751 merges: Rules → the `main` ruleset. Require `ci`, `Conventional commit subject` and `Changelog line`. A required check must run on `main` once before you can require it. - [x] Issues → Labels: create `no changelog`. **Clean-up that the release does not need** - [ ] Delete the repository variables `AUTO_RELEASE` and `PUBLISH_TO_PYPI` if they exist. Turn off "Allow auto-merge" unless you want it for other PRs. - [x] Remove `specsolve` from the release app's (`fluxopt-release-bot`) repository access. If no other repository uses the app, uninstall it and delete the secrets `APP_CLIENT_ID` and `APP_PRIVATE_KEY`. - [x] Delete the branches `release-please--branches--main--components--farkas` and `release-please--branches--main--components--linopy-yaml`. - [x] Close #1675 and #1676. #1751 replaces both. - [ ] Open PRs owe a changelog line after #1751 merges: #1737, #1527, #1516 and #1541. #1541 (drop the linopy lane for linopy's own `from_spec`) conflicts with #1755; close it or rebase it. ### 3. Before this PR merges: what the PyPI page shows - [x] `[project.urls]` has only `repository`. Add `Documentation`, `Issues` and `Changelog`, as energy-models/mathspec#707 did. - [x] The README has two relative links, `CONTRIBUTING.md` and `docs/about/prior-art.md`. On PyPI they do not resolve. Make them absolute. - [x] `src/specsolve/relational/parquet.py` has two error messages that say "while the package is on 0.0.1aN". `tests/test_archive.py` asserts the same. Reword them to "before 1.0". - [x] RELEASING.md, "Relaxed while in early development": it says to tighten CI before 0.1.0, starting with a Python matrix behind the gate job. Do that, or change the sentence. ### 4. This PR - [x] Merge #1751, then #1755. Then this PR's base must be `main`. GitHub retargets it when #1751's branch is deleted; if the branch stays, retarget by hand. - [x] Mark the PR ready. - [x] Set the date in `## 0.1.0rc1 (2026-09-25)` to the day you merge, if that is not the 25th. - [x] Edit the notes under the heading. Add the lines of the PRs merged since. The text becomes the GitHub release notes, as written. - [x] Check that `ci` is green. Its step "Check the changelog headings" prints `releases 0.1.0rc1 on merge`. - [x] Review, then squash-merge. ### 5. After the merge - [ ] Actions → **Release**, the run for the merge commit: - [ ] `Tag the version the changelog names` created the tag `v0.1.0rc1` and the GitHub release `0.1.0rc1`, marked as a pre-release. - [ ] `Build` passed, and its check printed `specsolve-0.1.0rc1`. - [ ] `Publish to PyPI` waits for approval: "Review deployments" → `pypi` → Approve. - [ ] https://pypi.org/project/specsolve/0.1.0rc1/ exists. The README renders, and the project links work. - [ ] Smoke test in a fresh environment: ```bash python -m venv /tmp/ss && /tmp/ss/bin/pip install specsolve==0.1.0rc1 /tmp/ss/bin/python -c "import specsolve, mathspec; print(specsolve.__version__, mathspec.__version__)" # 0.1.0rc1 0.1.0 /tmp/ss/bin/pip install specsolve # finds no stable version until 0.1.0 exists ``` - [ ] If a job failed, fix the cause, then use "Re-run failed jobs" on that run. If `0.1.0rc1` reached PyPI broken, release `0.1.0rc2`. ### 6. Then 0.1.0 - [ ] Open the next release PR, titled `chore: release 0.1.0`. It renames the next `## Upcoming version` to `## 0.1.0 (date)`, with notes that say this is the first release on PyPI. Merge it and approve the upload as above. - [ ] Smoke test `pip install specsolve` without a version. It should install `0.1.0`. ### 7. Follow-ups - [ ] 83 links in the tree point at `math-spec.readthedocs.io`. The redirect keeps them working. Point them at `mathspec.readthedocs.io`. - [ ] Optional: a conda-forge recipe. - [ ] Patch releases to an older version line. No path exists yet; mathspec tracks the same gap in energy-models/mathspec#710. <details><summary>What this PR changes, what merging does, gates</summary> **The changes:** - `CHANGELOG.md`: an empty `## Upcoming version`, then `## 0.1.0rc1 (2026-09-25)`, with a paragraph and #1748's line. - `AGENTS.md`: "The project is `0.0.1aN` and holds no compatibility promise" becomes "The project holds no compatibility promise before 1.0". One sentence is added: a release that breaks a model file or an import raises the minor version, and its notes name the break. - `CONTRIBUTING.md`: the same rule, in the same words. **Why a release candidate:** it goes through the same tag, GitHub release, build, approval and trusted-publishing upload as `0.1.0`. The only addition is `--prerelease` on the GitHub release. pip skips it unless someone asks for it, so a broken first upload reaches no user. The version sorts after `0.0.1a358` and before `0.1.0`. **What merging does:** `release.yaml` finds `0.1.0rc1` with no tag. It creates `v0.1.0rc1` and a GitHub pre-release, builds from the tag, checks that the wheel and the sdist are `0.1.0rc1`, and waits for approval on `pypi` before the upload. **Gates:** `python -m tools.changelog check` prints "releases 0.1.0rc1 on merge", and `notes 0.1.0rc1` prints the section above. The suite on #1751's commit is in that PR. This commit changes only prose and the changelog. **Not verified:** the name `specsolve` on PyPI. The PyPI API cannot be reached from this container, so check that the name is free when you add the pending publisher. The type is `chore`, which owes no changelog line. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw Co-authored-by: Claude <noreply@anthropic.com>
Collaborator
Author
|
Note The following content was generated by AI. Closed as overtaken by #1755. That PR removed the Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
src/lpspec/linopy/is deleted and the differential oracle is nowlinopy.Model.from_spec, pinned at PyPSA/linopy#922'shead
8b88ac72. This cannot merge as it stands: that PR is a draft, and itsbuild refuses nine of the ten ported models over a rule math-spec decides the
other way.
The blocker, first
linopy's port made absence uniform — a missing parameter row is refused
wherever it is used. Rule 8
says a missing row reads as the value that contributes nothing, which for a
coefficient is
0, and refuses only where no such value exists: a divisor,a bound, a constant side, a piecewise breakpoint. So the oracle refuses the
language's own sparsity idiom.
A second, smaller one: a
dtype: datetimedimension's labels are canonicalisedagainst the declared dtype here (#1076) and compared as handed over there, so a
datetime.dateindex and adatetime64lookup value are two labels to theoracle and one to this engine.
Both are recorded as strict xfails naming the divergence — 20 of them,
against
spec_oracle.SPARSE_COEFFICIENTandspec_oracle.DATETIME_SPELLING—so each XPASSes and the constant comes out the day it is fixed upstream. Nine
of those are the whole of
test_corpus_parity'sORACLE_GAPStable. Untilthey are fixed, this PR trades a working in-repo oracle for one that does not
run on most of the corpus, which is the reason not to merge it yet rather than
a detail of it.
What is in the diff
lpspec/linopy/(1550 lines, 8 modules),lanes.py(Buildablemoves to
api.py,LANEShad one entry and goes),capabilities.lane_cannot_build_message,check(spec, sink='linopy'), andtests/test_linopy_lane.py(941 lines).tests/spec_oracle.py— two verbs keeping the deleted lane'ssignatures over
Model.from_spec/model.spec.evaluate, pluslinopy_sources, which respells this package's tables into linopy'scontainers and validates nothing, so a defect under test reaches
linopy.spec.attachand comes back in linopy's words.tests/oracle.pykeeps the
[linopy]guard and grows a second one forModel.from_spec.both_lanes_refuse→both_refuse: it no longer compares the twosentences. That claim was only ever true because both lanes read through one
tidy_sources; linopy has its own reader now, so what is checkable is thatboth refuse and neither calls a data defect a language error.
test_data_parity's verdict isREFUSED/ACCEPTED, not an exceptionclass — each package raises its own.
linopy @ git+…@8b88ac72, a rev rather than master. Thatreverses the rule the pin carried, and the comment says why: the oracle is
linopy's implementation of the same language now, so its math-spec and ours
must be the same one, and master moving would swap the oracle's language
under a suite that pins its own. The
pixi.lockcomment that leaned on"the extra resolves a branch" is updated with it.
differential/pypsa/parity.pybuilds through the same module (it importstests.spec_oracledirectly — it is not collected by pytest and has no bareinstall to run on).
Coverage that moved, and coverage that is gone
test_linopy_lane.py— the lane's builder, loader, operators,where, absence helperstest/test_spec_*.pytest_sum_by_lookups's directoperator_grouped_sumprobetest_architecture.test_the_linopy_lane_stays_two_verbstest_architecture.test_both_lanes_dispatch_on_every_plan_nodetest_the_engine_dispatches_on_every_plan_node; the cross-lane half is the differential's nowtest_docs_site.test_the_translation_table_names_every_built_in_operator_in_linopy_lane, the exemption in two import rules)check(spec, sink='linopy')LaneErrornaming the way round a quadratic constraintTypeError.test_linopy_cannot_build_one_and_this_engine_canpins thatVerified
ruff check·ruff format --check·pyrefly check·pytest -n 8·mkdocs build --strict— all clean. Run in auvvenv rather than pixi (nopixi in this environment), against the pinned linopy at
8b88ac72.Not run:
pixi run test-floors(the bare-install shape),test-bench, andthe
PyPSA parityworkflow — none has pypsa or a pixi environment here, sodifferential/pypsa/parity.py's repoint is unexercised. Thedocs/examples/pypsa_ladder.mdcolumn rename is applied to the page and totools/ladder.pyin lockstep, but the page has not been regenerated.Suite
The 20 xfails are the two divergences above: 9 corpus models, 4 in
test_absence/test_arithmetic_laws, 2 intest_piecewise, 2 intest_reserves, 3 datetime cases intest_data_parity, and the sparsecoefficient row of its verdict table.
Departures from Part 2, and what I did not do
one correctness guard it touches (
tests/oracle.py's second guard) is arefusal to run at all, not a branch.
tests/spec_oracle.pyis a new module with one job — the shim rule inPart 2 says no new layer without something concrete needing it now. Two
callers need it now (the suite and the PyPSA harness) and the two packages
genuinely spell sources differently.
IN_BREACH214 → 209 intests/test_assertions.py, following thedeleted files.
[linopy]extra. After the drop its user-facingcontent is pandas and xarray for
Result.to_pandas/to_dataarray, andlinopy is a test dependency; renaming it touches
pypsa-parity.yml, the pixifeatures and the pin tests, and is separable.
Build and change models via math-spec PyPSA/linopy#922 and are the merge blocker.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KmAEXeq2gC4eFbr7Yeur74
Generated by Claude Code