diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da62ccbc..06a2d8e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/comfyui_manager/common/manager_util.py b/comfyui_manager/common/manager_util.py index 133e9a1c7..5e342dd92 100644 --- a/comfyui_manager/common/manager_util.py +++ b/comfyui_manager/common/manager_util.py @@ -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}") diff --git a/tests/test_pip_fixer_opencv.py b/tests/test_pip_fixer_opencv.py index dd9226c03..17ba8330a 100644 --- a/tests/test_pip_fixer_opencv.py +++ b/tests/test_pip_fixer_opencv.py @@ -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) == []