Skip to content

Commit 770a1ee

Browse files
authored
add UV, pre-commit, ruff, pyright (#47)
This pull request introduces a major overhaul of the project's Python environment management, CI configuration, and developer tooling. The changes replace the legacy tox and flake8 setup with modern tools such as uv, ruff, and pyright, streamline Windows batch/PowerShell scripts, and update CI workflows to use the latest GitHub Actions versions. The new configuration files (pyproject.toml, .pre-commit-config.yml, .python-version) standardize dependencies and development practices, while redundant or outdated files are removed. Environment and Dependency Management: Added pyproject.toml to define dependencies, build system, linting, and testing tools, replacing legacy requirements and configuration files. Introduced uv for dependency management and updated Python version requirements to 3.13. Added .python-version to specify the required Python interpreter for consistency across development environments. Continuous Integration and Pre-commit: Added .pre-commit-config.yml to configure automated linting, formatting, and type checking with ruff, pyright, and other hooks. This replaces manual linting steps and integrates with pre-commit CI. Updated .github/workflows/testCode.yaml to use the latest GitHub Actions versions, switch from PowerShell to batch scripts for test/lint/validation steps, and utilize uv for environment setup. Script Modernization: Replaced PowerShell scripts (*.ps1) for linting, testing, and validation with batch scripts (*.bat) that invoke Python tools via uv, improving Windows compatibility and simplifying execution. [1] [2] [3] [4] [5] [6] Added a robust ensureuv.ps1 script to ensure the correct version of uv is installed or updated, with user-friendly prompts for installation via WinGet or the official script. Cleanup and Removal: Removed legacy configuration and utility files, including tox.ini, PowerShell scripts for old virtualenv management, and outdated lint/test scripts. [1] [2] [3] [4] [5] [6] [7] These changes modernize the project's development workflow, improve CI reliability, and ensure consistent environments for contributors.
1 parent c747ccb commit 770a1ee

38 files changed

+1049
-922
lines changed

.github/workflows/testCode.yaml

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,38 @@ on:
88

99
jobs:
1010
testCode:
11-
1211
runs-on: windows-latest
1312
strategy:
1413
matrix:
1514
python-version: [3.13]
1615
steps:
16+
- name: Install the latest version of uv
17+
uses: astral-sh/setup-uv@v6
1718
- name: Checkout code
18-
uses: actions/checkout@v3
19+
uses: actions/checkout@v5
1920
- name: Set up Python ${{ matrix.python-version }}
20-
uses: actions/setup-python@v3
21+
uses: actions/setup-python@v5
2122
with:
2223
python-version: ${{ matrix.python-version }}
23-
architecture: x86
24-
- name: Install dependencies
25-
run: |
26-
python -m pip install --upgrade pip
27-
python -m pip install tox
28-
- name: Test with tox
24+
architecture: x64
25+
- name: Run unit tests
26+
shell: cmd
2927
# Run automated/unit tests
30-
run: tox
31-
- name: Lint with flake8
28+
run: .\rununittests.bat
29+
- name: Lint
30+
shell: cmd
3231
# Check code with the linter
33-
run: .\runlint.ps1
32+
run: .\runlint.bat
3433
- name: Validate metadata
34+
shell: cmd
3535
# E2E: test to check the script can be run, no need to actually test the file.
3636
# The internal checks are covered with unit tests.
37-
run: .\runvalidate.ps1 --dry-run _test/testData/addons/fake/13.0.json _tests\testData\nvdaAPIVersions.json
37+
run: .\runvalidate.bat --dry-run _test/testData/addons/fake/13.0.json tests\testData\nvdaAPIVersions.json
3838
- name: Get sha256
39+
shell: cmd
3940
# E2E: test to check the script can be run
40-
run: .\runsha.ps1 _tests\testData\fake.nvda-addon
41+
run: .\runsha.bat tests\testData\fake.nvda-addon
4142
- name: Generate json file
43+
shell: cmd
4244
# E2E: test to check the script can be run
43-
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"
45+
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"

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.tox
21
.venv
32
__pycache__
4-
_tests/testOutput
3+
testOutput
4+
*.egg-info

.pre-commit-config.yaml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# https://pre-commit.ci/
2+
# Configuration for Continuous Integration service
3+
ci:
4+
# Can't run Windows scons scripts on Linux.
5+
# Pyright does not seem to work in pre-commit CI
6+
skip: [unitTest, pyright]
7+
autoupdate_schedule: monthly
8+
autoupdate_commit_msg: "Pre-commit auto-update"
9+
autofix_commit_msg: "Pre-commit auto-fix"
10+
submodules: true
11+
12+
default_language_version:
13+
python: python3.13
14+
15+
repos:
16+
- repo: https://github.com/pre-commit-ci/pre-commit-ci-config
17+
rev: v1.6.1
18+
hooks:
19+
- id: check-pre-commit-ci-config
20+
21+
- repo: meta
22+
hooks:
23+
# ensures that exclude directives apply to any file in the repository.
24+
- id: check-useless-excludes
25+
# ensures that the configured hooks apply to at least one file in the repository.
26+
- id: check-hooks-apply
27+
28+
- repo: https://github.com/pre-commit/pre-commit-hooks
29+
rev: v6.0.0
30+
hooks:
31+
# Prevents commits to certain branches
32+
- id: no-commit-to-branch
33+
args: ["--branch", "main"]
34+
# Checks that large files have not been added. Default cut-off for "large" files is 500kb.
35+
- id: check-added-large-files
36+
# Checks python syntax
37+
- id: check-ast
38+
# Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time)
39+
- id: check-case-conflict
40+
# Checks for artifacts from resolving merge conflicts.
41+
- id: check-merge-conflict
42+
# Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs.
43+
- id: debug-statements
44+
# Removes trailing whitespace.
45+
- id: trailing-whitespace
46+
types_or: [python, batch, markdown, toml, yaml, powershell]
47+
# Ensures all files end in 1 (and only 1) newline.
48+
- id: end-of-file-fixer
49+
types_or: [python, batch, markdown, toml, yaml, powershell]
50+
# Removes the UTF-8 BOM from files that have it.
51+
# See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding
52+
- id: fix-byte-order-marker
53+
types_or: [python, batch, markdown, toml, yaml, powershell]
54+
# Validates TOML files.
55+
- id: check-toml
56+
# Validates YAML files.
57+
- id: check-yaml
58+
# Ensures that links to lines in files under version control point to a particular commit.
59+
- id: check-vcs-permalinks
60+
# Avoids using reserved Windows filenames.
61+
- id: check-illegal-windows-names
62+
# Checks that tests are named test_*.py.
63+
- id: name-tests-test
64+
args: ["--unittest"]
65+
66+
- repo: https://github.com/asottile/add-trailing-comma
67+
rev: v3.2.0
68+
hooks:
69+
# Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables,
70+
# if a trailing comma is added.
71+
# This adds a trailing comma to args/iterable items in case it was missed.
72+
- id: add-trailing-comma
73+
74+
- repo: https://github.com/astral-sh/ruff-pre-commit
75+
# Matches Ruff version in pyproject.
76+
rev: v0.13.0
77+
hooks:
78+
- id: ruff
79+
name: lint with ruff
80+
args: [ --fix ]
81+
- id: ruff-format
82+
name: format with ruff
83+
84+
- repo: https://github.com/RobertCraigie/pyright-python
85+
rev: v1.1.405
86+
hooks:
87+
- id: pyright
88+
name: Check types with pyright
89+
90+
- repo: https://github.com/astral-sh/uv-pre-commit
91+
rev: 0.8.17
92+
hooks:
93+
- id: uv-lock
94+
name: Verify uv lock file
95+
# Override python interpreter from .python-versions as that is too strict for pre-commit.ci
96+
args: ["-p3.13"]
97+
98+
- repo: local
99+
hooks:
100+
- id: unitTest
101+
name: unit tests
102+
entry: ./rununittests.bat
103+
language: script
104+
pass_filenames: false
105+
types_or: [python, batch]

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cpython-3.13-windows-x86_64-none

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The Action aims to validate the metadata of add-ons submitted to
1313
* The `*.nvda-addon` file can be downloaded
1414
* The Sha256 of the downloaded `*.nvda-addon` file matches.
1515
* Check data matches the addon's manifest file.
16-
* The manifest exists in the downloaded `*.nvda-addon` file and can be loaded by the `AddonManifest` class.
16+
* The manifest exists in the downloaded `*.nvda-addon` file and can be loaded by the `AddonManifest` class.
1717
* The submission addonName matches the manifest summary field
1818
* The submission description matches the manifest description field
1919
* The homepage URL matches the manifest URL field
@@ -40,8 +40,7 @@ From cmd.exe:
4040

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

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

4645
## Python linting
4746

_validate/addonManifest.py

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
1-
#!/usr/bin/env python
2-
3-
# Copyright (C) 2022-2023 NV Access Limited
1+
# Copyright (C) 2022-2025 NV Access Limited
42
# This file may be used under the terms of the GNU General Public License, version 2 or later.
53
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
64

7-
import os
8-
import sys
9-
from typing import (
10-
Optional,
11-
TextIO,
12-
Tuple,
13-
)
14-
from io import StringIO
5+
from io import StringIO, TextIOBase
6+
from typing import Any, cast
157

168
from configobj import ConfigObj
179
from configobj.validate import Validator, ValidateError
1810

19-
sys.path.append(os.path.dirname(__file__))
20-
# E402 module level import not at top of file
21-
from majorMinorPatch import MajorMinorPatch # noqa:E402
22-
del sys.path[-1]
11+
from .majorMinorPatch import MajorMinorPatch
12+
13+
ApiVersionT = tuple[int, int, int] # major, minor, patch
2314

2415

2516
class AddonManifest(ConfigObj):
2617
"""From the NVDA addonHandler module. Should be kept in sync.
27-
Add-on manifest file. It contains metadata about an NVDA add-on package. """
28-
configspec = ConfigObj(StringIO(
29-
"""
18+
Add-on manifest file. It contains metadata about an NVDA add-on package."""
19+
20+
configspec = ConfigObj(
21+
StringIO(
22+
"""
3023
# NVDA Add-on Manifest configuration specification
3124
# Add-on unique name
3225
# Suggested convention is lowerCamelCase.
@@ -66,56 +59,59 @@ class AddonManifest(ConfigObj):
6659
# "0.0.0" is also valid.
6760
# The final integer can be left out, and in that case will default to 0. E.g. 2019.1
6861
62+
""",
63+
),
64+
)
65+
66+
def __init__(self, input: str | TextIOBase, translatedInput: str | None = None):
6967
"""
70-
))
68+
Constructs an :class:`AddonManifest` instance from manifest string data.
7169
72-
def __init__(self, input: TextIO, translatedInput: Optional[TextIO] = None):
73-
""" Constructs an L{AddonManifest} instance from manifest string data
74-
@param input: data to read the manifest information
75-
@param translatedInput: translated manifest input
70+
:param input: data to read the manifest information. Can be a filename or a file-like object.
71+
:param translatedInput: translated manifest input
7672
"""
77-
super().__init__(
73+
super().__init__( # type: ignore[reportUnknownMemberType]
7874
input,
7975
configspec=self.configspec,
80-
encoding='utf-8',
81-
default_encoding='utf-8',
76+
encoding="utf-8",
77+
default_encoding="utf-8",
8278
)
83-
self._errors: Optional[str] = None
84-
val = Validator({"apiVersion": validate_apiVersionString})
85-
result = self.validate(val, copy=True, preserve_errors=True)
79+
self._errors: str | None = None
80+
validator = Validator({"apiVersion": validate_apiVersionString})
81+
result = self.validate(validator, copy=True, preserve_errors=True) # type: ignore[reportUnknownMemberType]
8682
if result is not True:
8783
self._errors = result
8884
elif self._validateApiVersionRange() is not True:
8985
self._errors = "Constraint not met: minimumNVDAVersion ({}) <= lastTestedNVDAVersion ({})".format(
90-
self.get("minimumNVDAVersion"),
91-
self.get("lastTestedNVDAVersion")
86+
cast(ApiVersionT, self.get("minimumNVDAVersion")), # type: ignore[reportUnknownMemberType]
87+
cast(ApiVersionT, self.get("lastTestedNVDAVersion")), # type: ignore[reportUnknownMemberType]
9288
)
9389
self._translatedConfig = None
9490
if translatedInput is not None:
95-
self._translatedConfig = ConfigObj(translatedInput, encoding='utf-8', default_encoding='utf-8')
96-
for key in ('summary', 'description'):
97-
val = self._translatedConfig.get(key)
91+
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]
9894
if val:
9995
self[key] = val
10096

10197
@property
102-
def errors(self) -> str:
98+
def errors(self) -> str | None:
10399
return self._errors
104100

105101
def _validateApiVersionRange(self) -> bool:
106-
lastTested = self.get("lastTestedNVDAVersion")
107-
minRequiredVersion = self.get("minimumNVDAVersion")
102+
lastTested = cast(ApiVersionT, self.get("lastTestedNVDAVersion")) # type: ignore[reportUnknownMemberType]
103+
minRequiredVersion = cast(ApiVersionT, self.get("minimumNVDAVersion")) # type: ignore[reportUnknownMemberType]
108104
return minRequiredVersion <= lastTested
109105

110106

111-
def validate_apiVersionString(value: str) -> Tuple[int, int, int]:
107+
def validate_apiVersionString(value: str | Any) -> ApiVersionT:
112108
"""From the NVDA addonHandler module. Should be kept in sync."""
113109
if not value or value == "None":
114110
return (0, 0, 0)
115111
if not isinstance(value, str):
116112
raise ValidateError(
117113
"Expected an apiVersion in the form of a string. "
118-
f"e.g. '2019.1.0' instead of {value} (type {type(value)})"
114+
f"e.g. '2019.1.0' instead of {value} (type {type(value)})",
119115
)
120116
try:
121117
versionParsed = MajorMinorPatch.getFromStr(value)

0 commit comments

Comments
 (0)