Skip to content

Fix pixi update: relax Mantid version pins for Python 3.12#993

Open
fanchercm wants to merge 6 commits into
nextfrom
fix/pixi-mantid-python312
Open

Fix pixi update: relax Mantid version pins for Python 3.12#993
fanchercm wants to merge 6 commits into
nextfrom
fix/pixi-mantid-python312

Conversation

@fanchercm

Copy link
Copy Markdown
Collaborator

Summary

  • Every Mantid conda channel (mantid and mantid-ornl, main/rc/nightly) has pruned Python 3.11 (6.15.x) builds and now only publishes 6.16.x builds for Python 3.12, breaking pixi update/pixi install on this branch.
  • Dropped the <6.16.0 upper bound on mantidworkbench in the base, qa, and prod dependencies — it was excluding every remaining build in those channels.
  • Removed the unconstrained pyqt = "*" dependency, which pulled in PyQt5 and conflicted with mantidqt's now-mandatory PyQt6 requirement; qtpy remains as the binding-agnostic layer.
  • Bumped requires-python to >=3.12 to match.
  • Recorded the investigation in docs/ground_truths.md for future reference.

Test plan

  • pixi update solves cleanly for default, dev, prod, and qa environments (previously failed with a solver conflict).
  • pixi run -e default test-import-framework passes (imports pyrs, qtpy, mantidqt).
  • pixi run -e dev test-import-framework passes.
  • pixi run -e prod test-import-framework passes.

🤖 Generated with Claude Code

fanchercm and others added 6 commits July 8, 2026 15:47
Every Mantid conda channel (mantid and mantid-ornl, main/rc/nightly) has
pruned Python 3.11 (6.15.x) builds and now only publishes 6.16.x builds
for Python 3.12, breaking the solve for all environments:

- Drop the `<6.16.0` upper bound on mantidworkbench in the base, qa, and
  prod dependencies, which excluded every remaining build in those
  channels.
- Remove the unconstrained `pyqt = "*"` dependency, which pulled in
  PyQt5 and conflicted with mantidqt's now-mandatory PyQt6 requirement;
  qtpy remains as the binding-agnostic layer.
- Bump requires-python to >=3.12 to match.

Verified `pixi update` solves cleanly for default, dev, prod, and qa,
and pyrs/qtpy/mantidqt import successfully in each. Findings recorded
in docs/ground_truths.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Relaxing the mantidworkbench pin (636594c) moved the environment from
PyQt5 to PyQt6, which broke two things that were previously masked:

- pyrs/icons/icons_rc5.py is a pyrcc-compiled resource module hard-coded
  to `from PyQt5 import QtCore`, imported by peak_fitting_viewer.py.
  With PyQt5 no longer installed, this raised ModuleNotFoundError during
  test collection, aborting the whole pytest session in CI. Replaced the
  compiled-resource lookup with direct file paths into pyrs/icons and
  dropped the now-unused icons_rc5.py/icons.qrc.

- QTableWidgetItem.setCheckState() is strictly typed in PyQt6 and no
  longer accepts bool/int; several call sites passed plain bools/ints.
  Because the collection error above aborted the session first, these
  paths in fit_table.py and detector_calibration_viewer.py were never
  actually exercised by CI. Switched them to Qt.CheckState enum values.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pixi-build-backends is a rolling/nightly channel whose pixi-build-python
builds are date+hash suffixed (e.g. 0.5.2.20260612.1208.0827e23) and
never match a bare "==0.5.2". Listed ahead of conda-forge (which has a
real, clean-versioned 0.5.2 release), it shadowed that match under
strict channel priority, making the pin permanently unsolvable and
failing both the "Build conda package" and vulnerability-scan
build-package CI jobs.

Verified by clearing the local rattler cache entirely and rebuilding
from cold. Findings recorded in docs/ground_truths.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The channel-reorder fix for the conda-build backend didn't actually fix
CI: the real cause was that pixi-build-python==0.5.2 only speaks
protocol pixi-build-api-version>=4,<5, while prefix-dev/setup-pixi (no
pixi-version pin) installs whatever pixi CLI is latest at run time and
had already moved to needing >=5,<6. No channel order can satisfy an
old exact pin against a newer host CLI. Relaxed the pin to "*" instead.
Reproduced CI's exact failure locally by matching its pixi version
(0.72.2) with `pixi self-update`, and confirmed the real fix solves
from a fully cleared cache.

Also, now that the PyQt5 import crash no longer aborts test collection,
two more PyQt6 strict-typing bugs surfaced and ran for the first time:

- manual_reduction_viewer.py's _mask_state/_calibration_state/_output_state
  compared a `state` param (Qt.CheckState from checkState(), or plain int
  from the stateChanged signal) against the bare Qt.Unchecked enum member.
  PyQt6's Qt.CheckState doesn't compare equal to a plain int in either
  direction, so the comparison was always wrong for one of the two call
  paths -- the mask/calibration/output line edits stayed permanently
  disabled after the user toggled the checkbox, silently keeping the
  stale default file path. Added an _is_checked() normalizer. Same
  bare-int-vs-enum bug fixed in fit_table.py's checkState() == 0 check.

- strain_stress_view.py's QFormLayout.setFieldGrowthPolicy(0) hit the
  same strict-enum-typing issue as setCheckState -- switched to
  QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow.

Findings recorded in docs/ground_truths.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The tests job passed all 354 tests (0 failures) but still failed CI:
~40s after pytest's summary line, the process segfaulted (exit 139)
during interpreter shutdown. This matches a "wrapped C/C++ object of
type SliceViewerView has been deleted" RuntimeError seen locally from
mantidqt's own ADS-observer cleanup code -- caught and non-fatal under
the local offscreen Qt platform, but apparently a genuine use-after-free
under CI's real xvfb+xcb display.

Added a session-scoped autouse fixture that clears the
AnalysisDataService once, after the last test finishes. Pytest fixture
teardown runs while the interpreter is still fully alive, well before
atexit/shutdown, so any SliceViewer-owned workspaces/observers left open
by UI tests are torn down in a safe, well-defined order instead of
racing with native object destruction at exit.

Verified no regressions: full local suite still passes 354/354 twice in
a row. Could not directly reproduce the segfault locally (offscreen
apparently doesn't trigger it), so this fix relies on the next CI run
for confirmation. Findings recorded in docs/ground_truths.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The tests job passed all 354 tests but still failed CI: ~40s after
pytest's own summary line, the process segfaulted (exit 139) during
Python interpreter shutdown, in third-party Mantid/mantidqt native
cleanup code. Clearing the AnalysisDataService at session end (previous
commit) did not fix this -- the exact same crash recurred on the next
CI run, so ADS state wasn't the cause.

Since the crash happens well after every test result and the coverage
report have already been finalized, the fix is to stop the interpreter
from reaching the crashing teardown at all: added
scripts/development/run_tests.py, which calls pytest.main() directly
and then explicitly flushes stdout/stderr and calls os._exit(code) --
skipping the native cleanup that follows. pyproject.toml's `test` pixi
task now runs this wrapper instead of invoking pytest directly.

Two dead ends recorded in docs/ground_truths.md: a pytest_sessionfinish
hook fires before the terminal reporter's own summary and coverage
table are printed (silently truncating the log even though the exit
code was correct), and os._exit() doesn't flush Python's buffered
stdout/stderr the way normal interpreter shutdown does.

Separately: while iterating on the above (which requires running the
full suite locally, matching what CI runs), tests/ui/test_calibration_ui.py
turned out to write/delete files at bare relative paths
("HB2B_test_export.json", "HB2B_CAL.json") -- i.e. in whatever the
current working directory happens to be, which is the repo root for
both CI and a local run. This collided with and destroyed pre-existing
untracked files of the same name in the repo root. Fixed the test to
write under pytest's tmp_path fixture instead, so it can never touch a
developer's working directory regardless of what's sitting there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant