Skip to content

Add GUI extra in auto-updater; support uv#3346

Open
C-Achard wants to merge 7 commits into
mainfrom
cy/uv&gui-in-auto-update
Open

Add GUI extra in auto-updater; support uv#3346
C-Achard wants to merge 7 commits into
mainfrom
cy/uv&gui-in-auto-update

Conversation

@C-Achard
Copy link
Copy Markdown
Collaborator

Scope

  • Introduces support for multiple installer backends (such as uv instead of justpip)
  • Added GUI extra since the auto-update process runs only there

Adds new utility functions for building update commands, refactors the update process to try preferred installers first with fallbacks, and adds tests for these.

Automated summary

Update backend improvements:

  • Added package_specs_for_update and build_update_commands utility functions in deeplabcut/gui/utils.py to generate the correct package specifications (ensuring deeplabcut is updated with [gui] extra) and to build a prioritized list of installer commands, preferring uv if available and always falling back to pip ([deeplabcut/gui/utils.pyR239-R292](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4279538071ed35262d378e598eb69f5d2e1aced94717f42138692f0d8c718e3cR239-R292)).
  • Refactored the update process in deeplabcut/gui/window.py to use the new backend selection logic, attempting each backend in order and providing more informative error messages and logs for each attempt ([[1]](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4dbcd4272a9f0fb80775a2e54ed8d52f229695085258e5b5d0172f05870a3772L344-R380), [[2]](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4dbcd4272a9f0fb80775a2e54ed8d52f229695085258e5b5d0172f05870a3772R336-R358), [[3]](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4dbcd4272a9f0fb80775a2e54ed8d52f229695085258e5b5d0172f05870a3772L408-L429), [[4]](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4dbcd4272a9f0fb80775a2e54ed8d52f229695085258e5b5d0172f05870a3772R418-R444), [[5]](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4dbcd4272a9f0fb80775a2e54ed8d52f229695085258e5b5d0172f05870a3772R404-R409)).

Testing and reliability:

  • Added a new test suite tests/gui/test_auto_update.py to verify that package specs are built correctly and that the update command selection logic works as intended, including backend prioritization and fallback behavior ([tests/gui/test_auto_update.pyR1-R143](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-fe6347302c05d4e806136ae254f592f3a74c323a95a79724b435dd5c0d83c75eR1-R143)).

Dependency management:

  • Ensured that updates to DeepLabCut via the GUI always include the [gui] extra, keeping GUI dependencies in sync and reducing the risk of mismatched versions ([deeplabcut/gui/utils.pyR239-R292](https://github.com/DeepLabCut/DeepLabCut/pull/3346/files#diff-4279538071ed35262d378e598eb69f5d2e1aced94717f42138692f0d8c718e3cR239-R292)).

C-Achard added 2 commits May 26, 2026 16:32
Add utilities to construct package specs and installer commands (supporting uv then pip) and import shutil/sys. Update MainWindow to run update commands in sequence: store a queue of backends, start each with _start_next_update_command, accumulate attempt outputs and backend names, and retry fallbacks when one backend fails. Improve error messages and logging, adjust progress/cleanup behavior, and surface a consolidated failure dialog if no backend succeeds.
Add unit tests for deeplabcut.gui.utils functions package_specs_for_update and build_update_commands. Tests verify package_specs_for_update adds the GUI extra to 'deeplabcut', preserves other packages, and strips surrounding whitespace. Tests for build_update_commands assert backend ordering and that 'uv' is used when available, the exact command arguments (including use of sys.executable), and that a pip fallback is always present; monkeypatching of shutil.which is used to simulate installer availability.
@C-Achard C-Achard requested a review from Copilot May 26, 2026 14:35
@C-Achard C-Achard self-assigned this May 26, 2026
@C-Achard C-Achard linked an issue May 26, 2026 that may be closed by this pull request
2 tasks
@C-Achard C-Achard added enhancement New feature or request GUI issues relating to GUI labels May 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the GUI auto-updater to support multiple installer backends, preferring uv when available with a pip fallback, and ensures DeepLabCut updates from the GUI always include the [gui] extra so GUI dependencies stay in sync.

Changes:

  • Added package_specs_for_update and build_update_commands utilities to generate update package specs (including deeplabcut[gui]) and a prioritized installer command list.
  • Refactored the GUI update flow to sequentially try each backend and aggregate per-backend failure output for clearer diagnostics.
  • Added unit tests validating package spec generation and backend command selection.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
deeplabcut/gui/utils.py Adds helpers to build package specs (injecting [gui]) and generate backend-specific update commands (uv → pip).
deeplabcut/gui/window.py Updates GUI auto-update execution to try backends in order and improve logging/error reporting across attempts.
tests/gui/test_auto_update.py Adds tests for package spec normalization and backend command prioritization/fallback behavior.
Comments suppressed due to low confidence (1)

tests/gui/test_auto_update.py:38

  • The parameterization includes two identical {"uv": "/mock/bin/uv"} cases expecting the same backend order. This duplicates test work without increasing coverage; remove the redundant entry to keep the test intent clear.
        ({}, ["pip"]),
        ({"uv": "/mock/bin/uv"}, ["uv", "pip"]),
        (
            {"uv": "/mock/bin/uv"},
            ["uv", "pip"],
        ),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/gui/test_auto_update.py
Comment thread deeplabcut/gui/window.py Outdated
Initialize _update_process_output to an empty list when tearing down the update process to avoid carrying over stale output after process deletion. Also make the GUI auto-update test skip when PySide6 is not installed by adding pytest.importorskip("PySide6") to tests/gui/test_auto_update.py, preventing failures in environments without the GUI dependency.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread deeplabcut/gui/window.py Outdated
Comment thread tests/gui/test_auto_update.py Outdated
C-Achard and others added 2 commits May 27, 2026 16:37
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Add robust handling for the update QProcess to avoid races and leaks. Introduces _disconnect_update_process to safely disconnect signals, updates _drain_update_process_output to accept a process (and resolve sender()), and adds sender checks in error/finished handlers so only the current process is handled. Ensures signals are disconnected before deleteLater and prevents acting on stale or unrelated processes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread tests/gui/test_auto_update.py
Comment thread deeplabcut/gui/window.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@C-Achard C-Achard marked this pull request as ready for review May 28, 2026 13:41
@C-Achard C-Achard requested a review from deruyter92 May 28, 2026 13:42
Copy link
Copy Markdown
Collaborator

@deruyter92 deruyter92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix! Just some minor comments and suggestions.

Comment thread deeplabcut/gui/window.py
Comment on lines +388 to +395
if not self._start_next_update_command():
self._cleanup_update_process()
QtWidgets.QMessageBox.warning(
self,
"Update failed",
"No available installer backend was found.",
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_update_commands unconditionally appends the pip fallback so the list if never empty. This means that this code is unreachable, correct?

Comment thread deeplabcut/gui/window.py
Comment on lines +402 to 407
if process is not self._update_process:
return

def _drain_update_process_output(self):
if self._update_process is None:
if process is None:
return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second condition is only met when (simulataneous) self._update_process is None AND process is None. Consider changing to:

Suggested change
if process is not self._update_process:
return
def _drain_update_process_output(self):
if self._update_process is None:
if process is None:
return
if process is None or process is not self._update_process:
return

Comment thread deeplabcut/gui/window.py
Comment on lines +482 to 491
process = self.sender()
if process is not self._update_process:
return

if self._closing:
self._cleanup_update_process()
return

if self._update_process is None:
if process is None:
return
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
process = self.sender()
if process is not self._update_process:
return
if self._closing:
self._cleanup_update_process()
return
if self._update_process is None:
if process is None:
return
process = self.sender()
if process is None or process is not self._update_process:
return
if self._closing:
self._cleanup_update_process()
return

Comment thread deeplabcut/gui/window.py
if output:
self.logger.error(output)

self._cleanup_update_process()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calls _progress_bar.hide() again. Maybe better to remove in one of both places to avoid confusion.

Comment thread deeplabcut/gui/window.py
Comment on lines +466 to +467
self._disconnect_update_process(process)
self._update_process.deleteLater()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this only works because the guard (process is not self._update_processreturn). Safer is process.deleteLater() like in _on_update_process_finished:

Suggested change
self._disconnect_update_process(process)
self._update_process.deleteLater()
self._disconnect_update_process(process)
process.deleteLater()

Comment thread deeplabcut/gui/utils.py
Comment on lines +241 to +270
def package_specs_for_update(packages: list[str]) -> list[str]:
"""Return package specs to install for GUI updates.

DeepLabCut itself should be updated with the GUI extra so GUI dependencies
are kept in sync.
"""
specs = []

for package in packages:
normalized = package.strip()

if normalized == "deeplabcut":
specs.append("deeplabcut[gui]")
else:
specs.append(normalized)

return specs


def build_update_commands(packages: list[str]) -> list[tuple[str, str, list[str]]]:
"""Build installer commands, ordered from preferred to fallback.

Returns tuples of:

(backend_name, program, arguments)

The order is:
1. uv, if available
2. current-interpreter pip fallback
"""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick:

These functions seem designed as a generic public function for building normalized package specs install commands, but in practice they only work for very specific input (e.g. case-sensitive exact match for "deeplabcut"). Also they are specifically intended for the auto-update flow, not general GUI utilities. Maybe make these functions private or document this somehow, e.g.:

Suggested change
def package_specs_for_update(packages: list[str]) -> list[str]:
"""Return package specs to install for GUI updates.
DeepLabCut itself should be updated with the GUI extra so GUI dependencies
are kept in sync.
"""
specs = []
for package in packages:
normalized = package.strip()
if normalized == "deeplabcut":
specs.append("deeplabcut[gui]")
else:
specs.append(normalized)
return specs
def build_update_commands(packages: list[str]) -> list[tuple[str, str, list[str]]]:
"""Build installer commands, ordered from preferred to fallback.
Returns tuples of:
(backend_name, program, arguments)
The order is:
1. uv, if available
2. current-interpreter pip fallback
"""
DLCPackage: TypeAlias = Literal["deeplabcut", "napari-deeplabcut"]
def _package_specs_for_update(packages: list[DLCPackage]) -> list[str]:
"""Return package specs to install for GUI updates.
DeepLabCut itself should be updated with the GUI extra so GUI dependencies
are kept in sync.
"""
specs = []
for package in packages:
normalized = package.strip()
if normalized == "deeplabcut":
specs.append("deeplabcut[gui]")
else:
specs.append(normalized)
return specs
def _build_update_commands(packages: list[DLCPackage]) -> list[tuple[str, str, list[str]]]:
"""Build installer commands, ordered from preferred to fallback.
Returns tuples of:
(backend_name, program, arguments)
The order is:
1. uv, if available
2. current-interpreter pip fallback
"""

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request GUI issues relating to GUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dependency mismatch when upgrading from rc14 to 3.0.0 (pydantic, napari)

3 participants