feat(doctor): report a leftover GUI OpenCV after an upgrade - #584
Open
chinmayajha wants to merge 2 commits into
Open
chinmayajha wants to merge 2 commits into
chinmayajha wants to merge 2 commits into
Conversation
Switching the dependency to opencv-python-headless fixed fresh installs on minimal Linux images, but pip does not uninstall opencv-python just because the new metadata names a different distribution. Both ship the same cv2 module, so an upgraded environment carries two distributions for one import name and which one wins depends on install order -- observed while testing the switch: a force-reinstall left the GUI build in place and `optics --version` kept failing on libxcb.so.1. Doctor is where a user goes when the CLI misbehaves, so it now says so: resolving both distributions through importlib.metadata is cheap and unambiguous, and the row carries the command that removes the GUI build. The row is a warning, not a failure. On any machine where doctor runs at all the import already succeeded, so this is a latent portability problem rather than something blocking the run in front of the user -- and doctor reserves failures for the project-config rows that `--check` gates CI on. The remedy is environment-aware for the same reason installs are: a bare `pip uninstall` edits the pip on PATH, which in a pipx environment is some other environment entirely, and in a lock-managed one is undone by the next sync. removal_command() mirrors plan_install()'s ownership rules -- prune through the manager for a project environment, name the interpreter for a tool environment, plain pip everywhere else.
The removal hint stopped at `pip uninstall -y opencv-python`, which is harmful advice in exactly the environment the row fires in. The two distributions do not merely share an import name -- they write the same cv2 files, and the second install overwrote the first's. Uninstalling one therefore deletes files the survivor's metadata still claims, turning an ambiguous install into one where cv2 no longer imports at all. The hint now reinstalls the headless build after removing the GUI one, pinned to the version already recorded so the repair re-resolves nothing. A tool environment is rebuilt through its manager instead, which does both halves in one command and cannot leave a half-removed cv2 behind. That also drops the lock-managed special case. Pruning was the wrong instrument twice over: it leaves the survivor's files deleted just the same, and the objection it was answering does not apply here -- removing a distribution the lockfile does not list, and restoring the version it does, converges on the lock rather than drifting behind the manager's back.
|
This branch has not been deployed
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.



Closes #579
The failure
opencv-pythonandopencv-python-headlessare two distributions that ship the samecv2module. When #577 moved the dependency to the headless build, that fixed fresh installs on minimal Linux images and nothing else: pip does not uninstallopencv-pythonjust because the new metadata names a different distribution. An upgraded environment ends up holding both, one import name is provided twice, and which one wins depends on install order.Observed while testing #577 — force-reinstalling over an environment that already had
opencv-pythonleft the GUI build in place, andoptics --versionkept failing onlibxcb.so.1exactly as before the fix.The fix
A new
opencvrow inoptics doctor's Core section, fired when both distributions resolve throughimportlib.metadata:Release notes were the alternative. They are read once, and this failure surfaces much later as a mysterious import error — doctor is where a user already goes when the CLI misbehaves.
Why the repair reinstalls
Uninstalling alone is not enough, and would be actively harmful. The two wheels do not merely share an import name — they write the same
cv2/files, and whichever installed second overwrote the first's.pip uninstall opencv-pythonwalks its RECORD and deletes those files, leavingopencv-python-headlessinstalled according to its metadata and unimportable in fact. So the hint lays the survivor back down, pinned to the version already recorded so the repair re-resolves nothing and a lock-managed project stays on its lock.Severity: warn, not fail
Doctor's module docstring is explicit that machine checks report warnings so a fresh machine gets a readable to-do list, and reserves failures for the project-config rows that
run_doctor(check=True)turns into a non-zero exit. Two further reasons this row belongs on the warning side:cv2already imported — the CLI's startup guard would have aborted otherwise. The row is a latent portability problem (it bites when the same environment is moved to a minimal image), not something blocking the run in front of the user.failwould makeoptics doctor --checkexit non-zero in CI for a condition that is not breaking anything on that runner.The row is deliberately not added to
_mandatory_hints, for the same reason: that list is the "before your first real run" call-to-action for the enabled driver's own must-haves, and a dual OpenCV install does not block the first run on the machine reporting it.Why the hint is environment-aware
A bare
pip uninstallis wrong advice in environments this project already classifies, sohelper/environment.pygrows areplace_command()besideplan_install()andproject_add_command()— the module CLAUDE.md designates for "where an install is allowed to write", and the oneabort.reinstall_guidance()already consumes for this class of advice:pipx reinstall optics-framework/uv tool install --reinstall optics-frameworkuv pip uninstall --python <interpreter> … && uv pip install --python <interpreter> --reinstall …A lock-managed environment gets no case of its own, unlike
plan_install: removing a distribution the lockfile does not list, and restoring the exact version it does, converges on the lock rather than drifting behind the manager's back, so there is nothing for the next sync to undo.check_core()now callsdetect()once and passes the result to bothdescribe()and the new row, instead of detecting twice.Known blind spot
The row cannot fire in the fully-broken state. If the GUI
cv2fails to import,helper/cli.py's import guard aborts beforeDoctorCommandexists, and the user gets the generic startup panel fromabort.reinstall_guidance(). That is by design here: the row is preventive, and catches the dual install on the machine where the environment is built — which still importscv2fine — before it is shipped to an image that does not. Making the startup guard itself name the duplicate distribution is a change toabort.pyand out of scope for this one.Stragglers
Checked, none found.
pyproject.tomlandpoetry.lockname onlyopencv-python-headless;ALL_ENGINESinhelper/setup.pyhas no OpenCV entry (it is a core dependency, not an installable extra); theopencvmentions inhelper/live.pyare comments about C-extension stderr behaviour and the one indocs/usage/REST_API_usage.mdis animage_detectionengine key, neither of which is the distribution name. #577 touched no docs, so there is nothing stale to correct.Testing
TestOpenCvRowintests/units/helpers/test_doctor.py— both installed fires the row with the repair command and the recorded headless pin; headless-only, GUI-only and neither-installed are all silent; missing metadata leaves the other Core rows intact; the hint follows the install environment.importlib.metadataanddetect()are both mocked, so nothing depends on the machine running the suite.TestReplaceCommandintests/units/helpers/test_environment.py— the full decision table on constructedEnvironmentvalues, including the no-pip-and-no-uv corner and a guard that the survivor is always reinstalled.1371 passed, 2 xfailed.pre-commit run --files <changed>clean (ruff, bandit, whitespace, EOF, gitleaks).