Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions source/addonStore/models/addon.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2023 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2022-2026 NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from collections.abc import Generator
import dataclasses
import json
import os
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
List,
Optional,
Protocol,
Union,
)

from requests.structures import CaseInsensitiveDict
Expand All @@ -38,7 +35,7 @@
AddonManifest,
)

AddonGUICollectionT = Dict[Channel, CaseInsensitiveDict["_AddonGUIModel"]]
AddonGUICollectionT = dict[Channel, CaseInsensitiveDict["_AddonGUIModel"]]
"""
Add-ons that have the same ID except differ in casing cause a path collision,
as add-on IDs are installed to a case insensitive path.
Expand Down Expand Up @@ -97,16 +94,20 @@ def name(self) -> str:
def listItemVMId(self) -> str:
return f"{self.addonId}-{self.channel}"

def asdict(self) -> Dict[str, Any]:
def asdict(self) -> dict[str, Any]:
assert dataclasses.is_dataclass(self)
jsonData = dataclasses.asdict(self)
for field in jsonData:
jsonDataCopy = jsonData.copy()
for field in jsonDataCopy:
# dataclasses.asdict parses NamedTuples to JSON arrays,
# rather than JSON object dictionaries,
# which is expected by add-on infrastructure.
fieldValue = getattr(self, field)
if isinstance(fieldValue, MajorMinorPatch):
jsonData[field] = fieldValue._asdict()
elif isinstance(fieldValue, VirusTotalScanResults):
jsonData["vtScanUrl"] = fieldValue.scanUrl
jsonData[field] = fieldValue.toDict()
return jsonData


Expand Down Expand Up @@ -337,10 +338,10 @@ class CachedAddonsModel:
cacheHash: Optional[str]
cachedLanguage: str
# AddonApiVersionT or the string .network._LATEST_API_VER
nvdaAPIVersion: Union[addonAPIVersion.AddonApiVersionT, str]
nvdaAPIVersion: addonAPIVersion.AddonApiVersionT | str


def _createInstalledStoreModelFromData(addon: Dict[str, Any]) -> InstalledAddonStoreModel:
def _createInstalledStoreModelFromData(addon: dict[str, Any]) -> InstalledAddonStoreModel:
return InstalledAddonStoreModel(
addonId=addon["addonId"],
publisher=addon["publisher"],
Expand All @@ -362,7 +363,7 @@ def _createInstalledStoreModelFromData(addon: Dict[str, Any]) -> InstalledAddonS
)


def _createStoreModelFromData(addon: Dict[str, Any]) -> AddonStoreModel:
def _createStoreModelFromData(addon: dict[str, Any]) -> AddonStoreModel:
return AddonStoreModel(
addonId=addon["addonId"],
displayName=addon["displayName"],
Expand Down Expand Up @@ -417,7 +418,7 @@ def _createStoreCollectionFromJson(jsonData: str) -> "AddonGUICollectionT":
See https://github.com/nvaccess/addon-datastore#api-data-generation-details
for details of the data.
"""
data: List[Dict[str, Any]] = json.loads(jsonData)
data: list[dict[str, Any]] = json.loads(jsonData)
addonCollection = _createAddonGUICollection()

for addon in data:
Expand Down
22 changes: 22 additions & 0 deletions source/addonStore/models/scanResults.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ def fromDict(cls, addon: dict[str, Any]) -> "VirusTotalScanResults | None":
log.error(f"Malformed add-on scan results.: {addon!r}", exc_info=True)
return None

def toDict(self) -> dict[str, list[dict[str, dict[str, int]]]]:
"""Store scan data in the same format as the original add-on scan results.

:return: A dictionary representing the scan results.
Comment on lines +48 to +50

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring for toDict says it stores data "in the same format as the original add-on scan results", but this method only serializes the scanResults structure and does not include the vtScanUrl/scanUrl portion (which is stored separately on the add-on dict). Please clarify the wording to reflect that it returns the format for addon["scanResults"] (or explicitly document how vtScanUrl is handled elsewhere).

Suggested change
"""Store scan data in the same format as the original add-on scan results.
:return: A dictionary representing the scan results.
"""Store scan data in the format used for ``addon["scanResults"]``.
This serializes only the VirusTotal scan results payload. The scan URL
(``vtScanUrl``, represented by :attr:`scanUrl`) is stored separately on the
add-on dictionary and is not included in this return value.
:return: A dictionary representing ``addon["scanResults"]``.

Copilot uses AI. Check for mistakes.
"""
return {
"virusTotal": [
{
"last_analysis_stats": {
"malicious": self.malicious,
"undetected": self.undetected,
"harmless": self.harmless,
"suspicious": self.suspicious,
"failure": self.failure,
"timeout": self.timeout,
"confirmed-timeout": self.confirmedTimeout,
"type-unsupported": self.typeUnsupported,
},
},
],
}

@property
def totalScans(self) -> int:
return self.malicious + self.undetected + self.harmless + self.suspicious
Expand Down
Loading
Loading