nvda-addon-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvda_addon_kit/__init__.py +5 -0
- nvda_addon_kit/build.py +117 -0
- nvda_addon_kit/bundle.py +62 -0
- nvda_addon_kit/cli.py +377 -0
- nvda_addon_kit/config.py +412 -0
- nvda_addon_kit/crowdin.py +209 -0
- nvda_addon_kit/docs.py +99 -0
- nvda_addon_kit/errors.py +40 -0
- nvda_addon_kit/install.py +115 -0
- nvda_addon_kit/l10n/__init__.py +27 -0
- nvda_addon_kit/l10n/catalogs.py +141 -0
- nvda_addon_kit/l10n/extract.py +65 -0
- nvda_addon_kit/manifest.py +141 -0
- nvda_addon_kit/migrate.py +311 -0
- nvda_addon_kit/models.py +206 -0
- nvda_addon_kit/release.py +199 -0
- nvda_addon_kit/scaffold.py +116 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/01-bug_report.yaml +170 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/02-feature_request.yaml +44 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/03-developer_facing_changes.yaml +48 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/11-advanced_bug_report.md +28 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/12-advanced_feature_request.md +19 -0
- nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/config.yml +1 -0
- nvda_addon_kit/templates/ci/dot-github/dependabot.yml +14 -0
- nvda_addon_kit/templates/ci/dot-github/scripts/checkTranslation.py +146 -0
- nvda_addon_kit/templates/ci/dot-github/scripts/crowdinSync.ps1 +227 -0
- nvda_addon_kit/templates/ci/dot-github/scripts/languageMappings.json +15 -0
- nvda_addon_kit/templates/ci/dot-github/workflows/build_addon.yml +92 -0
- nvda_addon_kit/templates/ci/dot-github/workflows/crowdinL10n.yml +60 -0
- nvda_addon_kit/templates/core/COPYING.txt +340 -0
- nvda_addon_kit/templates/core/addon.toml +107 -0
- nvda_addon_kit/templates/core/changelog.md +9 -0
- nvda_addon_kit/templates/core/dot-gitattributes +8 -0
- nvda_addon_kit/templates/core/dot-gitignore +20 -0
- nvda_addon_kit/templates/core/readme.md +41 -0
- nvda_addon_kit/templates/core/style.css +26 -0
- nvda_addon_kit/templates/plugin/appModule.py +30 -0
- nvda_addon_kit/templates/plugin/brailleDisplayDriver.py +48 -0
- nvda_addon_kit/templates/plugin/globalPlugin.py +66 -0
- nvda_addon_kit/templates/plugin/synthDriver.py +60 -0
- nvda_addon_kit/templates/plugin/visionEnhancementProvider.py +45 -0
- nvda_addon_kit/templates/tooling/.ruff_cache/.gitignore +2 -0
- nvda_addon_kit/templates/tooling/.ruff_cache/CACHEDIR.TAG +1 -0
- nvda_addon_kit/templates/tooling/dot-python-version +1 -0
- nvda_addon_kit/templates/tooling/pyproject.toml +40 -0
- nvda_addon_kit/wizard.py +172 -0
- nvda_addon_kit-0.1.0.dist-info/METADATA +122 -0
- nvda_addon_kit-0.1.0.dist-info/RECORD +51 -0
- nvda_addon_kit-0.1.0.dist-info/WHEEL +4 -0
- nvda_addon_kit-0.1.0.dist-info/entry_points.txt +3 -0
- nvda_addon_kit-0.1.0.dist-info/licenses/COPYING.txt +340 -0
nvda_addon_kit/build.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Copyright (C) 2026 NVDA add-on kit contributors
|
|
2
|
+
# This file is covered by the GNU General Public License.
|
|
3
|
+
# See the file COPYING.txt for more details.
|
|
4
|
+
|
|
5
|
+
from copy import replace
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import docs, l10n
|
|
10
|
+
from .bundle import createBundle
|
|
11
|
+
from .manifest import writeManifest, writeTranslatedManifest
|
|
12
|
+
from .models import AddonConfig, AddonMetadata, Channel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PreparedAddon:
|
|
17
|
+
metadata: AddonMetadata
|
|
18
|
+
languages: list[str]
|
|
19
|
+
documents: list[Path]
|
|
20
|
+
|
|
21
|
+
def defaultBundlePath(self, root: Path) -> Path:
|
|
22
|
+
return root / f"{self.metadata.name}-{self.metadata.version}.nvda-addon"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class BuildResult:
|
|
27
|
+
bundle: Path
|
|
28
|
+
metadata: AddonMetadata
|
|
29
|
+
languages: list[str]
|
|
30
|
+
documents: list[Path]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolveMetadata(
|
|
34
|
+
config: AddonConfig,
|
|
35
|
+
*,
|
|
36
|
+
version: str | None = None,
|
|
37
|
+
channel: Channel | None = None,
|
|
38
|
+
) -> AddonMetadata:
|
|
39
|
+
metadata = config.addon
|
|
40
|
+
if channel is not None:
|
|
41
|
+
metadata = replace(metadata, updateChannel=channel)
|
|
42
|
+
if version:
|
|
43
|
+
metadata = replace(metadata, version=version)
|
|
44
|
+
return metadata
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prepare(config: AddonConfig, metadata: AddonMetadata) -> PreparedAddon:
|
|
48
|
+
writeManifest(config, metadata)
|
|
49
|
+
|
|
50
|
+
languages = l10n.discoverLocales(config)
|
|
51
|
+
translationsByLanguage: dict[str, dict[str, str]] = {}
|
|
52
|
+
for language in languages:
|
|
53
|
+
translations = l10n.loadTranslations(config, language)
|
|
54
|
+
translationsByLanguage[language] = translations
|
|
55
|
+
writeTranslatedManifest(config, language, translations, metadata)
|
|
56
|
+
l10n.compileLocales(config, languages)
|
|
57
|
+
|
|
58
|
+
docs.stageBaseDocuments(config)
|
|
59
|
+
documents = docs.renderAll(config, translationsByLanguage, metadata)
|
|
60
|
+
|
|
61
|
+
return PreparedAddon(metadata=metadata, languages=languages, documents=documents)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build(
|
|
65
|
+
config: AddonConfig,
|
|
66
|
+
*,
|
|
67
|
+
version: str | None = None,
|
|
68
|
+
channel: Channel | None = None,
|
|
69
|
+
output: Path | None = None,
|
|
70
|
+
) -> BuildResult:
|
|
71
|
+
metadata = resolveMetadata(config, version=version, channel=channel)
|
|
72
|
+
prepared = prepare(config, metadata)
|
|
73
|
+
|
|
74
|
+
destination = output or prepared.defaultBundlePath(config.root)
|
|
75
|
+
bundle = createBundle(config.addonDir, destination, config.build.excludedFiles)
|
|
76
|
+
|
|
77
|
+
return BuildResult(
|
|
78
|
+
bundle=bundle,
|
|
79
|
+
metadata=prepared.metadata,
|
|
80
|
+
languages=prepared.languages,
|
|
81
|
+
documents=prepared.documents,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _removeFile(path: Path, removed: list[Path]) -> None:
|
|
86
|
+
if path.is_file():
|
|
87
|
+
path.unlink()
|
|
88
|
+
removed.append(path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clean(config: AddonConfig) -> list[Path]:
|
|
92
|
+
removed: list[Path] = []
|
|
93
|
+
|
|
94
|
+
_removeFile(config.addonDir / "manifest.ini", removed)
|
|
95
|
+
_removeFile(config.docsDir / "style.css", removed)
|
|
96
|
+
|
|
97
|
+
for language in l10n.discoverLocales(config):
|
|
98
|
+
_removeFile(l10n.moPath(config, language), removed)
|
|
99
|
+
_removeFile(config.localeDir / language / "manifest.ini", removed)
|
|
100
|
+
|
|
101
|
+
if config.docsDir.is_dir():
|
|
102
|
+
for html in sorted(config.docsDir.glob("*/*.html")):
|
|
103
|
+
_removeFile(html, removed)
|
|
104
|
+
baseDocs = config.docsDir / config.l10n.baseLanguage
|
|
105
|
+
if baseDocs.is_dir():
|
|
106
|
+
for staged in sorted(baseDocs.glob("*.md")):
|
|
107
|
+
_removeFile(staged, removed)
|
|
108
|
+
if not any(baseDocs.iterdir()):
|
|
109
|
+
baseDocs.rmdir()
|
|
110
|
+
removed.append(baseDocs)
|
|
111
|
+
|
|
112
|
+
for bundle in sorted(config.root.glob("*.nvda-addon")):
|
|
113
|
+
_removeFile(bundle, removed)
|
|
114
|
+
for potFile in sorted(config.root.glob("*.pot")):
|
|
115
|
+
_removeFile(potFile, removed)
|
|
116
|
+
|
|
117
|
+
return removed
|
nvda_addon_kit/bundle.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (C) 2026 NVDA add-on kit contributors
|
|
2
|
+
# This file is covered by the GNU General Public License.
|
|
3
|
+
# See the file COPYING.txt for more details.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import zipfile
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .errors import BuildError
|
|
11
|
+
|
|
12
|
+
_ALWAYS_EXCLUDED: tuple[str, ...] = (
|
|
13
|
+
"**/__pycache__/**",
|
|
14
|
+
"**/*.py[co]",
|
|
15
|
+
"**/.git/**",
|
|
16
|
+
"**/.DS_Store",
|
|
17
|
+
"**/*.orig",
|
|
18
|
+
"**/*.rej",
|
|
19
|
+
"**/*.po",
|
|
20
|
+
"**/*.pot",
|
|
21
|
+
"**/*.md",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _isExcluded(relativePath: Path, patterns: Iterable[str]) -> bool:
|
|
26
|
+
return any(relativePath.full_match(pattern) or relativePath.match(pattern) for pattern in patterns)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def createBundle(
|
|
30
|
+
addonDir: Path,
|
|
31
|
+
destination: Path,
|
|
32
|
+
excludePatterns: Iterable[str] = (),
|
|
33
|
+
) -> Path:
|
|
34
|
+
if not addonDir.is_dir():
|
|
35
|
+
raise BuildError(f"Add-on directory {addonDir} does not exist.")
|
|
36
|
+
|
|
37
|
+
manifest = addonDir / "manifest.ini"
|
|
38
|
+
if not manifest.is_file():
|
|
39
|
+
raise BuildError(
|
|
40
|
+
f"{manifest} is missing, so the archive would not be a valid add-on. "
|
|
41
|
+
f"This is generated during the build; run `nvaddon build`.",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
patterns = (*_ALWAYS_EXCLUDED, *excludePatterns)
|
|
45
|
+
base = addonDir.absolute()
|
|
46
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
with zipfile.ZipFile(destination, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
49
|
+
for path in sorted(base.rglob("*")):
|
|
50
|
+
if path.is_dir():
|
|
51
|
+
continue
|
|
52
|
+
relativePath = path.relative_to(base)
|
|
53
|
+
if _isExcluded(relativePath, patterns):
|
|
54
|
+
continue
|
|
55
|
+
archive.write(path, relativePath.as_posix())
|
|
56
|
+
|
|
57
|
+
return destination
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def listBundle(archivePath: Path) -> list[str]:
|
|
61
|
+
with zipfile.ZipFile(archivePath) as archive:
|
|
62
|
+
return sorted(archive.namelist())
|
nvda_addon_kit/cli.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Copyright (C) 2026 NVDA add-on kit contributors
|
|
2
|
+
# This file is covered by the GNU General Public License.
|
|
3
|
+
# See the file COPYING.txt for more details.
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__, l10n
|
|
11
|
+
from . import build as buildModule
|
|
12
|
+
from . import config as configModule
|
|
13
|
+
from .errors import AddonKitError
|
|
14
|
+
from .models import AddonConfig, Channel
|
|
15
|
+
|
|
16
|
+
PLUGIN_KINDS: tuple[str, ...] = (
|
|
17
|
+
"globalPlugin",
|
|
18
|
+
"appModule",
|
|
19
|
+
"synthDriver",
|
|
20
|
+
"brailleDisplayDriver",
|
|
21
|
+
"visionEnhancementProvider",
|
|
22
|
+
"none",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _loadConfig(args: argparse.Namespace) -> AddonConfig:
|
|
27
|
+
start = Path(args.directory) if getattr(args, "directory", None) else None
|
|
28
|
+
return configModule.load(start)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _relative(path: Path, root: Path) -> str:
|
|
32
|
+
try:
|
|
33
|
+
return str(path.relative_to(root))
|
|
34
|
+
except ValueError:
|
|
35
|
+
return str(path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmdInit(args: argparse.Namespace) -> int:
|
|
39
|
+
from .scaffold import initialiseAddon
|
|
40
|
+
|
|
41
|
+
destination = Path(args.directory_positional or args.directory or ".").resolve()
|
|
42
|
+
created = initialiseAddon(destination, args)
|
|
43
|
+
print(f"Created {len(created)} files in {destination}")
|
|
44
|
+
print("\nNext steps:")
|
|
45
|
+
print(f" cd {destination.name}")
|
|
46
|
+
print(" nvaddon build")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmdBuild(args: argparse.Namespace) -> int:
|
|
51
|
+
config = _loadConfig(args)
|
|
52
|
+
result = buildModule.build(
|
|
53
|
+
config,
|
|
54
|
+
version=args.version,
|
|
55
|
+
channel=args.channel,
|
|
56
|
+
output=Path(args.output) if args.output else None,
|
|
57
|
+
)
|
|
58
|
+
if result.languages:
|
|
59
|
+
print(f"Compiled {len(result.languages)} translations: {', '.join(result.languages)}")
|
|
60
|
+
if result.documents:
|
|
61
|
+
print(f"Rendered {len(result.documents)} documentation pages")
|
|
62
|
+
print(f"Built {_relative(result.bundle, config.root)} ({result.bundle.stat().st_size:,} bytes)")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmdInstall(args: argparse.Namespace) -> int:
|
|
67
|
+
from .install import handOffToNVDA, linkAddon
|
|
68
|
+
|
|
69
|
+
config = _loadConfig(args)
|
|
70
|
+
nvdaPath = Path(args.nvda_path) if args.nvda_path else None
|
|
71
|
+
|
|
72
|
+
if args.link:
|
|
73
|
+
metadata = buildModule.resolveMetadata(config, version=args.version, channel=args.channel)
|
|
74
|
+
_ = buildModule.prepare(config, metadata)
|
|
75
|
+
link = linkAddon(config, nvdaPath)
|
|
76
|
+
print(f"Linked {_relative(config.addonDir, config.root)} to {link}")
|
|
77
|
+
print("Restart NVDA, or reload plugins, to pick up the add-on.")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
result = buildModule.build(config, version=args.version, channel=args.channel)
|
|
81
|
+
handOffToNVDA(result.bundle)
|
|
82
|
+
print(f"Opened {result.bundle.name} with NVDA. Confirm the installation prompt to finish.")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmdPot(args: argparse.Namespace) -> int:
|
|
87
|
+
config = _loadConfig(args)
|
|
88
|
+
destination = Path(args.output) if args.output else None
|
|
89
|
+
potFile = l10n.writePot(config, merge=args.merge, destination=destination)
|
|
90
|
+
catalog = l10n.buildCatalog(config)
|
|
91
|
+
count = sum(1 for message in catalog if message.id)
|
|
92
|
+
print(f"Wrote {_relative(potFile, config.root)} with {count} messages")
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmdLocaleAdd(args: argparse.Namespace) -> int:
|
|
97
|
+
config = _loadConfig(args)
|
|
98
|
+
created = l10n.addLocales(config, args.languages)
|
|
99
|
+
for path in created:
|
|
100
|
+
print(f"Created {_relative(path, config.root)}")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmdLocaleUpdate(args: argparse.Namespace) -> int:
|
|
105
|
+
config = _loadConfig(args)
|
|
106
|
+
summary = l10n.updateLocales(config, args.languages or None)
|
|
107
|
+
if not summary:
|
|
108
|
+
print("No translations to update.")
|
|
109
|
+
return 0
|
|
110
|
+
for language, (untranslated, fuzzy) in summary.items():
|
|
111
|
+
details = [f"{untranslated} untranslated"]
|
|
112
|
+
if fuzzy:
|
|
113
|
+
details.append(f"{fuzzy} fuzzy")
|
|
114
|
+
print(f"{language}: {', '.join(details)}")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmdLocaleCompile(args: argparse.Namespace) -> int:
|
|
119
|
+
config = _loadConfig(args)
|
|
120
|
+
compiled = l10n.compileLocales(config, args.languages or None)
|
|
121
|
+
if not compiled:
|
|
122
|
+
print("No translations to compile.")
|
|
123
|
+
return 0
|
|
124
|
+
for path in compiled:
|
|
125
|
+
print(f"Compiled {_relative(path, config.root)}")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmdCheck(args: argparse.Namespace) -> int:
|
|
130
|
+
config = _loadConfig(args)
|
|
131
|
+
issues = configModule.validate(config)
|
|
132
|
+
errors = [issue for issue in issues if issue.level == "error"]
|
|
133
|
+
warnings = [issue for issue in issues if issue.level == "warning"]
|
|
134
|
+
|
|
135
|
+
for issue in errors + warnings:
|
|
136
|
+
print(issue, file=sys.stderr if issue.level == "error" else sys.stdout)
|
|
137
|
+
|
|
138
|
+
if not issues:
|
|
139
|
+
print(f"{config.addon.name} {config.addon.version}: no problems found.")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
print(f"\n{len(errors)} error(s), {len(warnings)} warning(s).")
|
|
143
|
+
return 1 if errors else 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmdClean(args: argparse.Namespace) -> int:
|
|
147
|
+
config = _loadConfig(args)
|
|
148
|
+
removed = buildModule.clean(config)
|
|
149
|
+
if not removed:
|
|
150
|
+
print("Nothing to clean.")
|
|
151
|
+
return 0
|
|
152
|
+
for path in removed:
|
|
153
|
+
print(f"Removed {_relative(path, config.root)}")
|
|
154
|
+
print(f"\nRemoved {len(removed)} build outputs.")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmdMigrate(args: argparse.Namespace) -> int:
|
|
159
|
+
from .migrate import migrateBuildVars
|
|
160
|
+
|
|
161
|
+
start = Path(args.directory) if args.directory else Path.cwd()
|
|
162
|
+
source = Path(args.buildvars) if args.buildvars else start / "buildVars.py"
|
|
163
|
+
destination = Path(args.output) if args.output else start / configModule.CONFIG_FILENAME
|
|
164
|
+
|
|
165
|
+
result = migrateBuildVars(source, destination, force=args.force)
|
|
166
|
+
print(f"Wrote {destination} from {source}")
|
|
167
|
+
for note in result.notes:
|
|
168
|
+
print(f" note: {note}")
|
|
169
|
+
print("\nReview the result, then run `nvaddon check` and `nvaddon build`.")
|
|
170
|
+
print("Once the build output looks right, delete buildVars.py, sconstruct and site_scons/.")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmdRelease(args: argparse.Namespace) -> int:
|
|
175
|
+
from .release import release
|
|
176
|
+
|
|
177
|
+
config = _loadConfig(args)
|
|
178
|
+
configPath = configModule.findConfig(config.root)
|
|
179
|
+
|
|
180
|
+
plan = release(
|
|
181
|
+
configPath,
|
|
182
|
+
args.version,
|
|
183
|
+
args.channel,
|
|
184
|
+
push=not args.no_push,
|
|
185
|
+
changelog=args.changelog,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
print(f"{config.addon.version} -> {plan.version} on the {plan.channel} channel")
|
|
189
|
+
print(f"Committed and tagged {plan.tag}")
|
|
190
|
+
if plan.pushed:
|
|
191
|
+
print("Pushed the commit and the tag together.")
|
|
192
|
+
else:
|
|
193
|
+
print("Not pushed. When you are ready:")
|
|
194
|
+
print(f" git push --atomic origin HEAD refs/tags/{plan.tag}")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmdCrowdin(args: argparse.Namespace) -> int:
|
|
199
|
+
from .crowdin import runCrowdin
|
|
200
|
+
|
|
201
|
+
config = _loadConfig(args)
|
|
202
|
+
return runCrowdin(config, args)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _addInitArguments(parser: argparse.ArgumentParser) -> None:
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"directory_positional",
|
|
208
|
+
nargs="?",
|
|
209
|
+
metavar="directory",
|
|
210
|
+
help="where to create the add-on (default: current directory)",
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument("--name", help="add-on name, internal to NVDA (e.g. myAddon)")
|
|
213
|
+
parser.add_argument("--summary", help="user visible name of the add-on")
|
|
214
|
+
parser.add_argument("--description", help="longer description shown in the add-on store")
|
|
215
|
+
parser.add_argument("--author", help='author, as "Name <email@example.com>"')
|
|
216
|
+
parser.add_argument("--addon-version", default=None, help="initial version (default: 1.0.0)")
|
|
217
|
+
parser.add_argument("--url", help="URL of the add-on's documentation or homepage")
|
|
218
|
+
parser.add_argument("--source-url", help="URL of the source repository")
|
|
219
|
+
parser.add_argument("--license", help='licence name (e.g. "GPL v2")')
|
|
220
|
+
parser.add_argument("--license-url", help="URL of the licence text")
|
|
221
|
+
parser.add_argument("--min-nvda", help="minimum supported NVDA version (e.g. 2024.1)")
|
|
222
|
+
parser.add_argument("--last-tested", help="most recent NVDA version tested (e.g. 2025.3)")
|
|
223
|
+
parser.add_argument("--base-language", help="language the add-on is written in (default: en)")
|
|
224
|
+
parser.add_argument("--plugin", choices=PLUGIN_KINDS, help="starter plugin to scaffold")
|
|
225
|
+
parser.add_argument(
|
|
226
|
+
"--no-ci",
|
|
227
|
+
action="store_true",
|
|
228
|
+
help="skip GitHub workflows, issue templates and the Crowdin sync",
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--no-tooling",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help="skip the lint configuration and the Python version pin",
|
|
234
|
+
)
|
|
235
|
+
parser.add_argument(
|
|
236
|
+
"-y", "--yes", action="store_true", help="do not prompt; take every answer from flags and defaults"
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument("--force", action="store_true", help="write into a non-empty directory")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _addVersionArguments(parser: argparse.ArgumentParser) -> None:
|
|
242
|
+
parser.add_argument("--version", metavar="VERSION", help="override the version for this build")
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"--channel",
|
|
245
|
+
type=Channel,
|
|
246
|
+
choices=list(Channel),
|
|
247
|
+
help="override the update channel for this build",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _addLanguageArgument(parser: argparse.ArgumentParser, *, required: bool, action: str) -> None:
|
|
252
|
+
parser.add_argument(
|
|
253
|
+
"languages",
|
|
254
|
+
nargs="+" if required else "*",
|
|
255
|
+
metavar="LANG",
|
|
256
|
+
help="language codes, e.g. de pt_BR" if required else f"languages to {action} (default: all)",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def buildParser() -> argparse.ArgumentParser:
|
|
261
|
+
parser = argparse.ArgumentParser(
|
|
262
|
+
prog="nvaddon",
|
|
263
|
+
description="Scaffold, build, translate and install NVDA add-ons.",
|
|
264
|
+
)
|
|
265
|
+
parser.add_argument("--version", action="version", version=f"nvda-addon-kit {__version__}")
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"-C",
|
|
268
|
+
"--directory",
|
|
269
|
+
metavar="PATH",
|
|
270
|
+
help="run as though started in PATH",
|
|
271
|
+
)
|
|
272
|
+
subparsers = parser.add_subparsers(dest="command", metavar="command")
|
|
273
|
+
|
|
274
|
+
init = subparsers.add_parser("init", help="create a new add-on")
|
|
275
|
+
_addInitArguments(init)
|
|
276
|
+
init.set_defaults(func=cmdInit)
|
|
277
|
+
|
|
278
|
+
build = subparsers.add_parser("build", help="build the .nvda-addon")
|
|
279
|
+
_addVersionArguments(build)
|
|
280
|
+
build.add_argument(
|
|
281
|
+
"-o", "--output", metavar="PATH", help="write the add-on here instead of the default name"
|
|
282
|
+
)
|
|
283
|
+
build.set_defaults(func=cmdBuild)
|
|
284
|
+
|
|
285
|
+
install = subparsers.add_parser("install", help="build and install into NVDA")
|
|
286
|
+
install.add_argument(
|
|
287
|
+
"--link",
|
|
288
|
+
action="store_true",
|
|
289
|
+
help="link the working tree into NVDA's add-ons directory instead of installing a built add-on",
|
|
290
|
+
)
|
|
291
|
+
install.add_argument("--nvda-path", metavar="PATH", help="target a portable NVDA copy at PATH")
|
|
292
|
+
_addVersionArguments(install)
|
|
293
|
+
install.set_defaults(func=cmdInstall)
|
|
294
|
+
|
|
295
|
+
pot = subparsers.add_parser("pot", help="generate the translation template")
|
|
296
|
+
pot.add_argument(
|
|
297
|
+
"--merge",
|
|
298
|
+
action="store_true",
|
|
299
|
+
help="omit the header and source locations, for merging into catalogues",
|
|
300
|
+
)
|
|
301
|
+
pot.add_argument("-o", "--output", metavar="PATH", help="write the template here")
|
|
302
|
+
pot.set_defaults(func=cmdPot)
|
|
303
|
+
|
|
304
|
+
localeAdd = subparsers.add_parser("locale-add", help="start a new translation")
|
|
305
|
+
_addLanguageArgument(localeAdd, required=True, action="add")
|
|
306
|
+
localeAdd.set_defaults(func=cmdLocaleAdd)
|
|
307
|
+
|
|
308
|
+
localeUpdate = subparsers.add_parser(
|
|
309
|
+
"locale-update", help="merge new messages into existing translations"
|
|
310
|
+
)
|
|
311
|
+
_addLanguageArgument(localeUpdate, required=False, action="update")
|
|
312
|
+
localeUpdate.set_defaults(func=cmdLocaleUpdate)
|
|
313
|
+
|
|
314
|
+
localeCompile = subparsers.add_parser("locale-compile", help="compile translations to .mo files")
|
|
315
|
+
_addLanguageArgument(localeCompile, required=False, action="compile")
|
|
316
|
+
localeCompile.set_defaults(func=cmdLocaleCompile)
|
|
317
|
+
|
|
318
|
+
check = subparsers.add_parser("check", help="validate addon.toml without building")
|
|
319
|
+
check.set_defaults(func=cmdCheck)
|
|
320
|
+
|
|
321
|
+
clean = subparsers.add_parser("clean", help="remove generated build outputs")
|
|
322
|
+
clean.set_defaults(func=cmdClean)
|
|
323
|
+
|
|
324
|
+
migrate = subparsers.add_parser("migrate", help="convert buildVars.py to addon.toml")
|
|
325
|
+
migrate.add_argument("--buildvars", metavar="PATH", help="path to buildVars.py")
|
|
326
|
+
migrate.add_argument("-o", "--output", metavar="PATH", help="path to write addon.toml to")
|
|
327
|
+
migrate.add_argument("--force", action="store_true", help="overwrite an existing addon.toml")
|
|
328
|
+
migrate.set_defaults(func=cmdMigrate)
|
|
329
|
+
|
|
330
|
+
release = subparsers.add_parser("release", help="set the version and channel, tag, and push the release")
|
|
331
|
+
release.add_argument("version", help="major, minor, patch, or an explicit version such as 1.2.3")
|
|
332
|
+
release.add_argument(
|
|
333
|
+
"--channel",
|
|
334
|
+
type=Channel,
|
|
335
|
+
choices=list(Channel),
|
|
336
|
+
default=Channel.STABLE,
|
|
337
|
+
help="channel to release on (default: stable); the tag is <version>-<channel>",
|
|
338
|
+
)
|
|
339
|
+
release.add_argument("--no-push", action="store_true", help="commit and tag locally without pushing")
|
|
340
|
+
release.add_argument("--changelog", action="store_true", help="add a stub entry to changelog.md")
|
|
341
|
+
release.set_defaults(func=cmdRelease)
|
|
342
|
+
|
|
343
|
+
crowdin = subparsers.add_parser("crowdin", help="sync translations with Crowdin")
|
|
344
|
+
crowdin.add_argument("action", choices=("upload", "download", "status"), help="what to sync")
|
|
345
|
+
crowdin.add_argument("--project-id", type=int, help="Crowdin project id (default: $CROWDIN_PROJECT_ID)")
|
|
346
|
+
crowdin.add_argument("--token", help="Crowdin API token (default: $CROWDIN_TOKEN)")
|
|
347
|
+
crowdin.add_argument(
|
|
348
|
+
"--min-percent",
|
|
349
|
+
type=int,
|
|
350
|
+
default=50,
|
|
351
|
+
help="minimum translated percentage to download a language (default: 50)",
|
|
352
|
+
)
|
|
353
|
+
crowdin.set_defaults(func=cmdCrowdin)
|
|
354
|
+
|
|
355
|
+
return parser
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
359
|
+
parser = buildParser()
|
|
360
|
+
args = parser.parse_args(argv)
|
|
361
|
+
|
|
362
|
+
if not getattr(args, "func", None):
|
|
363
|
+
parser.print_help()
|
|
364
|
+
return 2
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
return args.func(args)
|
|
368
|
+
except AddonKitError as error:
|
|
369
|
+
print(f"error: {error}", file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
except KeyboardInterrupt:
|
|
372
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
373
|
+
return 130
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
raise SystemExit(main())
|