Coverage for graphica/gui/plugin_update_flow.py: 70.9%
124 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""プラグインの更新: 起動時(1日1回)とヘルプメニュー・環境設定から新しい版を調べ、一覧で知らせて、選んだものを入れ替える。
3調べる・落とす・入れ替えるのは別スレッド(TaskRunner)。入れ替えたプラグインは次の起動から使われる
4(読み込んだプラグインをその場で入れ替える仕組みは無い)。
5"""
6import datetime
7import logging
8import os
9import zipfile
11from graphica.core.app_paths import get_user_plugins_dir
12from graphica.core.i18n import tr
13from graphica.core.plugin_install import PluginInstallError, install_plugin_zip
14from graphica.core.plugin_manifest import PluginManifestError
15from graphica.core.plugin_updates import check_plugin_updates, check_update_zip
16from graphica.gui import app_settings, notify
17from graphica.gui.task_runner import TaskRunner
18from graphica.gui.updater import download_file, update_download_dir
20logger = logging.getLogger(__name__)
23def _today():
24 return datetime.date.today().isoformat()
27def _start_check(app, on_succeeded, on_failed):
28 if getattr(app, '_plugin_update_runner', None) is not None: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 return False
30 runner = TaskRunner(check_plugin_updates, get_user_plugins_dir(), parent=app)
31 runner.succeeded.connect(on_succeeded)
32 runner.failed.connect(on_failed)
33 app._plugin_update_runner = runner
34 runner.start()
35 return True
38def _finish(app):
39 app._plugin_update_runner = None
42def start_startup_check(app):
43 """1日1回だけ調べ、新しい版があるときだけ知らせる(失敗・無しなら何も出さない)。"""
44 if app_settings.PLUGIN_UPDATE_LAST_CHECK.read(app.settings) == _today():
45 return
47 def succeeded(updates):
48 _finish(app)
49 app_settings.PLUGIN_UPDATE_LAST_CHECK.write(app.settings, _today())
50 if updates:
51 offer_updates(app, updates)
53 def failed(message):
54 _finish(app)
55 logger.info("起動時のプラグインの更新の確認に失敗しました(オフライン等の可能性): %s", message)
57 _start_check(app, succeeded, failed)
60def check_now(app):
61 """メニュー・環境設定の「プラグインの更新を確認」。最新でも失敗でも結果を知らせる。"""
62 def succeeded(updates):
63 _finish(app)
64 app_settings.PLUGIN_UPDATE_LAST_CHECK.write(app.settings, _today())
65 if updates: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 offer_updates(app, updates)
67 else:
68 notify.information(app, tr("プラグインの更新"), tr("入れているプラグインはすべて最新です。"))
70 def failed(message):
71 _finish(app)
72 logger.warning("プラグインの更新の確認に失敗しました: %s", message)
73 notify.warning(app, tr("プラグインの更新"),
74 tr("更新を確認できませんでした。インターネットに接続しているか確かめてください。") + f"\n\n{message}")
76 if not _start_check(app, succeeded, failed): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 notify.information(app, tr("プラグインの更新"), tr("確認中です。完了までお待ちください。"))
80def offer_updates(app, updates):
81 """新しい版の一覧を出し、選んだものを入れ替える。"""
82 from graphica.gui.dialogs import PluginUpdatesDialog
83 dialog = PluginUpdatesDialog(updates, app)
84 if dialog.exec() != dialog.DialogCode.Accepted: 84 ↛ 85line 84 didn't jump to line 85 because the condition on line 84 was never true
85 return
86 selected = dialog.selected_updates()
87 if selected: 87 ↛ exitline 87 didn't return from function 'offer_updates' because the condition on line 87 was always true
88 apply_updates(app, selected)
91def download_and_install(updates, plugins_dir, download_dir, report_progress=None, is_cancelled=None):
92 """1件ずつ落として確かめ、入れ替える。結果は [(更新の情報, エラーの文 or None)]。TaskRunner 用。"""
93 results = []
94 for number, update in enumerate(updates, start=1):
95 if is_cancelled is not None and is_cancelled(): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 break
97 if report_progress is not None: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 report_progress(number - 1, len(updates), update['name'])
99 asset = update['zip']
100 path = os.path.join(download_dir, f"{update['folder']}-{update['latest']}.zip")
101 try:
102 if download_file(asset['url'], path, asset.get('size', 0), asset.get('sha256', ''), 102 ↛ 104line 102 didn't jump to line 104 because the condition on line 102 was never true
103 is_cancelled=is_cancelled) is None:
104 break
105 check_update_zip(path, update['folder'])
106 install_plugin_zip(path, plugins_dir)
107 results.append((update, None))
108 except (OSError, ValueError, zipfile.BadZipFile, PluginManifestError, PluginInstallError) as e:
109 # 1件の失敗でほかを止めない(理由は結果に出す)
110 logger.warning("プラグイン %s の更新に失敗しました", update['name'], exc_info=True)
111 results.append((update, str(e)))
112 finally:
113 if os.path.exists(path): 113 ↛ 94line 113 didn't jump to line 94 because the condition on line 113 was always true
114 try:
115 os.remove(path)
116 except OSError:
117 pass
118 return results
121def apply_updates(app, updates):
122 runner = TaskRunner(download_and_install, updates, get_user_plugins_dir(), update_download_dir(), parent=app)
123 runner.succeeded.connect(lambda results: _on_installed(app, results))
124 runner.failed.connect(lambda message: _on_install_failed(app, message))
125 app._plugin_update_runner = runner
126 app.statusBar().showMessage(tr("プラグインを更新しています..."))
127 runner.start()
130def _on_installed(app, results):
131 _finish(app)
132 app.statusBar().clearMessage()
133 done = [update for update, error in results if error is None]
134 failed = [(update, error) for update, error in results if error is not None]
135 lines = []
136 if done: 136 ↛ 139line 136 didn't jump to line 139 because the condition on line 136 was always true
137 lines.append(tr("次のプラグインを更新しました。Graphica を起動し直すと新しい版が使われます。"))
138 lines += [f" • {u['name']} {u['current']} → {u['latest']}" for u in done]
139 if failed: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 if lines:
141 lines.append("")
142 lines.append(tr("次のプラグインは更新できませんでした。"))
143 lines += [f" • {u['name']}: {error}" for u, error in failed]
144 if not lines: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 return
146 (notify.warning if failed else notify.information)(app, tr("プラグインの更新"), "\n".join(lines))
149def _on_install_failed(app, message):
150 _finish(app)
151 app.statusBar().clearMessage()
152 notify.warning(app, tr("プラグインの更新"), tr("プラグインを更新できませんでした。") + f"\n\n{message}")
155def stop(app):
156 """タブを閉じるとき: 動いている確認・更新を止めて待つ(閉じたタブに結果を返さない)。"""
157 runner = getattr(app, '_plugin_update_runner', None)
158 if runner is None: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true
159 return
160 try:
161 runner.succeeded.disconnect()
162 runner.failed.disconnect()
163 except (RuntimeError, TypeError):
164 pass
165 runner.requestInterruption()
166 runner.wait()
167 runner.deleteLater()
168 app._plugin_update_runner = None