-
-
Notifications
You must be signed in to change notification settings - Fork 655
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
Add ability to customize automatic update channels for add-ons #17597
base: master
Are you sure you want to change the base?
Changes from all commits
ba90559
f7b039b
aa10ee4
5d1c1d4
110336e
7756b8d
481039f
b538505
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 | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,137 @@ | ||||||||||||||||||||||||||
# A part of NonVisual Desktop Access (NVDA) | ||||||||||||||||||||||||||
# Copyright (C) 2025 NV Access Limited | ||||||||||||||||||||||||||
# This file is covered by the GNU General Public License. | ||||||||||||||||||||||||||
# See the file COPYING for more details. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
from dataclasses import dataclass, replace | ||||||||||||||||||||||||||
import json | ||||||||||||||||||||||||||
import os | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
from logHandler import log | ||||||||||||||||||||||||||
import NVDAState | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
from .models.channel import UpdateChannel | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@dataclass | ||||||||||||||||||||||||||
class _AddonSettings: | ||||||||||||||||||||||||||
"""Settings for the Add-on Store management of an add-on. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
All options must have a default value. | ||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
updateChannel: UpdateChannel = UpdateChannel.DEFAULT | ||||||||||||||||||||||||||
"""Preferred update channels for the add-on.""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# TODO: migrate enabled/disabled/blocked state tracking | ||||||||||||||||||||||||||
# from addonHandler.AddonState/AddonStateCategory to here. | ||||||||||||||||||||||||||
# The set based state tracking could be replaced by maintaining state data on each add-on. | ||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||
# blocked: bool = False | ||||||||||||||||||||||||||
# """Whether the add-on is blocked from being running due to incompatibility.""" | ||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||
# disabled: bool = False | ||||||||||||||||||||||||||
# """Whether the add-on is disabled.""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
class _AddonStoreSettings: | ||||||||||||||||||||||||||
"""Settings for the Add-on Store.""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
_CACHE_FILENAME: str = "_cachedSettings.json" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
_showWarning: bool | ||||||||||||||||||||||||||
"""Show warning when opening Add-on Store.""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
_addonSettings: dict[str, _AddonSettings] | ||||||||||||||||||||||||||
"""Settings related to the management of add-ons""" | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def __init__(self): | ||||||||||||||||||||||||||
self._storeSettingsFile = os.path.join( | ||||||||||||||||||||||||||
NVDAState.WritePaths.addonStoreDir, | ||||||||||||||||||||||||||
self._CACHE_FILENAME, | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
self._showWarning = True | ||||||||||||||||||||||||||
self._addonSettings = {} | ||||||||||||||||||||||||||
self.load() | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def load(self): | ||||||||||||||||||||||||||
settingsDict = { | ||||||||||||||||||||||||||
"showWarning": True, | ||||||||||||||||||||||||||
"addonSettings": {}, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
Comment on lines
+58
to
+61
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 defaults are already given at lines 59-60, it seems WET to repeat them here. 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. sorry, bit lost here. Where else are these values defined? can you link the code? 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. In nvda/source/addonStore/settings.py Lines 53 to 54 in b538505
So it probably makes sense to
|
||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||
with open(self._storeSettingsFile, "r", encoding="utf-8") as storeSettingsFile: | ||||||||||||||||||||||||||
settingsDict = json.load(storeSettingsFile) | ||||||||||||||||||||||||||
except FileNotFoundError: | ||||||||||||||||||||||||||
pass | ||||||||||||||||||||||||||
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
|
||||||||||||||||||||||||||
except Exception: | ||||||||||||||||||||||||||
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. According to the docs, |
||||||||||||||||||||||||||
log.exception("Invalid add-on store settings") | ||||||||||||||||||||||||||
if NVDAState.shouldWriteToDisk(): | ||||||||||||||||||||||||||
os.remove(self._storeSettingsFile) | ||||||||||||||||||||||||||
SaschaCowley marked this conversation as resolved.
Show resolved
Hide resolved
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
|
||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||
if not isinstance(settingsDict["addonSettings"], dict): | ||||||||||||||||||||||||||
raise ValueError("addonSettings must be a dict") | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if not isinstance(settingsDict["showWarning"], bool): | ||||||||||||||||||||||||||
raise ValueError("showWarning must be a bool") | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
except (KeyError, ValueError): | ||||||||||||||||||||||||||
log.exception(f"Invalid add-on store cache:\n{settingsDict}") | ||||||||||||||||||||||||||
if NVDAState.shouldWriteToDisk(): | ||||||||||||||||||||||||||
os.remove(self._storeSettingsFile) | ||||||||||||||||||||||||||
SaschaCowley marked this conversation as resolved.
Show resolved
Hide resolved
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._showWarning = settingsDict["showWarning"] | ||||||||||||||||||||||||||
for addonId, settings in settingsDict["addonSettings"].items(): | ||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||
updateChannel = UpdateChannel(settings["updateChannel"]) | ||||||||||||||||||||||||||
except ValueError: | ||||||||||||||||||||||||||
log.exception(f"Invalid add-on settings for {addonId}:\n{settings}. Ignoring settings") | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||
self._addonSettings[addonId] = _AddonSettings( | ||||||||||||||||||||||||||
updateChannel=updateChannel, | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def save(self): | ||||||||||||||||||||||||||
if not NVDAState.shouldWriteToDisk(): | ||||||||||||||||||||||||||
log.error("Shouldn't write to disk, not saving add-on store settings") | ||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||
seanbudd marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
settingsDict = { | ||||||||||||||||||||||||||
"showWarning": self._showWarning, | ||||||||||||||||||||||||||
"addonSettings": { | ||||||||||||||||||||||||||
addonId: { | ||||||||||||||||||||||||||
"updateChannel": addonSettings.updateChannel.value, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
for addonId, addonSettings in self._addonSettings.items() | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
with open(self._storeSettingsFile, "w", encoding="utf-8") as storeSettingsFile: | ||||||||||||||||||||||||||
json.dump(settingsDict, storeSettingsFile, ensure_ascii=False) | ||||||||||||||||||||||||||
SaschaCowley marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def setAddonSettings(self, addonId: str, **kwargs): | ||||||||||||||||||||||||||
"""Set settings for an add-on. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Keyword arguments the same as _AddonSettings: | ||||||||||||||||||||||||||
- updateChannel: Update channel for the add-on. | ||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||
if addonId not in self._addonSettings: | ||||||||||||||||||||||||||
self._addonSettings[addonId] = _AddonSettings(**kwargs) | ||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||
self._addonSettings[addonId] = replace(self._addonSettings[addonId], **kwargs) | ||||||||||||||||||||||||||
self.save() | ||||||||||||||||||||||||||
SaschaCowley marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def getAddonSettings(self, addonId: str) -> _AddonSettings: | ||||||||||||||||||||||||||
"""Get settings for an add-on. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Returns default settings if the add-on has no stored settings. | ||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||
return self._addonSettings.get(addonId, _AddonSettings()) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@property | ||||||||||||||||||||||||||
def showWarning(self) -> bool: | ||||||||||||||||||||||||||
return self._showWarning | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@showWarning.setter | ||||||||||||||||||||||||||
def showWarning(self, showWarning: bool): | ||||||||||||||||||||||||||
self._showWarning = showWarning | ||||||||||||||||||||||||||
self.save() | ||||||||||||||||||||||||||
SaschaCowley marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# A part of NonVisual Desktop Access (NVDA) | ||
# Copyright (C) 2006-2024 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, | ||
# Copyright (C) 2006-2025 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, | ||
# Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith, | ||
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka | ||
# This file is covered by the GNU General Public License. | ||
|
@@ -13,7 +13,7 @@ | |
#: provide an upgrade step (@see profileUpgradeSteps.py). An upgrade step does not need to be added when | ||
#: just adding a new element to (or removing from) the schema, only when old versions of the config | ||
#: (conforming to old schema versions) will not work correctly with the new schema. | ||
latestSchemaVersion = 14 | ||
latestSchemaVersion = 15 | ||
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. This may need to be increased if #17505 is merged first. |
||
|
||
#: The configuration specification string | ||
#: @type: String | ||
|
@@ -335,9 +335,11 @@ | |
playErrorSound = integer(0, 1, default=0) | ||
|
||
[addonStore] | ||
showWarning = boolean(default=true) | ||
automaticUpdates = option("notify", "disabled", default="notify") | ||
baseServerURL = string(default="") | ||
# UpdateChannel values: | ||
# same channel (default), any channel, do not update, stable, beta & dev, beta, dev | ||
defaultUpdateChannel = integer(0, 6, default=0) | ||
""" | ||
|
||
#: The configuration specification | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# A part of NonVisual Desktop Access (NVDA) | ||
# Copyright (C) 2016-2024 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter | ||
# Copyright (C) 2016-2025 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter | ||
# This file is covered by the GNU General Public License. | ||
# See the file COPYING for more details. | ||
|
||
|
@@ -471,3 +471,15 @@ def _friendlyNameToEndpointId(friendlyName: str) -> str | None: | |
# Proceed to the next device state. | ||
continue | ||
return None | ||
|
||
|
||
def upgradeConfigFrom_14_to_15(profile: ConfigObj) -> None: | ||
""" | ||
Remove the addonStore.showWarning setting. | ||
""" | ||
if "addonStore" in profile and "showWarning" in profile["addonStore"]: | ||
from addonStore.dataManager import addonDataManager | ||
|
||
addonDataManager.storeSettings.showWarning = profile["addonStore"].as_bool("showWarning") | ||
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. What happens if the config contains an invalid value? 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. I believe when there is an invalid config it fails to load in NVDA and resets the users config. 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. Validation happens after upgrade (the old config could be invalid according to the new schema, so it logically has to happen in this order), so there's no guarantee that you'll get a bool. |
||
del profile["addonStore"]["showWarning"] | ||
log.debug("Removed addonStore.showWarning setting.") |
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.
Has this intentionally not been done?
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.
yep, out of scope for this PR
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.
Is there an issue for it? If not, can you create one?