use an object for add-on metadata instead of reading the dict directly

also, sort disabled add-ons to the end
This commit is contained in:
Damien Elmes 2020-01-19 12:44:53 +10:00
parent ef8bb61fc3
commit 54ca4efb8e

View File

@ -67,6 +67,10 @@ class DownloadError:
exception: Optional[Exception] = None
# first arg is add-on id
DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]]
@dataclass
class UpdateInfo:
id: int
@ -74,8 +78,34 @@ class UpdateInfo:
max_point_version: Optional[int]
# first arg is add-on id
DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]]
@dataclass
class AddonMeta:
dir_name: str
human_name: str
enabled: bool
installed_at: int
ankiweb_id: Optional[int]
conflicts: List[str]
def ankiweb_id_for_dir(dir_name: str) -> Optional[int]:
m = re.match(r"^\d+", dir_name)
if m:
return int(m.group(0))
else:
return None
def addon_meta(dir_name: str, json_meta: Dict[str, Any]) -> AddonMeta:
return AddonMeta(
dir_name=dir_name,
human_name=json_meta.get("name", dir_name),
enabled=not json_meta.get("disabled"),
installed_at=json_meta.get("mod", 0),
ankiweb_id=ankiweb_id_for_dir(dir_name),
conflicts=json_meta.get("conflicts", []),
)
# fixme: this class should not have any GUI code in it
class AddonManager:
@ -99,6 +129,7 @@ class AddonManager:
f.actionAdd_ons.triggered.connect(self.onAddonsDialog)
sys.path.insert(0, self.addonsFolder())
# in new code, you may want all_addon_meta() instead
def allAddons(self):
l = []
for d in os.listdir(self.addonsFolder()):
@ -111,8 +142,8 @@ class AddonManager:
l = reversed(l)
return l
def managedAddons(self):
return [d for d in self.allAddons() if re.match(r"^\d+$", d)]
def all_addon_meta(self) -> Iterable[AddonMeta]:
return map(self.addon_meta, self.allAddons())
def addonsFolder(self, dir=None):
root = self.mw.pm.addonFolder()
@ -120,14 +151,13 @@ class AddonManager:
return root
return os.path.join(root, dir)
def loadAddons(self):
for dir in self.allAddons():
meta = self.addonMeta(dir)
if meta.get("disabled"):
def loadAddons(self) -> None:
for addon in self.all_addon_meta():
if not addon.enabled:
continue
self.dirty = True
try:
__import__(dir)
__import__(addon.dir_name)
except:
showWarning(
_(
@ -139,7 +169,7 @@ When loading '%(name)s':
%(traceback)s
"""
)
% dict(name=meta.get("name", dir), traceback=traceback.format_exc())
% dict(name=addon.human_name, traceback=traceback.format_exc())
)
def onAddonsDialog(self):
@ -148,9 +178,15 @@ When loading '%(name)s':
# Metadata
######################################################################
def addon_meta(self, dir_name: str) -> AddonMeta:
"""Get info about an installed add-on."""
json_obj = self.addonMeta(dir_name)
return addon_meta(dir_name, json_obj)
def _addonMetaPath(self, dir):
return os.path.join(self.addonsFolder(dir), "meta.json")
# in new code, use addon_meta() instead
def addonMeta(self, dir: str) -> Dict[str, Any]:
path = self._addonMetaPath(dir)
try:
@ -164,10 +200,6 @@ When loading '%(name)s':
with open(path, "w", encoding="utf8") as f:
json.dump(meta, f)
def isEnabled(self, dir):
meta = self.addonMeta(dir)
return not meta.get("disabled")
def toggleEnabled(self, dir, enable=None):
meta = self.addonMeta(dir)
enabled = enable if enable is not None else meta.get("disabled")
@ -187,37 +219,42 @@ and have been disabled: %(found)s"
meta["disabled"] = not enabled
self.writeAddonMeta(dir, meta)
def addonName(self, dir):
return self.addonMeta(dir).get("name", dir)
def annotatedName(self, dir):
buf = self.addonName(dir)
if not self.isEnabled(dir):
buf += _(" (disabled)")
return buf
def enabled_addon_ids(self) -> List[int]:
def enabled_ankiweb_addons(self) -> List[int]:
ids = []
for dir in self.managedAddons():
meta = self.addonMeta(dir)
if not meta.get("disabled"):
ids.append(int(dir))
for meta in self.all_addon_meta():
if meta.ankiweb_id is not None and meta.enabled:
ids.append(meta.ankiweb_id)
return ids
# Legacy helpers
######################################################################
def isEnabled(self, dir: str) -> bool:
return self.addon_meta(dir).enabled
def addonName(self, dir: str) -> str:
return self.addon_meta(dir).human_name
def addonConflicts(self, dir) -> List[str]:
return self.addon_meta(dir).conflicts
def annotatedName(self, dir: str) -> str:
meta = self.addon_meta(dir)
name = meta.human_name
if not meta.enabled:
name += _(" (disabled)")
return name
# Conflict resolution
######################################################################
def addonConflicts(self, dir):
return self.addonMeta(dir).get("conflicts", [])
def allAddonConflicts(self):
all_conflicts = defaultdict(list)
for dir in self.allAddons():
if not self.isEnabled(dir):
def allAddonConflicts(self) -> Dict[str, List[str]]:
all_conflicts: Dict[str, List[str]] = defaultdict(list)
for addon in self.all_addon_meta():
if not addon.enabled:
continue
conflicts = self.addonConflicts(dir)
for other_dir in conflicts:
all_conflicts[other_dir].append(dir)
for other_dir in addon.conflicts:
all_conflicts[other_dir].append(addon.dir_name)
return all_conflicts
def _disableConflicting(self, dir, conflicts=None):
@ -412,7 +449,8 @@ and have been disabled: %(found)s"
return need_update
def addon_is_latest(self, id: int, server_update: int) -> bool:
return self.addonMeta(str(id)).get("mod", 0) >= (server_update or 0)
meta = self.addon_meta(str(id))
return meta.installed_at >= server_update
# Add-on Config
######################################################################
@ -439,10 +477,10 @@ and have been disabled: %(found)s"
def addonFromModule(self, module):
return module.split(".")[0]
def configAction(self, addon):
def configAction(self, addon: str) -> Callable[[], Optional[bool]]:
return self._configButtonActions.get(addon)
def configUpdatedAction(self, addon):
def configUpdatedAction(self, addon: str) -> Callable[[Any], None]:
return self._configUpdatedActions.get(addon)
# Add-on Config API
@ -559,37 +597,51 @@ class AddonsDialog(QDialog):
saveGeom(self, "addons")
return QDialog.reject(self)
def redrawAddons(self):
def name_for_addon_list(self, addon: AddonMeta) -> str:
name = addon.human_name
if not addon.enabled:
return name + " " + _("(disabled)")
return name
def redrawAddons(self,) -> None:
addonList = self.form.addonList
mgr = self.mgr
self.addons = [(mgr.annotatedName(d), d) for d in mgr.allAddons()]
self.addons.sort()
self.addons = list(mgr.all_addon_meta())
self.addons.sort(key=lambda a: a.human_name.lower())
self.addons.sort(key=lambda a: a.enabled, reverse=True)
selected = set(self.selectedAddons())
addonList.clear()
for name, dir in self.addons:
for addon in self.addons:
name = self.name_for_addon_list(addon)
item = QListWidgetItem(name, addonList)
if not mgr.isEnabled(dir):
if not addon.enabled:
item.setForeground(Qt.gray)
if dir in selected:
if addon.dir_name in selected:
item.setSelected(True)
addonList.reset()
def _onAddonItemSelected(self, row_int):
def _onAddonItemSelected(self, row_int: int) -> None:
try:
addon = self.addons[row_int][1]
addon = self.addons[row_int]
except IndexError:
addon = ""
self.form.viewPage.setEnabled(bool(re.match(r"^\d+$", addon)))
return
self.form.viewPage.setEnabled(addon.ankiweb_id is not None)
self.form.config.setEnabled(
bool(self.mgr.getConfig(addon) or self.mgr.configAction(addon))
bool(
self.mgr.getConfig(addon.dir_name)
or self.mgr.configAction(addon.dir_name)
)
)
return
def selectedAddons(self):
def selectedAddons(self) -> List[str]:
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
return [self.addons[idx][1] for idx in idxs]
return [self.addons[idx].dir_name for idx in idxs]
def onlyOneSelected(self):
dirs = self.selectedAddons()
@ -939,7 +991,7 @@ def check_for_updates(
client = HttpClient()
def check():
return fetch_update_info(client, mgr.enabled_addon_ids())
return fetch_update_info(client, mgr.enabled_ankiweb_addons())
def update_info_received(future: Future):
# if syncing/in profile screen, defer message delivery