v4: Settle which OpenCV package provides cv2 after the repair (#3493)

The package installed last provides cv2. The repair ranked the
packages with the GUI above the headless ones, so on a Linux without
the graphics libraries it replaced a working headless cv2 with one
that cannot be imported.

- Install a headless package after one with the GUI, and within each
  kind the one with the contrib modules last: opencv-python,
  opencv-contrib-python, opencv-python-headless,
  opencv-contrib-python-headless
- The package that ends up providing cv2 follows from this order alone,
  not from which package was the older one
- Update the tests and correct the CHANGELOG sentence
This commit is contained in:
Dr.Lt.Data
2026-11-12 08:49:21 +09:00
committed by GitHub
parent e519c56057
commit 573742e800
3 changed files with 51 additions and 40 deletions
+6 -5
View File
@@ -47,11 +47,12 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
`opencv-python`, `opencv-python-headless`, `opencv-contrib-python`, and
`opencv-contrib-python-headless` are installed at different versions, the older
ones are reinstalled at the newest installed version, without their
dependencies, so numpy is left as it is. The most complete installed package
is installed last, so the contrib modules stay available, and OpenCV packages
listed in `pip_auto_fix.list` are left to that list. The repair found no
installed OpenCV package and did nothing; it now also compares four-part
versions such as `4.11.0.100` numerically.
dependencies, so numpy is left as it is. A headless package is installed
after one with the GUI, so `cv2` stays importable on a Linux without the
graphics libraries; within each kind, the package with the contrib modules is
installed last. OpenCV packages listed in `pip_auto_fix.list` are left to
that list. The repair found no installed OpenCV package and did nothing; it
now also compares four-part versions such as `4.11.0.100` numerically.
## [4.3] - 2026-09-19
+7 -6
View File
@@ -613,10 +613,11 @@ class PIPFixer:
# fix opencv - the OpenCV packages all install the `cv2` module; keep them at one version
try:
# From least to most complete: the package installed last provides `cv2`,
# so that must be the most complete one that is installed.
# From least to most preferred: the package installed last provides `cv2`.
# A headless package comes after one with the GUI, which needs system libraries that
# Linux may lack; within each kind, the one with the contrib modules comes last.
installed = {}
for x in ['opencv-python-headless', 'opencv-python', 'opencv-contrib-python-headless', 'opencv-contrib-python']:
for x in ['opencv-python', 'opencv-contrib-python', 'opencv-python-headless', 'opencv-contrib-python-headless']:
version = new_pip_versions.get(normalize_pip_name(x))
if version is not None:
installed[x] = version
@@ -635,10 +636,10 @@ class PIPFixer:
cmd = make_pip_cmd(['install', '--no-deps', f"{x}=={newest}"])
subprocess.check_output(cmd, universal_newlines=True)
most_complete = list(installed)[-1]
if most_complete not in targets:
preferred = list(installed)[-1]
if preferred not in targets:
# it was already at the newest version, and the installs above replaced its `cv2`
cmd = make_pip_cmd(['install', '--no-deps', '--force-reinstall', f"{most_complete}=={newest}"])
cmd = make_pip_cmd(['install', '--no-deps', '--force-reinstall', f"{preferred}=={newest}"])
subprocess.check_output(cmd, universal_newlines=True)
logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
+38 -29
View File
@@ -3,8 +3,9 @@
The four OpenCV packages all install the `cv2` module, and the one installed last
provides it. When they are installed at different versions, PIPFixer reinstalls the
older ones at the newest installed version, without their dependencies, and leaves
`cv2` to the most complete package that is installed. OpenCV packages listed in
pip_auto_fix.list are left to that list.
`cv2` to the preferred one: a headless package rather than one with the GUI, and within
each kind the one with the contrib modules. Which package was the older one makes no
difference. OpenCV packages listed in pip_auto_fix.list are left to that list.
"""
import subprocess
@@ -56,47 +57,55 @@ def fix_broken(monkeypatch, tmp_path):
def test_older_packages_are_raised_to_the_newest_installed_version(fix_broken):
"""The most complete package, opencv-contrib-python, is installed last."""
"""The preferred package, opencv-contrib-python-headless, is installed last."""
assert fix_broken({
'opencv-python': '4.11.0.86',
'opencv-python-headless': '4.10.0.84',
'opencv-contrib-python': '4.10.0.84',
'opencv-contrib-python-headless': '4.9.0.80',
}) == [
'install --no-deps opencv-contrib-python==4.11.0.86',
'install --no-deps opencv-python-headless==4.11.0.86',
'install --no-deps opencv-contrib-python-headless==4.11.0.86',
'install --no-deps opencv-contrib-python==4.11.0.86',
]
def test_most_complete_package_is_reinstalled_when_it_was_already_the_newest(fix_broken):
"""Raising opencv-python replaces the `cv2` of opencv-contrib-python, which must be put back."""
assert fix_broken({
'opencv-python': '4.10.0.84',
'opencv-contrib-python': '4.11.0.86',
}) == [
'install --no-deps opencv-python==4.11.0.86',
'install --no-deps --force-reinstall opencv-contrib-python==4.11.0.86',
@pytest.mark.parametrize('gui, headless', [
('opencv-python', 'opencv-python-headless'),
('opencv-contrib-python', 'opencv-python-headless'),
('opencv-contrib-python', 'opencv-contrib-python-headless'),
])
def test_headless_package_provides_cv2_beside_a_gui_package(fix_broken, gui, headless):
"""A package with the GUI needs system libraries that Linux may lack, so it must not replace a headless `cv2`.
The result is the same whichever of the two was the older one.
"""
assert fix_broken({gui: '4.13.0.92', headless: '4.10.0.84'}) == [f'install --no-deps {headless}==4.13.0.92']
assert fix_broken({gui: '4.10.0.84', headless: '4.13.0.92'}) == [
f'install --no-deps {gui}==4.13.0.92',
f'install --no-deps --force-reinstall {headless}==4.13.0.92',
]
def test_contrib_modules_rank_above_the_gui(fix_broken):
"""opencv-contrib-python-headless is raised last, so it provides `cv2`; opencv-python is not reinstalled."""
assert fix_broken({
'opencv-python': '4.11.0.86',
'opencv-contrib-python-headless': '4.10.0.84',
}) == ['install --no-deps opencv-contrib-python-headless==4.11.0.86']
@pytest.mark.parametrize('plain, contrib', [
('opencv-python', 'opencv-contrib-python'),
('opencv-python-headless', 'opencv-contrib-python-headless'),
])
def test_contrib_package_provides_cv2_beside_the_plain_one_of_its_kind(fix_broken, plain, contrib):
"""Raising the plain package replaces the `cv2` of the contrib one, which must be put back."""
assert fix_broken({plain: '4.11.0.86', contrib: '4.10.0.84'}) == [f'install --no-deps {contrib}==4.11.0.86']
assert fix_broken({plain: '4.10.0.84', contrib: '4.11.0.86'}) == [
f'install --no-deps {plain}==4.11.0.86',
f'install --no-deps --force-reinstall {contrib}==4.11.0.86',
]
def test_fourth_version_component_is_compared_as_a_number(fix_broken):
"""4.11.0.100 is newer than 4.11.0.86; compared as text it would be older."""
assert fix_broken({
'opencv-python': '4.11.0.100',
'opencv-python-headless': '4.11.0.86',
}) == [
'install --no-deps opencv-python-headless==4.11.0.100',
'install --no-deps --force-reinstall opencv-python==4.11.0.100',
]
'opencv-contrib-python': '4.11.0.86',
}) == ['install --no-deps opencv-contrib-python==4.11.0.100']
def test_opencv_packages_listed_in_pip_auto_fix_are_left_to_that_list(fix_broken):
@@ -109,18 +118,18 @@ def test_opencv_packages_listed_in_pip_auto_fix_are_left_to_that_list(fix_broken
def test_other_packages_in_pip_auto_fix_do_not_stop_the_repair(fix_broken):
assert fix_broken({
'opencv-python-headless': '4.10.0.84',
'opencv-python': '4.11.0.86',
'opencv-python': '4.10.0.84',
'opencv-python-headless': '4.11.0.86',
}, pip_auto_fix='torch>=2.5.0\n') == [
'install --no-deps opencv-python-headless==4.11.0.86',
'install --no-deps --force-reinstall opencv-python==4.11.0.86',
'install --no-deps opencv-python==4.11.0.86',
'install --no-deps --force-reinstall opencv-python-headless==4.11.0.86',
]
@pytest.mark.parametrize('installed', [
{'opencv-python': '4.11.0.86'},
{'opencv-python': '4.11.0.86', 'opencv-python-headless': '4.11.0.86'},
{'opencv-python-rolling': '5.0.0.20221015', 'opencv-python': '4.11.0.86', 'opencv-python-headless': '4.11.0.86'},
], ids=['one installed', 'same version', 'package with a longer name'])
], ids=['same version', 'package with a longer name'])
def test_nothing_is_installed_when_the_versions_already_agree(fix_broken, installed):
"""The order of preference is applied only when a package has to be raised."""
assert fix_broken(installed) == []