-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add GUI extra in auto-updater; support uv #3346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bf1ec07
68cfb2e
8f0d7ed
faeb05d
8890667
5298ced
defdc96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,7 +56,7 @@ | |||||||||||||||||||||||||||||||||||||
| UnsupervizedIdTracking, | ||||||||||||||||||||||||||||||||||||||
| VideoEditor, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| from deeplabcut.gui.utils import UpdateChecker | ||||||||||||||||||||||||||||||||||||||
| from deeplabcut.gui.utils import UpdateChecker, build_update_commands | ||||||||||||||||||||||||||||||||||||||
| from deeplabcut.gui.widgets import StreamReceiver, StreamWriter | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| warnings.filterwarnings( | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,6 +98,9 @@ def __init__(self, app): | |||||||||||||||||||||||||||||||||||||
| # Update checks | ||||||||||||||||||||||||||||||||||||||
| self._update_process = None | ||||||||||||||||||||||||||||||||||||||
| self._update_process_output = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_commands = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_attempt_outputs = [] | ||||||||||||||||||||||||||||||||||||||
| self._current_update_backend = None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._scheduled_update_check_silent = True | ||||||||||||||||||||||||||||||||||||||
| self._update_check_timer = QtCore.QTimer(self) | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -309,6 +312,20 @@ def video_type(self, ext): | |||||||||||||||||||||||||||||||||||||
| def video_files(self): | ||||||||||||||||||||||||||||||||||||||
| return self.files | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _disconnect_update_process(self, process): | ||||||||||||||||||||||||||||||||||||||
| if process is None: | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for signal, slot in ( | ||||||||||||||||||||||||||||||||||||||
| (process.finished, self._on_update_process_finished), | ||||||||||||||||||||||||||||||||||||||
| (process.errorOccurred, self._on_update_process_error), | ||||||||||||||||||||||||||||||||||||||
| (process.readyRead, self._drain_update_process_output), | ||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| signal.disconnect(slot) | ||||||||||||||||||||||||||||||||||||||
| except (TypeError, RuntimeError): | ||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def check_for_updates(self, *, silent=True, delay_ms=0): | ||||||||||||||||||||||||||||||||||||||
| """Start an update check immediately or schedule it after a delay.""" | ||||||||||||||||||||||||||||||||||||||
| if self._closing: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -330,6 +347,29 @@ def _trigger_scheduled_update_check(self): | |||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| self._updater.check(silent=self._scheduled_update_check_silent) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _start_next_update_command(self): | ||||||||||||||||||||||||||||||||||||||
| """Start the next update backend. Return True if one was started.""" | ||||||||||||||||||||||||||||||||||||||
| if not self._update_commands: | ||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| backend_name, program, arguments = self._update_commands.pop(0) | ||||||||||||||||||||||||||||||||||||||
| self._current_update_backend = backend_name | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.status_bar.showMessage(f"Installing updates with {backend_name}...") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._update_process = QtCore.QProcess(self) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setProgram(program) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setArguments(arguments) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._update_process.finished.connect(self._on_update_process_finished) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.errorOccurred.connect(self._on_update_process_error) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.readyRead.connect(self._drain_update_process_output) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setProcessChannelMode(QtCore.QProcess.MergedChannels) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.start() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.logger.info("Starting update command: %s %s", program, " ".join(arguments)) | ||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _run_update_command(self, packages): | ||||||||||||||||||||||||||||||||||||||
| if not packages: | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -341,21 +381,31 @@ def _run_update_command(self, packages): | |||||||||||||||||||||||||||||||||||||
| self.status_bar.showMessage("Installing updates...") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._update_process_output = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_process = QtCore.QProcess(self) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setProgram(sys.executable) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setArguments(["-m", "pip", "install", "-U", *packages]) | ||||||||||||||||||||||||||||||||||||||
| self._update_attempt_outputs = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_commands = build_update_commands(packages) | ||||||||||||||||||||||||||||||||||||||
| self._current_update_backend = None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._update_process.finished.connect(self._on_update_process_finished) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.errorOccurred.connect(self._on_update_process_error) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.readyRead.connect(self._drain_update_process_output) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.setProcessChannelMode(QtCore.QProcess.MergedChannels) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.start() | ||||||||||||||||||||||||||||||||||||||
| if not self._start_next_update_command(): | ||||||||||||||||||||||||||||||||||||||
| self._cleanup_update_process() | ||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.warning( | ||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||
| "Update failed", | ||||||||||||||||||||||||||||||||||||||
| "No available installer backend was found.", | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+388
to
+395
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||
| def _drain_update_process_output(self, process=None): | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if process is None: | ||||||||||||||||||||||||||||||||||||||
| sender = self.sender() | ||||||||||||||||||||||||||||||||||||||
| process = sender if sender is not None else self._update_process | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+402
to
407
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||||||||||||||||
| data = bytes(self._update_process.readAll()) | ||||||||||||||||||||||||||||||||||||||
| data = bytes(process.readAll()) | ||||||||||||||||||||||||||||||||||||||
| if not data: | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -371,62 +421,114 @@ def _drain_update_process_output(self): | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _cleanup_update_process(self): | ||||||||||||||||||||||||||||||||||||||
| if self._update_process is not None: | ||||||||||||||||||||||||||||||||||||||
| self._disconnect_update_process(self._update_process) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.deleteLater() | ||||||||||||||||||||||||||||||||||||||
| self._update_process = None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._update_process_output = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_commands = [] | ||||||||||||||||||||||||||||||||||||||
| self._update_attempt_outputs = [] | ||||||||||||||||||||||||||||||||||||||
| self._current_update_backend = None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._progress_bar.hide() | ||||||||||||||||||||||||||||||||||||||
| self.status_bar.showMessage("www.deeplabcut.org") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _on_update_process_error(self, error): | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| process = self.sender() | ||||||||||||||||||||||||||||||||||||||
| if process is not self._update_process: | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if self._closing: | ||||||||||||||||||||||||||||||||||||||
| self._cleanup_update_process() | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| backend = self._current_update_backend or "installer" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| error_strings = { | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.FailedToStart: ( | ||||||||||||||||||||||||||||||||||||||
| "Process failed to start. Check that pip is available and you have sufficient permissions." | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.Crashed: "Update process crashed unexpectedly.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.Timedout: "Update process timed out.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.WriteError: "Could not write to update process.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.ReadError: "Could not read from update process.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.UnknownError: "An unknown error occurred.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.FailedToStart: f"The {backend} update process failed to start.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.Crashed: f"The {backend} update process crashed unexpectedly.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.Timedout: f"The {backend} update process timed out.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.WriteError: f"Could not write to the {backend} update process.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.ReadError: f"Could not read from the {backend} update process.", | ||||||||||||||||||||||||||||||||||||||
| QtCore.QProcess.UnknownError: f"An unknown {backend} update error occurred.", | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| message = error_strings.get(error, "An unknown error occurred.") | ||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.warning(self, "Update failed", message) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if process is not None: | ||||||||||||||||||||||||||||||||||||||
| self._drain_update_process_output(process) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| output = "".join(self._update_process_output).strip() | ||||||||||||||||||||||||||||||||||||||
| message = error_strings.get(error, f"An unknown {backend} update error occurred.") | ||||||||||||||||||||||||||||||||||||||
| failed_output = f"{message}\n\n{output}" if output else message | ||||||||||||||||||||||||||||||||||||||
| self.logger.warning(failed_output) | ||||||||||||||||||||||||||||||||||||||
| self._update_attempt_outputs.append(failed_output) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._disconnect_update_process(process) | ||||||||||||||||||||||||||||||||||||||
| self._update_process.deleteLater() | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+466
to
+467
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently this only works because the guard (
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| self._update_process = None | ||||||||||||||||||||||||||||||||||||||
|
C-Achard marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| self._update_process_output = [] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if self._start_next_update_command(): | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.warning( | ||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||
| "Update failed", | ||||||||||||||||||||||||||||||||||||||
| "No update backend completed successfully.\n\n" + "\n\n---\n\n".join(self._update_attempt_outputs), | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| self._cleanup_update_process() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _on_update_process_finished(self, exit_code, exit_status): | ||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+482
to
491
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._progress_bar.hide() | ||||||||||||||||||||||||||||||||||||||
| self._drain_update_process_output() | ||||||||||||||||||||||||||||||||||||||
| self._drain_update_process_output(process) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| output = "".join(self._update_process_output).strip() | ||||||||||||||||||||||||||||||||||||||
| backend = self._current_update_backend or "installer" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if exit_status == QtCore.QProcess.NormalExit and exit_code == 0: | ||||||||||||||||||||||||||||||||||||||
| self._progress_bar.hide() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.information( | ||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||
| "Update complete", | ||||||||||||||||||||||||||||||||||||||
| "The update completed successfully.\n\nPlease restart DeepLabCut to use the updated packages.", | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if output: | ||||||||||||||||||||||||||||||||||||||
| self.logger.info(output) | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.warning( | ||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||
| "Update failed", | ||||||||||||||||||||||||||||||||||||||
| "The update command did not complete successfully.\n\n" | ||||||||||||||||||||||||||||||||||||||
| f"{output or 'No additional output was captured.'}", | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| if output: | ||||||||||||||||||||||||||||||||||||||
| self.logger.error(output) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._cleanup_update_process() | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. calls |
||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| failed_output = ( | ||||||||||||||||||||||||||||||||||||||
| f"{backend} failed with exit code {exit_code}.\n\n{output or 'No additional output was captured.'}" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| self._update_attempt_outputs.append(failed_output) | ||||||||||||||||||||||||||||||||||||||
| self.logger.warning(failed_output) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._disconnect_update_process(process) | ||||||||||||||||||||||||||||||||||||||
| process.deleteLater() | ||||||||||||||||||||||||||||||||||||||
| self._update_process = None | ||||||||||||||||||||||||||||||||||||||
| self._update_process_output = [] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if self._start_next_update_command(): | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| QtWidgets.QMessageBox.warning( | ||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||
| "Update failed", | ||||||||||||||||||||||||||||||||||||||
| "No update backend completed successfully.\n\n" + "\n\n---\n\n".join(self._update_attempt_outputs), | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self._cleanup_update_process() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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.: