Skip to content

Commit d3d933a

Browse files
authored
Add ability to provide changelog for add-ons (#52)
This pull request adds support for a new changelog field to the add-on manifest and submission JSON schema, enabling tracking and validation of changes between add-on versions. The changes ensure that both the manifest and all related processing, validation, and test code can handle the new changelog field, including its translation in localized manifests. Schema and Manifest Updates: Added a changelog field to the AddonManifest class in _validate/addonManifest.py, and updated the manifest parsing logic to support reading and translating this field. [1] [2] Updated the JSON schema in _validate/addonVersion_schema.json to define the changelog field for both the base and translations, including examples and descriptions. [1] [2] Updated test data and example manifests to include the new changelog field. [1] [2] Data Model and Processing: Updated the AddonData dataclass and related processing functions in _validate/createJson.py to include changelog and its translations, handling None values properly. [1] [2] [3] Updated regeneration logic in _validate/regenerateTranslations.py to process the changelog field for both the main manifest and translations. Validation Logic: Added a new validation function checkChangelogMatches in _validate/validate.py to ensure the submission changelog matches the manifest, and integrated it into the submission validation pipeline. [1] [2] Tests: Added unit tests to verify validation of the changelog field, and updated existing tests and test data to include the new field and its expected values. [1] [2] These changes collectively introduce the changelog field into the add-on manifest and submission workflow, ensuring proper handling, validation, and testing throughout the codebase.
1 parent 7daee09 commit d3d933a

File tree

11 files changed

+116
-33
lines changed

11 files changed

+116
-33
lines changed

_validate/addonManifest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class AddonManifest(ConfigObj):
3131
# Long description with further information and instructions
3232
description = string(default=None)
3333
34+
# Document changes between the previous and the current versions.
35+
changelog = string(default=None)
36+
3437
# Name of the author or entity that created the add-on
3538
author = string()
3639
@@ -89,8 +92,8 @@ def __init__(self, input: str | TextIOBase, translatedInput: str | None = None):
8992
self._translatedConfig = None
9093
if translatedInput is not None:
9194
self._translatedConfig = ConfigObj(translatedInput, encoding="utf-8", default_encoding="utf-8")
92-
for key in ("summary", "description"):
93-
val: str = self._translatedConfig.get(key) # type: ignore[reportUnknownMemberType]
95+
for key in ("summary", "description", "changelog"):
96+
val: str | None = self._translatedConfig.get(key) # type: ignore[reportUnknownMemberType]
9497
if val:
9598
self[key] = val
9699

_validate/addonVersion_schema.json

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
"sourceURL": "https://github.com/nvaccess/addon-datastore/",
2929
"license": "GPL v2",
3030
"licenseURL": "https://github.com/nvaccess/addon-datastore/license.MD",
31+
"changelog": "New features",
3132
"translations": [
3233
{
3334
"language": "de",
3435
"displayName": "Mein Addon",
35-
"description": "erleichtert das Durchführen von xyz"
36+
"description": "erleichtert das Durchführen von xyz",
37+
"changelog": "Neue Funktionen"
3638
}
3739
],
3840
"reviewUrl": "https://github.com/nvaccess/addon-datastore/discussions/1942#discussioncomment-7453248",
@@ -228,6 +230,16 @@
228230
"title": "The URL of the license",
229231
"type": "string"
230232
},
233+
"changelog": {
234+
"$id": "#/properties/changelog",
235+
"default": "",
236+
"description": "Changes between the previous and the current version",
237+
"examples": [
238+
"New feature"
239+
],
240+
"title": "Add-on changelog (en)",
241+
"type": "string"
242+
},
231243
"legacy": {
232244
"$id": "#/properties/legacy",
233245
"default": false,
@@ -247,7 +259,8 @@
247259
{
248260
"language": "de",
249261
"displayName": "Mein Addon",
250-
"description": "erleichtert das Durchführen von xyz"
262+
"description": "erleichtert das Durchführen von xyz",
263+
"changelog": "Neue Funktionen"
251264
}
252265
]
253266
],
@@ -357,7 +370,8 @@
357370
{
358371
"language": "de",
359372
"displayName": "Mein Addon",
360-
"description": "erleichtert das Durchführen von xyz"
373+
"description": "erleichtert das Durchführen von xyz",
374+
"changelog": "Neue Funktionen"
361375
}
362376
],
363377
"required": [
@@ -393,6 +407,15 @@
393407
],
394408
"title": "The translated description",
395409
"type": "string"
410+
},
411+
"changelog": {
412+
"default": "",
413+
"description": "Translated description of changes between the previous and this add-on version",
414+
"examples": [
415+
"Neue Funktionen"
416+
],
417+
"title": "The translated changelog",
418+
"type": "string"
396419
}
397420
}
398421
}

_validate/createJson.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .manifestLoader import getAddonManifest, getAddonManifestLocalizations
1515
from .majorMinorPatch import MajorMinorPatch
1616
from .sha256 import sha256_checksum
17+
from .validate import parseConfigValue
1718

1819

1920
@dataclasses.dataclass
@@ -32,6 +33,7 @@ class AddonData:
3233
sourceURL: str
3334
license: str
3435
homepage: str | None
36+
changelog: str | None
3537
licenseURL: str | None
3638
submissionTime: int
3739
translations: list[dict[str, str]]
@@ -118,20 +120,22 @@ def _createDataclassMatchingJsonSchema(
118120
raise KeyError(f"Manifest missing required key '{key}'.")
119121

120122
# Add optional fields
121-
homepage: str | None = manifest.get("url") # type: ignore[reportUnknownMemberType]
122-
if not homepage or homepage == "None":
123-
homepage = None
124-
123+
homepage: str | None = parseConfigValue(manifest, "url")
124+
changelog: str | None = parseConfigValue(manifest, "changelog")
125125
translations: list[dict[str, str]] = []
126126
for langCode, translatedManifest in getAddonManifestLocalizations(manifest):
127+
# Add optional translated changelog.
128+
translatedChangelog: str | None = parseConfigValue(translatedManifest, "changelog")
129+
127130
try:
128-
translations.append(
129-
{
130-
"language": langCode,
131-
"displayName": cast(str, translatedManifest["summary"]),
132-
"description": cast(str, translatedManifest["description"]),
133-
},
134-
)
131+
translation: dict[str, str] = {
132+
"language": langCode,
133+
"displayName": cast(str, translatedManifest["summary"]),
134+
"description": cast(str, translatedManifest["description"]),
135+
}
136+
if translatedChangelog is not None:
137+
translation["changelog"] = translatedChangelog
138+
translations.append(translation)
135139
except KeyError as e:
136140
raise KeyError(f"Translation for {langCode} missing required key '{e.args[0]}'.") from e
137141

@@ -152,6 +156,7 @@ def _createDataclassMatchingJsonSchema(
152156
sourceURL=sourceUrl,
153157
license=licenseName,
154158
homepage=homepage,
159+
changelog=changelog,
155160
licenseURL=licenseUrl,
156161
submissionTime=getCurrentTime(),
157162
translations=translations,

_validate/regenerateTranslations.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import glob
77
import json
88
from urllib.request import urlretrieve
9+
from typing import cast
910

1011
from .manifestLoader import getAddonManifest, getAddonManifestLocalizations
12+
from .validate import parseConfigValue
1113

1214

1315
def regenerateJsonFile(filePath: str, errorFilePath: str | None) -> None:
@@ -23,15 +25,16 @@ def regenerateJsonFile(filePath: str, errorFilePath: str | None) -> None:
2325
with open(errorFilePath, "w") as errorFile:
2426
errorFile.write(f"Validation Errors:\n{manifest.errors}")
2527
return
26-
2728
for langCode, manifest in getAddonManifestLocalizations(manifest):
28-
addonData["translations"].append(
29-
{
30-
"language": langCode,
31-
"displayName": manifest["summary"],
32-
"description": manifest["description"],
33-
},
34-
)
29+
translatedChangelog: str | None = parseConfigValue(manifest, "changelog")
30+
translation: dict[str, str] = {
31+
"language": langCode,
32+
"displayName": cast(str, manifest["summary"]),
33+
"description": cast(str, manifest["description"]),
34+
}
35+
if translatedChangelog is not None:
36+
translation["changelog"] = translatedChangelog
37+
addonData["translations"].append(translation)
3538

3639
with open(filePath, "wt", encoding="utf-8") as f:
3740
json.dump(addonData, f, indent="\t", ensure_ascii=False)

_validate/validate.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,23 @@ def checkDescriptionMatches(manifest: AddonManifest, submission: JsonObjT) -> Va
134134
)
135135

136136

137+
def checkChangelogMatches(manifest: AddonManifest, submission: JsonObjT) -> ValidationErrorGenerator:
138+
"""The submission changelog must match the *.nvda-addon manifest changelog field."""
139+
changelog = parseConfigValue(manifest, "changelog")
140+
if changelog != submission.get("changelog"):
141+
yield (
142+
f"Submission 'changelog' must be set to '{changelog}' in json file."
143+
f" Instead got: '{submission.get('changelog')}'"
144+
)
145+
146+
137147
def checkUrlMatchesHomepage(manifest: AddonManifest, submission: JsonObjT) -> ValidationErrorGenerator:
138148
"""The submission homepage must match the *.nvda-addon manifest url field."""
139-
manifestUrl = manifest.get("url") # type: ignore[reportUnknownMemberType]
140-
if manifestUrl == "None":
141-
# The config default is None which is parsed by configobj as a string not a NoneType
142-
manifestUrl = None
149+
manifestUrl = parseConfigValue(manifest, "url")
143150
if manifestUrl != submission.get("homepage"):
144151
yield (
145-
f"Submission 'homepage' must be set to '{manifest.get('url')}' " # type: ignore[reportUnknownMemberType]
146-
f"in json file instead of {submission.get('homepage')}"
152+
f"Submission 'homepage' must be set to '{manifestUrl}' in json file."
153+
f" Instead got: {submission.get('homepage')}"
147154
)
148155

149156

@@ -171,6 +178,19 @@ def checkAddonId(
171178
)
172179

173180

181+
def parseConfigValue(manifest: AddonManifest, configKey: str) -> str | None:
182+
"""Converts a "None" config value to None.
183+
:param manifest: An add-on manifest.
184+
:param configKey: A key of an add-on manifest.
185+
:return: The parsed value for the provided config key.
186+
"""
187+
configValue = manifest.get(configKey) # type: ignore[reportUnknownMemberType]
188+
if configValue == "None":
189+
# The config default is None which is parsed by configobj as a string not a NoneType
190+
configValue = None
191+
return configValue # type: ignore[reportUnknownMemberType]
192+
193+
174194
VERSION_PARSE = re.compile(r"^(\d+)(?:$|(?:\.(\d+)$)|(?:\.(\d+)\.(\d+)$))")
175195

176196

@@ -332,6 +352,7 @@ def validateSubmission(submissionFilePath: str, verFilename: str) -> ValidationE
332352
manifest = getAddonManifest(addonDestPath)
333353
yield from checkSummaryMatchesDisplayName(manifest, submissionData)
334354
yield from checkDescriptionMatches(manifest, submissionData)
355+
yield from checkChangelogMatches(manifest, submissionData)
335356
yield from checkUrlMatchesHomepage(manifest, submissionData)
336357
yield from checkAddonId(manifest, submissionFilePath, submissionData)
337358
yield from checkMinNVDAVersionMatches(manifest, submissionData)

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ reportMissingTypeStubs = false
6767

6868
[tool.uv]
6969
default-groups = "all"
70-
python-preference = "only-system"
7170
environments = ["sys_platform == 'win32'"]
7271
required-version = ">=0.8"
7372

tests/testData/addons/fake/13.0.0.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"displayName": "mock addon",
1010
"description": "The description for the addon",
11+
"changelog": "Changes for this add-on version",
1112
"homepage": "https://nvaccess.org",
1213
"publisher": "Name of addon author or organisation",
1314
"minNVDAVersion": {
@@ -23,7 +24,7 @@
2324
"channel": "stable",
2425
"URL": "https://github.com/nvaccess/dont/use/this/address/fake.nvda-addon",
2526
"sha256-comment": "SHA for the fake.nvda-addon file",
26-
"sha256": "e27fa778cb99f83ececeb0bc089033929eab5a2fa475ce63e68f50b03b6ab585",
27+
"sha256": "50a8011a807665bcb8fdd177c276fef3b3f7f754796c5990ebe14e80c28b14ef",
2728
"sourceURL": "https://github.com/nvaccess/dont/use/this/address",
2829
"license": "GPL v2",
2930
"licenseURL": "https://www.gnu.org/licenses/gpl-2.0.html",

tests/testData/fake.nvda-addon

57 Bytes
Binary file not shown.

tests/testData/manifest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name = fake
22
summary = "mock addon"
33
description = """The description for the addon"""
4+
changelog = """Changes for this add-on version"""
45
author = "Name of addon author or organisation"
56
url = https://nvaccess.org
67
version = 13.0

tests/test_createJson.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def test_validVersion(self):
9999
sourceURL="https://example.com",
100100
license="GPL v2",
101101
homepage="https://example.com",
102+
changelog="""Changes for this add-on version""",
102103
licenseURL="https://www.gnu.org/licenses/gpl-2.0.html",
103104
submissionTime=createJson.getCurrentTime(),
104105
translations=[],

0 commit comments

Comments
 (0)