diff --git a/_validate/addonManifest.py b/_validate/addonManifest.py index 7be77e3..27ab13f 100644 --- a/_validate/addonManifest.py +++ b/_validate/addonManifest.py @@ -31,6 +31,9 @@ class AddonManifest(ConfigObj): # Long description with further information and instructions description = string(default=None) + # Document changes between the previous and the current versions. + changelog = string(default=None) + # Name of the author or entity that created the add-on author = string() @@ -89,8 +92,8 @@ def __init__(self, input: str | TextIOBase, translatedInput: str | None = None): self._translatedConfig = None if translatedInput is not None: self._translatedConfig = ConfigObj(translatedInput, encoding="utf-8", default_encoding="utf-8") - for key in ("summary", "description"): - val: str = self._translatedConfig.get(key) # type: ignore[reportUnknownMemberType] + for key in ("summary", "description", "changelog"): + val: str | None = self._translatedConfig.get(key) # type: ignore[reportUnknownMemberType] if val: self[key] = val diff --git a/_validate/addonVersion_schema.json b/_validate/addonVersion_schema.json index 8662c15..7fdf70b 100644 --- a/_validate/addonVersion_schema.json +++ b/_validate/addonVersion_schema.json @@ -28,11 +28,13 @@ "sourceURL": "https://github.com/nvaccess/addon-datastore/", "license": "GPL v2", "licenseURL": "https://github.com/nvaccess/addon-datastore/license.MD", + "changelog": "New features", "translations": [ { "language": "de", "displayName": "Mein Addon", - "description": "erleichtert das Durchführen von xyz" + "description": "erleichtert das Durchführen von xyz", + "changelog": "Neue Funktionen" } ], "reviewUrl": "https://github.com/nvaccess/addon-datastore/discussions/1942#discussioncomment-7453248", @@ -124,7 +126,7 @@ "Makes doing XYZ easier" ], "title": "The description (en) of the addon", - "type": "string" + "type": ["string", "null"] }, "homepage": { "$id": "#/properties/homepage", @@ -135,7 +137,7 @@ ], "pattern": "^https:.*", "title": "The homepage URL for the addon.", - "type": "string" + "type": ["string", "null"] }, "minNVDAVersion": { "$ref": "#/$defs/canonicalVersion", @@ -226,7 +228,17 @@ "https://github.com/nvaccess/addon-datastore/license.MD" ], "title": "The URL of the license", - "type": "string" + "type": ["string", "null"] + }, + "changelog": { + "$id": "#/properties/changelog", + "default": "", + "description": "Changes between the previous and the current version", + "examples": [ + "New feature" + ], + "title": "Add-on changelog (en)", + "type": ["string", "null"] }, "legacy": { "$id": "#/properties/legacy", @@ -247,7 +259,8 @@ { "language": "de", "displayName": "Mein Addon", - "description": "erleichtert das Durchführen von xyz" + "description": "erleichtert das Durchführen von xyz", + "changelog": "Neue Funktionen" } ] ], @@ -357,7 +370,8 @@ { "language": "de", "displayName": "Mein Addon", - "description": "erleichtert das Durchführen von xyz" + "description": "erleichtert das Durchführen von xyz", + "changelog": "Neue Funktionen" } ], "required": [ @@ -392,7 +406,16 @@ "erleichtert das Durchführen von xyz" ], "title": "The translated description", - "type": "string" + "type": ["string", "null"] + }, + "changelog": { + "default": "", + "description": "Translated description of changes between the previous and this add-on version", + "examples": [ + "Neue Funktionen" + ], + "title": "The translated changelog", + "type": ["string", "null"] } } } diff --git a/_validate/createJson.py b/_validate/createJson.py index 5dd2b85..27fce46 100644 --- a/_validate/createJson.py +++ b/_validate/createJson.py @@ -21,7 +21,7 @@ class AddonData: addonId: str displayName: str URL: str - description: str + description: str | None sha256: str addonVersionName: str addonVersionNumber: dict[str, int] @@ -32,9 +32,10 @@ class AddonData: sourceURL: str license: str homepage: str | None + changelog: str | None licenseURL: str | None submissionTime: int - translations: list[dict[str, str]] + translations: list[dict[str, str | None]] def getSha256(addonPath: str) -> str: @@ -108,17 +109,31 @@ def _createDataclassMatchingJsonSchema( # Add optional fields homepage: str | None = manifest.get("url") # type: ignore[reportUnknownMemberType] - if not homepage or homepage == "None": + if homepage == "None": + # The config default is None + # which is parsed by configobj as a string not a NoneType homepage = None - - translations: list[dict[str, str]] = [] + changelog: str | None = manifest.get("changelog") # type: ignore[reportUnknownMemberType] + if changelog == "None": + # The config default is None + # which is parsed by configobj as a string not a NoneType + changelog = None + translations: list[dict[str, str | None]] = [] for langCode, translatedManifest in getAddonManifestLocalizations(manifest): + # Add optional translated changelog. + translatedChangelog: str | None = translatedManifest.get("changelog") # type: ignore[reportUnknownMemberType] + if translatedChangelog == "None": + # The config default is None + # which is parsed by configobj as a string not a NoneType + translatedChangelog = None + try: translations.append( { "language": langCode, "displayName": cast(str, translatedManifest["summary"]), "description": cast(str, translatedManifest["description"]), + "changelog": translatedChangelog, }, ) except KeyError as e: @@ -141,6 +156,7 @@ def _createDataclassMatchingJsonSchema( sourceURL=sourceUrl, license=licenseName, homepage=homepage, + changelog=changelog, licenseURL=licenseUrl, submissionTime=getCurrentTime(), translations=translations, diff --git a/_validate/regenerateTranslations.py b/_validate/regenerateTranslations.py index 0f015b7..d590279 100644 --- a/_validate/regenerateTranslations.py +++ b/_validate/regenerateTranslations.py @@ -23,13 +23,23 @@ def regenerateJsonFile(filePath: str, errorFilePath: str | None) -> None: with open(errorFilePath, "w") as errorFile: errorFile.write(f"Validation Errors:\n{manifest.errors}") return - + changelog = manifest.get("changelog") # type: ignore[reportUnknownMemberType] + if changelog == "None": + # The config default is None + # which is parsed by configobj as a string not a NoneType + changelog = None for langCode, manifest in getAddonManifestLocalizations(manifest): + translatedChangelog = manifest.get("changelog") # type: ignore[reportUnknownMemberType] + if translatedChangelog == "None": + # The config default is None + # which is parsed by configobj as a string not a NoneType + translatedChangelog = None addonData["translations"].append( { "language": langCode, "displayName": manifest["summary"], "description": manifest["description"], + "changelog": translatedChangelog, }, ) diff --git a/_validate/validate.py b/_validate/validate.py index 5490511..113aae4 100644 --- a/_validate/validate.py +++ b/_validate/validate.py @@ -134,6 +134,19 @@ def checkDescriptionMatches(manifest: AddonManifest, submission: JsonObjT) -> Va ) +def checkChangelogMatches(manifest: AddonManifest, submission: JsonObjT) -> ValidationErrorGenerator: + """The submission changelog must match the *.nvda-addon manifest changelog field.""" + changelog = manifest.get("changelog") # type: ignore[reportUnknownMemberType] + if changelog == "None": + # The config default is None which is parsed by configobj as a string not a NoneType + changelog = None + if changelog != submission.get("changelog"): + yield ( + f"Submission 'changelog' must be set to '{changelog}' " + f"in json file instead of {submission.get('changelog')}" + ) + + def checkUrlMatchesHomepage(manifest: AddonManifest, submission: JsonObjT) -> ValidationErrorGenerator: """The submission homepage must match the *.nvda-addon manifest url field.""" manifestUrl = manifest.get("url") # type: ignore[reportUnknownMemberType] @@ -332,6 +345,7 @@ def validateSubmission(submissionFilePath: str, verFilename: str) -> ValidationE manifest = getAddonManifest(addonDestPath) yield from checkSummaryMatchesDisplayName(manifest, submissionData) yield from checkDescriptionMatches(manifest, submissionData) + yield from checkChangelogMatches(manifest, submissionData) yield from checkUrlMatchesHomepage(manifest, submissionData) yield from checkAddonId(manifest, submissionFilePath, submissionData) yield from checkMinNVDAVersionMatches(manifest, submissionData) diff --git a/pyproject.toml b/pyproject.toml index f3ec269..40e7ea4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ reportMissingTypeStubs = false [tool.uv] default-groups = "all" -python-preference = "only-system" environments = ["sys_platform == 'win32'"] required-version = ">=0.8" diff --git a/tests/testData/addons/fake/13.0.0.json b/tests/testData/addons/fake/13.0.0.json index 9c275cd..87bed67 100644 --- a/tests/testData/addons/fake/13.0.0.json +++ b/tests/testData/addons/fake/13.0.0.json @@ -8,6 +8,7 @@ }, "displayName": "mock addon", "description": "The description for the addon", + "changelog": "Changes for this add-on version", "homepage": "https://nvaccess.org", "publisher": "Name of addon author or organisation", "minNVDAVersion": { @@ -23,7 +24,7 @@ "channel": "stable", "URL": "https://github.com/nvaccess/dont/use/this/address/fake.nvda-addon", "sha256-comment": "SHA for the fake.nvda-addon file", - "sha256": "e27fa778cb99f83ececeb0bc089033929eab5a2fa475ce63e68f50b03b6ab585", + "sha256": "50a8011a807665bcb8fdd177c276fef3b3f7f754796c5990ebe14e80c28b14ef", "sourceURL": "https://github.com/nvaccess/dont/use/this/address", "license": "GPL v2", "licenseURL": "https://www.gnu.org/licenses/gpl-2.0.html", diff --git a/tests/testData/fake.nvda-addon b/tests/testData/fake.nvda-addon index fe805d5..9b9a077 100644 Binary files a/tests/testData/fake.nvda-addon and b/tests/testData/fake.nvda-addon differ diff --git a/tests/testData/manifest.ini b/tests/testData/manifest.ini index dd89098..e5202e2 100644 --- a/tests/testData/manifest.ini +++ b/tests/testData/manifest.ini @@ -1,6 +1,7 @@ name = fake summary = "mock addon" description = """The description for the addon""" +changelog = """Changes for this add-on version""" author = "Name of addon author or organisation" url = https://nvaccess.org version = 13.0 diff --git a/tests/test_createJson.py b/tests/test_createJson.py index 6241bd7..a42f15f 100644 --- a/tests/test_createJson.py +++ b/tests/test_createJson.py @@ -99,6 +99,7 @@ def test_validVersion(self): sourceURL="https://example.com", license="GPL v2", homepage="https://example.com", + changelog="""Changes for this add-on version""", licenseURL="https://www.gnu.org/licenses/gpl-2.0.html", submissionTime=createJson.getCurrentTime(), translations=[], diff --git a/tests/test_validate.py b/tests/test_validate.py index 0c59e4e..fcab869 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -89,7 +89,7 @@ def test_missingHTTPsAndExt(self): class Validate_checkSha256(unittest.TestCase): """Tests for the checkSha256 function""" - validSha = "e27fa778cb99f83ececeb0bc089033929eab5a2fa475ce63e68f50b03b6ab585" + validSha = "50a8011a807665bcb8fdd177c276fef3b3f7f754796c5990ebe14e80c28b14ef" def test_valid(self): errors = validate.checkSha256(ADDON_PACKAGE, expectedSha=self.validSha.upper()) @@ -152,6 +152,32 @@ def test_invalid(self): ) +class Validate_checkChangelogMatches(unittest.TestCase): + def setUp(self): + self.submissionData = getValidAddonSubmission() + self.manifest = getAddonManifest() + + def test_valid(self): + errors = list( + validate.checkChangelogMatches(self.manifest, self.submissionData), + ) + self.assertEqual(errors, []) + + def test_invalid(self): + badChangelog = "bad changelog" + self.submissionData["changelog"] = badChangelog + errors = list( + validate.checkChangelogMatches(self.manifest, self.submissionData), + ) + self.assertEqual( + errors, + [ + f"Submission 'changelog' must be set to '{self.manifest['changelog']}' in json file" + f" instead of {badChangelog}", + ], + ) + + class Validate_checkAddonId(unittest.TestCase): """ Manifest 'name' considered source of truth for addonID