Skip to content
Open
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
32 changes: 17 additions & 15 deletions .github/workflows/testCode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,38 @@ on:

jobs:
testCode:

runs-on: windows-latest
strategy:
matrix:
python-version: [3.13]
steps:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
architecture: x86
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox
- name: Test with tox
architecture: x64
- name: Run unit tests
shell: cmd
# Run automated/unit tests
run: tox
- name: Lint with flake8
run: .\rununittests.bat
- name: Lint
shell: cmd
# Check code with the linter
run: .\runlint.ps1
run: .\runlint.bat
- name: Validate metadata
shell: cmd
# E2E: test to check the script can be run, no need to actually test the file.
# The internal checks are covered with unit tests.
run: .\runvalidate.ps1 --dry-run _test/testData/addons/fake/13.0.json _tests\testData\nvdaAPIVersions.json
run: .\runvalidate.bat --dry-run _test/testData/addons/fake/13.0.json tests\testData\nvdaAPIVersions.json
- name: Get sha256
shell: cmd
# E2E: test to check the script can be run
run: .\runsha.ps1 _tests\testData\fake.nvda-addon
run: .\runsha.bat tests\testData\fake.nvda-addon
- name: Generate json file
shell: cmd
# E2E: test to check the script can be run
run: .\runcreatejson.ps1 -f _tests\testData\fake.nvda-addon --dir _tests\testOutput\test_runcreatejson --channel=stable --publisher=fakepublisher --sourceUrl=https://github.com/fake/ --url=https://github.com/fake.nvda-addon --licName="GPL v2" --licUrl="https://www.gnu.org/licenses/gpl-2.0.html"
run: .\runcreatejson.bat -f tests\testData\fake.nvda-addon --dir tests\testOutput\test_runcreatejson --channel=stable --publisher=fakepublisher --sourceUrl=https://github.com/fake/ --url=https://github.com/fake.nvda-addon --licName="GPL v2" --licUrl="https://www.gnu.org/licenses/gpl-2.0.html"
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.tox
.venv
__pycache__
_tests/testOutput
testOutput
*.egg-info
105 changes: 105 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# https://pre-commit.ci/
# Configuration for Continuous Integration service
ci:
# Can't run Windows scons scripts on Linux.
# Pyright does not seem to work in pre-commit CI
skip: [unitTest, pyright]
autoupdate_schedule: monthly
autoupdate_commit_msg: "Pre-commit auto-update"
autofix_commit_msg: "Pre-commit auto-fix"
submodules: true

default_language_version:
python: python3.13

repos:
- repo: https://github.com/pre-commit-ci/pre-commit-ci-config
rev: v1.6.1
hooks:
- id: check-pre-commit-ci-config

- repo: meta
hooks:
# ensures that exclude directives apply to any file in the repository.
- id: check-useless-excludes
# ensures that the configured hooks apply to at least one file in the repository.
- id: check-hooks-apply

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
# Prevents commits to certain branches
- id: no-commit-to-branch
args: ["--branch", "main"]
# Checks that large files have not been added. Default cut-off for "large" files is 500kb.
- id: check-added-large-files
# Checks python syntax
- id: check-ast
# Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time)
- id: check-case-conflict
# Checks for artifacts from resolving merge conflicts.
- id: check-merge-conflict
# Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs.
- id: debug-statements
# Removes trailing whitespace.
- id: trailing-whitespace
types_or: [python, batch, markdown, toml, yaml, powershell]
# Ensures all files end in 1 (and only 1) newline.
- id: end-of-file-fixer
types_or: [python, batch, markdown, toml, yaml, powershell]
# Removes the UTF-8 BOM from files that have it.
# See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding
- id: fix-byte-order-marker
types_or: [python, batch, markdown, toml, yaml, powershell]
# Validates TOML files.
- id: check-toml
# Validates YAML files.
- id: check-yaml
# Ensures that links to lines in files under version control point to a particular commit.
- id: check-vcs-permalinks
# Avoids using reserved Windows filenames.
- id: check-illegal-windows-names
# Checks that tests are named test_*.py.
- id: name-tests-test
args: ["--unittest"]

- repo: https://github.com/asottile/add-trailing-comma
rev: v3.2.0
hooks:
# Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables,
# if a trailing comma is added.
# This adds a trailing comma to args/iterable items in case it was missed.
- id: add-trailing-comma

- repo: https://github.com/astral-sh/ruff-pre-commit
# Matches Ruff version in pyproject.
rev: v0.13.0
hooks:
- id: ruff
name: lint with ruff
args: [ --fix ]
- id: ruff-format
name: format with ruff

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.405
hooks:
- id: pyright
name: Check types with pyright

- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.8.17
hooks:
- id: uv-lock
name: Verify uv lock file
# Override python interpreter from .python-versions as that is too strict for pre-commit.ci
args: ["-p3.13"]

- repo: local
hooks:
- id: unitTest
name: unit tests
entry: ./rununittests.bat
language: script
pass_filenames: false
types_or: [python, batch]
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cpython-3.13-windows-x86_64-none
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The Action aims to validate the metadata of add-ons submitted to
* The `*.nvda-addon` file can be downloaded
* The Sha256 of the downloaded `*.nvda-addon` file matches.
* Check data matches the addon's manifest file.
* The manifest exists in the downloaded `*.nvda-addon` file and can be loaded by the `AddonManifest` class.
* The manifest exists in the downloaded `*.nvda-addon` file and can be loaded by the `AddonManifest` class.
* The submission addonName matches the manifest summary field
* The submission description matches the manifest description field
* The homepage URL matches the manifest URL field
Expand All @@ -40,8 +40,7 @@ From cmd.exe:

To test the scripts used in this action, you can run the unit tests.

1. Install [tox](https://pypi.org/project/tox): `pip install tox`
1. `tox`
1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/)

## Python linting

Expand Down
74 changes: 34 additions & 40 deletions _validate/addonManifest.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
#!/usr/bin/env python

# Copyright (C) 2022-2023 NV Access Limited
# Copyright (C) 2022-2025 NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html

import os
import sys
from typing import (
Optional,
TextIO,
Tuple,
)
from io import StringIO
from io import StringIO, TextIOBase
from typing import Any, cast

from configobj import ConfigObj
from configobj.validate import Validator, ValidateError

sys.path.append(os.path.dirname(__file__))
# E402 module level import not at top of file
from majorMinorPatch import MajorMinorPatch # noqa:E402
del sys.path[-1]
from .majorMinorPatch import MajorMinorPatch

ApiVersionT = tuple[int, int, int] # major, minor, patch


class AddonManifest(ConfigObj):
"""From the NVDA addonHandler module. Should be kept in sync.
Add-on manifest file. It contains metadata about an NVDA add-on package. """
configspec = ConfigObj(StringIO(
"""
Add-on manifest file. It contains metadata about an NVDA add-on package."""

configspec = ConfigObj(
StringIO(
"""
# NVDA Add-on Manifest configuration specification
# Add-on unique name
# Suggested convention is lowerCamelCase.
Expand Down Expand Up @@ -66,56 +59,57 @@ class AddonManifest(ConfigObj):
# "0.0.0" is also valid.
# The final integer can be left out, and in that case will default to 0. E.g. 2019.1

"""
))
""",
),
)

def __init__(self, input: TextIO, translatedInput: Optional[TextIO] = None):
""" Constructs an L{AddonManifest} instance from manifest string data
@param input: data to read the manifest information
@param translatedInput: translated manifest input
def __init__(self, input: str | TextIOBase, translatedInput: str | None = None):
"""Constructs an L{AddonManifest} instance from manifest string data
:param input: data to read the manifest information. Can be a filename or a file-like object.
:param translatedInput: translated manifest input
"""
super().__init__(
super().__init__( # type: ignore[reportUnknownMemberType]
input,
configspec=self.configspec,
encoding='utf-8',
default_encoding='utf-8',
encoding="utf-8",
default_encoding="utf-8",
)
self._errors: Optional[str] = None
val = Validator({"apiVersion": validate_apiVersionString})
result = self.validate(val, copy=True, preserve_errors=True)
self._errors: str | None = None
validator = Validator({"apiVersion": validate_apiVersionString})
result = self.validate(validator, copy=True, preserve_errors=True) # type: ignore[reportUnknownMemberType]
if result is not True:
self._errors = result
elif self._validateApiVersionRange() is not True:
self._errors = "Constraint not met: minimumNVDAVersion ({}) <= lastTestedNVDAVersion ({})".format(
self.get("minimumNVDAVersion"),
self.get("lastTestedNVDAVersion")
cast(ApiVersionT, self.get("minimumNVDAVersion")), # type: ignore[reportUnknownMemberType]
cast(ApiVersionT, self.get("lastTestedNVDAVersion")), # type: ignore[reportUnknownMemberType]
)
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 = self._translatedConfig.get(key)
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]
if val:
self[key] = val

@property
def errors(self) -> str:
def errors(self) -> str | None:
return self._errors

def _validateApiVersionRange(self) -> bool:
lastTested = self.get("lastTestedNVDAVersion")
minRequiredVersion = self.get("minimumNVDAVersion")
lastTested = cast(ApiVersionT, self.get("lastTestedNVDAVersion")) # type: ignore[reportUnknownMemberType]
minRequiredVersion = cast(ApiVersionT, self.get("minimumNVDAVersion")) # type: ignore[reportUnknownMemberType]
return minRequiredVersion <= lastTested


def validate_apiVersionString(value: str) -> Tuple[int, int, int]:
def validate_apiVersionString(value: str | Any) -> ApiVersionT:
"""From the NVDA addonHandler module. Should be kept in sync."""
if not value or value == "None":
return (0, 0, 0)
if not isinstance(value, str):
raise ValidateError(
"Expected an apiVersion in the form of a string. "
f"e.g. '2019.1.0' instead of {value} (type {type(value)})"
f"e.g. '2019.1.0' instead of {value} (type {type(value)})",
)
try:
versionParsed = MajorMinorPatch.getFromStr(value)
Expand Down
Loading
Loading