From 95ce2fde6139053d89e4ff2114390b9d41c330e7 Mon Sep 17 00:00:00 2001 From: hwf1324 <1398969445@qq.com> Date: Wed, 17 Dec 2025 17:12:20 +0800 Subject: [PATCH 1/2] Update add-ons templates --- .github/workflows/build_addon.yml | 16 +- .pre-commit-config.yaml | 92 ++++++- _template_addon_release.json | 29 -- addon/globalPlugins/viewerFrame.py | 3 +- buildVars.py | 56 ++-- manifest-translated.ini.tpl | 1 + manifest.ini.tpl | 1 + pyproject.toml | 121 +++++++++ sconstruct | 284 +++++--------------- site_scons/site_tools/NVDATool/__init__.py | 105 ++++++++ site_scons/site_tools/NVDATool/addon.py | 24 ++ site_scons/site_tools/NVDATool/docs.py | 61 +++++ site_scons/site_tools/NVDATool/manifests.py | 69 +++++ site_scons/site_tools/NVDATool/typings.py | 39 +++ site_scons/site_tools/NVDATool/utils.py | 28 ++ 15 files changed, 642 insertions(+), 287 deletions(-) delete mode 100644 _template_addon_release.json create mode 100644 site_scons/site_tools/NVDATool/__init__.py create mode 100644 site_scons/site_tools/NVDATool/addon.py create mode 100644 site_scons/site_tools/NVDATool/docs.py create mode 100644 site_scons/site_tools/NVDATool/manifests.py create mode 100644 site_scons/site_tools/NVDATool/typings.py create mode 100644 site_scons/site_tools/NVDATool/utils.py diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 11da712..65a5bed 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -17,12 +17,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 cache: 'pip' @@ -40,7 +40,7 @@ jobs: - name: building addon run: scons && scons pot - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: packaged_addon path: | @@ -54,22 +54,22 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: download releases files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 - name: Display structure of downloaded files run: ls -R - name: Calculate sha256 run: | echo -e "\nSHA256: " >> changelog.md - sha256sum packaged_addon/*.nvda-addon >> changelog.md + sha256sum *.nvda-addon >> changelog.md - name: Release uses: softprops/action-gh-release@v2 with: files: | - packaged_addon/*.nvda-addon - packaged_addon/*.pot + *.nvda-addon + *.pot body_path: changelog.md fail_on_unmatched_files: true prerelease: ${{ contains(github.ref, '-') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd7a9d6..0c8f5c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,87 @@ +# Copied from https://github.com/nvaccess/nvda +# https://pre-commit.ci/ +# Configuration for Continuous Integration service +ci: + # Pyright does not seem to work in pre-commit CI + skip: [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/pre-commit-hooks - rev: v4.3.0 - hooks: - - id: check-ast - - id: check-case-conflict - - id: check-yaml +- 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: v5.0.0 + hooks: + # Prevents commits to certain branches + - id: no-commit-to-branch + args: ["--branch", "main", "master", ] + # 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, c, c++, batch, markdown, toml, yaml, powershell] + # Ensures all files end in 1 (and only 1) newline. + - id: end-of-file-fixer + types_or: [python, c, c++, 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, c, c++, 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 +- 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.12.7 + hooks: + - id: ruff + name: lint with ruff + args: [ --fix ] + - id: ruff-format + name: format with ruff + +- repo: local + hooks: + + - id: pyright + name: type check with pyright + entry: uv run pyright + language: system + types: [python] diff --git a/_template_addon_release.json b/_template_addon_release.json deleted file mode 100644 index c6d3a5f..0000000 --- a/_template_addon_release.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "addonId": "easyAddonTech.XYZ", - "addonVersionNumber": { - "major": 21, - "minor": 6, - "patch": 0 - }, - "addonVersionName": "21.06", - "displayName": "My addon", - "publisher": "easyAddonTech", - "description": "Makes doing XYZ easier", - "homepage": "https://github.com/nvaccess/addon-datastore", - "minNVDAVersion": { - "major": 2019, - "minor": 3, - "patch": 0 - }, - "lastTestedVersion": { - "major": 2020, - "minor": 4, - "patch": 0 - }, - "channel": "beta", - "URL": "https://github.com/nvaccess/addon-datastore/releases/download/v0.1.0/myAddon.nvda-addon", - "sha256": "69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82", - "sourceURL": "https://github.com/nvaccess/addon-datastore/", - "license": "GPL v2", - "licenseURL": "https://github.com/nvaccess/addon-datastore/license.MD" -} diff --git a/addon/globalPlugins/viewerFrame.py b/addon/globalPlugins/viewerFrame.py index 80c15fb..a40c3b7 100644 --- a/addon/globalPlugins/viewerFrame.py +++ b/addon/globalPlugins/viewerFrame.py @@ -23,7 +23,7 @@ def __init__(self, parent, namespace): wx.ID_ANY, # Translators: The title of the Object Viewer frame. _("Object Viewer"), - style=wx.DEFAULT_FRAME_STYLE | wx.STAY_ON_TOP, + # style=wx.DEFAULT_FRAME_STYLE | wx.STAY_ON_TOP, ) self.panel: wx.Panel = wx.Panel(self) @@ -70,6 +70,7 @@ def __init__(self, parent, namespace): self.SetMinSize(self.scaleSize(self.MIN_SIZE)) self.SetSize(self.scaleSize(self.INITIAL_SIZE)) # the size has changed, so recenter on the screen + self.SetTransparent(int(255 * 0.9)) self.CentreOnScreen() INITIAL_SIZE = (800, 480) diff --git a/buildVars.py b/buildVars.py index 3888443..c7c48bb 100644 --- a/buildVars.py +++ b/buildVars.py @@ -3,50 +3,54 @@ # Build customizations # Change this file instead of sconstruct or manifest files, whenever possible. +from site_scons.site_tools.NVDATool.typings import AddonInfo, BrailleTables, SymbolDictionaries # Since some strings in `addon_info` are translatable, # we need to include them in the .po files. # Gettext recognizes only strings given as parameters to the `_` function. -# To avoid initializing translations in this module we simply roll our own "fake" `_` function +# To avoid initializing translations in this module we simply import a "fake" `_` function # which returns whatever is given to it as an argument. -def _(arg): - return arg +from site_scons.site_tools.NVDATool.utils import _ # Add-on information variables -addon_info = { +addon_info = AddonInfo( # add-on Name/identifier, internal for NVDA - "addon_name": "objectViewer", + addon_name="objectViewer", # Add-on summary/title, usually the user visible name of the add-on # Translators: Summary/title for this add-on # to be shown on installation and add-on information found in add-on store - "addon_summary": _("Object Viewer"), + addon_summary=_("Object Viewer"), # Add-on description # Translators: Long description to be shown for this add-on on add-on information from add-on store - "addon_description": _("""This NVDA add-on use the GUI to view the NVDA Object."""), + addon_description=_("""This NVDA add-on use the GUI to view the NVDA Object."""), # version - "addon_version": "0.0.1", + addon_version="0.0.1", + # Brief changelog for this version + # Translators: what's new content for the add-on version to be shown in the add-on store + addon_changelog=_("""Changelog for the add-on version. +It can span multiple lines."""), # Author(s) - "addon_author": "hwf1324 <1398969445@qq.com>", + addon_author="hwf1324 <1398969445@qq.com>", # URL for the add-on documentation support - "addon_url": "https://github.com/hwf1324/objectViewer", + addon_url="https://github.com/hwf1324/objectViewer", # URL for the add-on repository where the source code can be found - "addon_sourceURL": "https://github.com/hwf1324/objectViewer", + addon_sourceURL="https://github.com/hwf1324/objectViewer", # Documentation file name - "addon_docFileName": "readme.html", + addon_docFileName="readme.html", # Minimum NVDA version supported (e.g. "2019.3.0", minor version is optional) - "addon_minimumNVDAVersion": "2023.1", + addon_minimumNVDAVersion="2023.1", # Last NVDA version supported/tested (e.g. "2024.4.0", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion": "2025.1", + addon_lastTestedNVDAVersion="2025.1", # Add-on update channel (default is None, denoting stable releases, # and for development releases, use "dev".) # Do not change unless you know what you are doing! - "addon_updateChannel": None, + addon_updateChannel=None, # Add-on license such as GPL 2 - "addon_license": "GPL v2", + addon_license="GPL v2", # URL for the license document the ad-on is licensed under - "addon_licenseURL": "https://www.gnu.org/licenses/gpl-2.0.html", -} + addon_licenseURL="https://www.gnu.org/licenses/gpl-2.0.html", +) # Define the python files that are the sources of your add-on. # You can either list every file (using ""/") as a path separator, @@ -56,27 +60,29 @@ def _(arg): # pythonSources = ["addon/globalPlugins/*.py"] # For more information on SCons Glob expressions please take a look at: # https://scons.org/doc/production/HTML/scons-user/apd.html -pythonSources = ["addon/globalPlugins/*.py"] +pythonSources: list[str] = ["addon/globalPlugins/*.py"] # Files that contain strings for translation. Usually your python sources -i18nSources = pythonSources + ["buildVars.py"] +i18nSources: list[str] = pythonSources + ["buildVars.py"] # Files that will be ignored when building the nvda-addon file # Paths are relative to the addon directory, not to the root directory of your addon sources. -excludedFiles = [] +# You can either list every file (using ""/") as a path separator, +# or use glob expressions. +excludedFiles: list[str] = [] # Base language for the NVDA add-on # If your add-on is written in a language other than english, modify this variable. # For example, set baseLanguage to "es" if your add-on is primarily written in spanish. # You must also edit .gitignore file to specify base language files to be ignored. -baseLanguage = "en" +baseLanguage: str = "en" # Markdown extensions for add-on documentation # Most add-ons do not require additional Markdown extensions. # If you need to add support for markup such as tables, fill out the below list. # Extensions string must be of the form "markdown.extensions.extensionName" # e.g. "markdown.extensions.tables" to add tables. -markdownExtensions = [] +markdownExtensions: list[str] = [] # Custom braille translation tables # If your add-on includes custom braille tables (most will not), fill out this dictionary. @@ -86,7 +92,7 @@ def _(arg): # contracted (contracted (True) or uncontracted (False) braille code), # output (shown in output table list), # input (shown in input table list). -brailleTables = {} +brailleTables: BrailleTables = {} # Custom speech symbol dictionaries # Symbol dictionary files reside in the locale folder, e.g. `locale\en`, and are named `symbols-.dic`. @@ -95,4 +101,4 @@ def _(arg): # with keys inside recording the following attributes: # displayName (name of the speech dictionary shown to users and translatable), # mandatory (True when always enabled, False when not. -symbolDictionaries = {} +symbolDictionaries: SymbolDictionaries = {} diff --git a/manifest-translated.ini.tpl b/manifest-translated.ini.tpl index c06aa84..6df6d42 100644 --- a/manifest-translated.ini.tpl +++ b/manifest-translated.ini.tpl @@ -1,2 +1,3 @@ summary = "{addon_summary}" description = """{addon_description}""" +changelog = """{addon_changelog}""" diff --git a/manifest.ini.tpl b/manifest.ini.tpl index d44355d..2b7b0eb 100644 --- a/manifest.ini.tpl +++ b/manifest.ini.tpl @@ -4,6 +4,7 @@ description = """{addon_description}""" author = "{addon_author}" url = {addon_url} version = {addon_version} +changelog = """{addon_changelog}""" docFileName = {addon_docFileName} minimumNVDAVersion = {addon_minimumNVDAVersion} lastTestedNVDAVersion = {addon_lastTestedNVDAVersion} diff --git a/pyproject.toml b/pyproject.toml index 4d76bfe..97189ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,124 @@ ignore = [ # sconstruct contains many inbuilt functions not recognised by the lint, # so ignore F821. "sconstruct" = ["F821"] + +[tool.pyright] +pythonPlatform = "Windows" +typeCheckingMode = "strict" + +include = [ + "**/*.py", +] + +exclude = [ + "sconstruct", + ".git", + "__pycache__", + # When excluding concrete paths relative to a directory, + # not matching multiple folders by name e.g. `__pycache__`, + # paths are relative to the configuration file. +] + +# Tell pyright where to load python code from +extraPaths = [ + "./addon", +] + +# General config +analyzeUnannotatedFunctions = true +deprecateTypingAliases = true + +# Stricter typing +strictParameterNoneValue = true +strictListInference = true +strictDictionaryInference = true +strictSetInference = true + +# Compliant rules +reportAbstractUsage = true +reportArgumentType = true +reportAssertAlwaysTrue = true +reportAssertTypeFailure = true +reportAssignmentType = true +reportAttributeAccessIssue = true +reportCallInDefaultInitializer = true +reportCallIssue = true +reportConstantRedefinition = true +reportDuplicateImport = true +reportFunctionMemberAccess = true +reportGeneralTypeIssues = true +reportImplicitOverride = true +reportImplicitStringConcatenation = true +reportImportCycles = true +reportIncompatibleMethodOverride = true +reportIncompatibleVariableOverride = true +reportIncompleteStub = true +reportInconsistentConstructor = true +reportInconsistentOverload = true +reportIndexIssue = true +reportInvalidStringEscapeSequence = true +reportInvalidStubStatement = true +reportInvalidTypeArguments = true +reportInvalidTypeForm = true +reportInvalidTypeVarUse = true +reportMatchNotExhaustive = true +reportMissingImports = true +reportMissingModuleSource = true +reportMissingParameterType = true +reportMissingSuperCall = true +reportMissingTypeArgument = true +reportNoOverloadImplementation = true +reportOperatorIssue = true +reportOptionalCall = true +reportOptionalContextManager = true +reportOptionalIterable = true +reportOptionalMemberAccess = true +reportOptionalOperand = true +reportOptionalSubscript = true +reportOverlappingOverload = true +reportPossiblyUnboundVariable = true +reportPrivateImportUsage = true +reportPrivateUsage = true +reportPropertyTypeMismatch = true +reportRedeclaration = true +reportReturnType = true +reportSelfClsParameterName = true +reportShadowedImports = true +reportTypeCommentUsage = true +reportTypedDictNotRequiredAccess = true +reportUnboundVariable = true +reportUndefinedVariable = true +reportUnhashable = true +reportUninitializedInstanceVariable = true +reportUnknownArgumentType = true +reportUnknownLambdaType = true +reportUnknownMemberType = true +reportUnknownParameterType = true +reportUnknownVariableType = true +reportUnnecessaryCast = true +reportUnnecessaryComparison = true +reportUnnecessaryContains = true +reportUnnecessaryIsInstance = true +reportUnnecessaryTypeIgnoreComment = true +reportUnsupportedDunderAll = true +reportUntypedBaseClass = true +reportUntypedClassDecorator = true +reportUntypedFunctionDecorator = true +reportUntypedNamedTuple = true +reportUnusedCallResult = true +reportUnusedClass = true +reportUnusedCoroutine = true +reportUnusedExcept = true +reportUnusedExpression = true +reportUnusedFunction = true +reportUnusedImport = true +reportUnusedVariable = true +reportWildcardImportFromLibrary = true + +reportDeprecated = true + +# Can be enabled by generating type stubs for modules via pyright CLI +reportMissingTypeStubs = false + +# Bad rules +# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. diff --git a/sconstruct b/sconstruct index ac689d2..481a7ac 100644 --- a/sconstruct +++ b/sconstruct @@ -3,90 +3,51 @@ # This file is covered by the GNU General Public License. # See the file COPYING.txt for more details. -import codecs -import gettext import os import os.path -import zipfile import sys +from pathlib import Path +from collections.abc import Iterable +from typing import Final + +# While names imported below are available by default in every SConscript +# Linters aren't aware about them. +# To avoid PyRight `reportUndefinedVariable` errors about them they are imported explicitly. +# When using other Scons functions please add them to the line below. +from SCons.Script import EnsurePythonVersion, Variables, BoolVariable, Environment, Copy + +# Imports for type hints +from SCons.Node import FS # Add-on localization exchange facility and the template requires Python 3.10. # For best practice, use Python 3.11 or later to align with NVDA development. EnsurePythonVersion(3, 10) -sys.dont_write_bytecode = True # Bytecode should not be written for build vars module to keep the repository root folder clean. -import buildVars # NOQA: E402 - - -def md2html(source, dest): - import markdown - - # Use extensions if defined. - mdExtensions = buildVars.markdownExtensions - lang = os.path.basename(os.path.dirname(source)).replace("_", "-") - localeLang = os.path.basename(os.path.dirname(source)) - try: - _ = gettext.translation( - "nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang] - ).gettext - summary = _(buildVars.addon_info["addon_summary"]) - except Exception: - summary = buildVars.addon_info["addon_summary"] - title = "{addonSummary} {addonVersion}".format( - addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"] - ) - headerDic = { - '[[!meta title="': "# ", - '"]]': " #", - } - with codecs.open(source, "r", "utf-8") as f: - mdText = f.read() - for k, v in headerDic.items(): - mdText = mdText.replace(k, v, 1) - htmlText = markdown.markdown(mdText, extensions=mdExtensions) - # Optimization: build resulting HTML text in one go instead of writing parts separately. - docText = "\n".join( - [ - "", - f'', - "", - '', - '', - '', - f"{title}", - "\n", - htmlText, - "\n", - ] - ) - with codecs.open(dest, "w", "utf-8") as f: - f.write(docText) - +sys.dont_write_bytecode = True -def mdTool(env): - mdAction = env.Action( - lambda target, source, env: md2html(source[0].path, target[0].path), - lambda target, source, env: f"Generating {target[0]}", - ) - mdBuilder = env.Builder( - action=mdAction, - suffix=".html", - src_suffix=".md", - ) - env["BUILDERS"]["markdown"] = mdBuilder +import buildVars # NOQA: E402 -def validateVersionNumber(key, val, env): +def validateVersionNumber(key: str, val: str, _): # Used to make sure version major.minor.patch are integers to comply with NV Access add-on store. # Ignore all this if version number is not specified. if val == "0.0.0": return versionNumber = val.split(".") if len(versionNumber) < 3: - raise ValueError("versionNumber must have three parts (major.minor.patch)") + raise ValueError(f"{key} must have three parts (major.minor.patch)") if not all([part.isnumeric() for part in versionNumber]): - raise ValueError("versionNumber (major.minor.patch) must be integers") + raise ValueError(f"{key} (major.minor.patch) must be integers") + + +def expandGlobs(patterns: Iterable[str], rootdir: Path = Path(".")) -> list[FS.Entry]: + return [env.Entry(e) for pattern in patterns for e in rootdir.glob(pattern.lstrip('/'))] + + +addonDir: Final = Path("addon/") +localeDir: Final = addonDir / "locale" +docsDir: Final = addonDir / "doc" vars = Variables() @@ -95,170 +56,48 @@ vars.Add("versionNumber", "Version number of the form major.minor.patch", "0.0.0 vars.Add(BoolVariable("dev", "Whether this is a daily development version", False)) vars.Add("channel", "Update channel for this build", buildVars.addon_info["addon_updateChannel"]) -env = Environment(variables=vars, ENV=os.environ, tools=["gettexttool", mdTool]) -env.Append(**buildVars.addon_info) +env = Environment(variables=vars, ENV=os.environ, tools=["gettexttool", "NVDATool"]) +env.Append( + addon_info=buildVars.addon_info, + brailleTables=buildVars.brailleTables, + symbolDictionaries=buildVars.symbolDictionaries, +) if env["dev"]: - import datetime + from datetime import date - buildDate = datetime.datetime.now() - year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day) - versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)]) - env["addon_version"] = f"{versionTimestamp}.0.0" - env["versionNumber"] = f"{versionTimestamp}.0.0" + versionTimestamp = date.today().strftime('%Y%m%d') + version = f"{versionTimestamp}.0.0" + env["addon_info"]["addon_version"] = version + env["versionNumber"] = version env["channel"] = "dev" elif env["version"] is not None: - env["addon_version"] = env["version"] + env["addon_info"]["addon_version"] = env["version"] if "channel" in env and env["channel"] is not None: - env["addon_updateChannel"] = env["channel"] - -buildVars.addon_info["addon_version"] = env["addon_version"] -buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"] - -addonFile = env.File("${addon_name}-${addon_version}.nvda-addon") - - -def addonGenerator(target, source, env, for_signature): - action = env.Action( - lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, - lambda target, source, env: f"Generating Addon {target[0]}", - ) - return action + env["addon_info"]["addon_updateChannel"] = env["channel"] +# This is necessary for further use in formatting file names. +env.Append(**env["addon_info"]) -def manifestGenerator(target, source, env, for_signature): - action = env.Action( - lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None, - lambda target, source, env: f"Generating manifest {target[0]}", - ) - return action - - -def translatedManifestGenerator(target, source, env, for_signature): - dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), "..")) - lang = os.path.basename(dir) - action = env.Action( - lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) - and None, - lambda target, source, env: f"Generating translated manifest {target[0]}", - ) - return action - - -env["BUILDERS"]["NVDAAddon"] = Builder(generator=addonGenerator) -env["BUILDERS"]["NVDAManifest"] = Builder(generator=manifestGenerator) -env["BUILDERS"]["NVDATranslatedManifest"] = Builder(generator=translatedManifestGenerator) - - -def createAddonHelp(dir): - docsDir = os.path.join(dir, "doc") - if os.path.isfile("style.css"): - cssPath = os.path.join(docsDir, "style.css") - cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE")) - env.Depends(addon, cssTarget) - if os.path.isfile("readme.md"): - readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md") - readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE")) - env.Depends(addon, readmeTarget) - - -def createAddonBundleFromPath(path, dest): - """Creates a bundle from a directory that contains an addon manifest file.""" - basedir = os.path.abspath(path) - with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z: - # FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled. - for dir, dirnames, filenames in os.walk(basedir): - relativePath = os.path.relpath(dir, basedir) - for filename in filenames: - pathInBundle = os.path.join(relativePath, filename) - absPath = os.path.join(dir, filename) - if pathInBundle not in buildVars.excludedFiles: - z.write(absPath, pathInBundle) - return dest - - -def generateManifest(source, dest): - # Prepare the root manifest section - addon_info = buildVars.addon_info - with codecs.open(source, "r", "utf-8") as f: - manifest_template = f.read() - manifest = manifest_template.format(**addon_info) - # Add additional manifest sections such as custom braile tables - # Custom braille translation tables - if getattr(buildVars, "brailleTables", {}): - manifest_brailleTables = ["\n[brailleTables]"] - for table in buildVars.brailleTables.keys(): - manifest_brailleTables.append(f"[[{table}]]") - for key, val in buildVars.brailleTables[table].items(): - manifest_brailleTables.append(f"{key} = {val}") - manifest += "\n".join(manifest_brailleTables) + "\n" - # Custom speech symbol dictionaries - if getattr(buildVars, "symbolDictionaries", {}): - manifest_symbolDictionaries = ["\n[symbolDictionaries]"] - for dictionary in buildVars.symbolDictionaries.keys(): - manifest_symbolDictionaries.append(f"[[{dictionary}]]") - for key, val in buildVars.symbolDictionaries[dictionary].items(): - manifest_symbolDictionaries.append(f"{key} = {val}") - manifest += "\n".join(manifest_symbolDictionaries) + "\n" - - with codecs.open(dest, "w", "utf-8") as f: - f.write(manifest) - - -def generateTranslatedManifest(source, language, out): - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext - vars = {} - for var in ("addon_summary", "addon_description"): - vars[var] = _(buildVars.addon_info[var]) - with codecs.open(source, "r", "utf-8") as f: - manifest_template = f.read() - result = manifest_template.format(**vars) - # Add additional manifest sections such as custom braile tables - # Custom braille translation tables - if getattr(buildVars, "brailleTables", {}): - result_brailleTables = ["\n[brailleTables]"] - for table in buildVars.brailleTables.keys(): - result_brailleTables.append(f"[[{table}]]") - # Fetch display name only. - result_brailleTables.append(f"displayName = {_(buildVars.brailleTables[table]['displayName'])}") - result += "\n".join(result_brailleTables) + "\n" - - # Custom speech symbol dictionaries - if getattr(buildVars, "symbolDictionaries", {}): - result_symbolDictionaries = ["\n[symbolDictionaries]"] - for dictionary in buildVars.symbolDictionaries.keys(): - result_symbolDictionaries.append(f"[[{dictionary}]]") - # Fetch display name only. - result_symbolDictionaries.append( - f"displayName = {_(buildVars.symbolDictionaries[dictionary]['displayName'])}" - ) - result += "\n".join(result_symbolDictionaries) + "\n" - - with codecs.open(out, "w", "utf-8") as f: - f.write(result) - - -def expandGlobs(files): - return [f for pattern in files for f in env.Glob(pattern)] - - -addon = env.NVDAAddon(addonFile, env.Dir("addon")) +addonFile = env.File("${addon_name}-${addon_version}.nvda-addon") +addon = env.NVDAAddon(addonFile, env.Dir(addonDir), excludePatterns=buildVars.excludedFiles) -langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))] +langDirs: list[FS.Dir] = [env.Dir(d) for d in env.Glob(localeDir/"*/") if d.isdir()] # Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated -moByLang = {} +moByLang: dict[str, FS.File] = {} for dir in langDirs: poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po")) - moFile = env.gettextMoFile(poFile) - moByLang[dir] = moFile - env.Depends(moFile, poFile) + moTarget = env.gettextMoFile(poFile) + moFile = env.File(moTarget[0]) + moByLang[dir.name] = moFile + env.Depends(moTarget, poFile) translatedManifest = env.NVDATranslatedManifest( - dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")] + dir.File("manifest.ini"), [moFile, "manifest-translated.ini.tpl"] ) env.Depends(translatedManifest, ["buildVars.py"]) - env.Depends(addon, [translatedManifest, moFile]) + env.Depends(addon, [translatedManifest, moTarget]) pythonFiles = expandGlobs(buildVars.pythonSources) for file in pythonFiles: @@ -266,13 +105,22 @@ for file in pythonFiles: # Convert markdown files to html # We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager -createAddonHelp("addon") -for mdFile in env.Glob(os.path.join("addon", "doc", "*", "*.md")): +if (cssFile := Path("style.css")).is_file(): + cssPath = docsDir / cssFile + cssTarget = env.Command(str(cssPath), str(cssFile), Copy("$TARGET", "$SOURCE")) + env.Depends(addon, cssTarget) + +if (readmeFile := Path("readme.md")).is_file(): + readmePath = docsDir / buildVars.baseLanguage / readmeFile + readmeTarget = env.Command(str(readmePath), str(readmeFile), Copy("$TARGET", "$SOURCE")) + env.Depends(addon, readmeTarget) + +for mdFile in env.Glob(docsDir/"*/*.md"): # the title of the html file is translated based on the contents of something in the moFile for a language. # Thus, we find the moFile for this language and depend on it if it exists. - lang = os.path.basename(os.path.dirname(mdFile.get_abspath())) + lang = mdFile.dir.name moFile = moByLang.get(lang) - htmlFile = env.markdown(mdFile) + htmlFile = env.md2html(mdFile, moFile=moFile, mdExtensions=buildVars.markdownExtensions) env.Depends(htmlFile, mdFile) if moFile: env.Depends(htmlFile, moFile) @@ -280,7 +128,7 @@ for mdFile in env.Glob(os.path.join("addon", "doc", "*", "*.md")): # Pot target i18nFiles = expandGlobs(buildVars.i18nSources) -gettextvars = { +gettextvars: dict[str, str] = { "gettext_package_bugs_address": "nvda-translations@groups.io", "gettext_package_name": buildVars.addon_info["addon_name"], "gettext_package_version": buildVars.addon_info["addon_version"], @@ -294,7 +142,7 @@ env.Alias("mergePot", mergePot) env.Depends(mergePot, i18nFiles) # Generate Manifest path -manifest = env.NVDAManifest(os.path.join("addon", "manifest.ini"), os.path.join("manifest.ini.tpl")) +manifest = env.NVDAManifest(env.File(addonDir/"manifest.ini"), "manifest.ini.tpl") # Ensure manifest is rebuilt if buildVars is updated. env.Depends(manifest, "buildVars.py") diff --git a/site_scons/site_tools/NVDATool/__init__.py b/site_scons/site_tools/NVDATool/__init__.py new file mode 100644 index 0000000..6b4a37c --- /dev/null +++ b/site_scons/site_tools/NVDATool/__init__.py @@ -0,0 +1,105 @@ +""" +This tool generates NVDA extensions. + +Builders: + +- NVDAAddon: Creates a .nvda-addon zip file. Requires the `excludePatterns` environment variable. +- NVDAManifest: Creates the manifest.ini file. +- NVDATranslatedManifest: Creates the manifest.ini file with only translated information. +- md2html: Build HTML from Markdown + +The following environment variables are required to create the manifest: + +- addon_info: .typing.AddonInfo +- brailleTables: .typings.BrailleTables +- symbolDictionaries: .typings.SymbolDictionaries + +The following environment variables are required to build the HTML: + +- moFile: str | pathlib.Path | None +- mdExtensions: list[str] +- addon_info: .typings.AddonInfo + +""" + +from SCons.Script import Environment, Builder + +from .addon import createAddonBundleFromPath +from .manifests import generateManifest, generateTranslatedManifest +from .docs import md2html + + + +def generate(env: Environment): + env.SetDefault(excludePatterns=tuple()) + + addonAction = env.Action( + lambda target, source, env: createAddonBundleFromPath( + source[0].abspath, target[0].abspath, env["excludePatterns"] + ) and None, + lambda target, source, env: f"Generating Addon {target[0]}", + ) + env["BUILDERS"]["NVDAAddon"] = Builder( + action=addonAction, + suffix=".nvda-addon", + src_suffix="/" + ) + + env.SetDefault(brailleTables={}) + env.SetDefault(symbolDictionaries={}) + + manifestAction = env.Action( + lambda target, source, env: generateManifest( + source[0].abspath, + target[0].abspath, + addon_info=env["addon_info"], + brailleTables=env["brailleTables"], + symbolDictionaries=env["symbolDictionaries"], + ) and None, + lambda target, source, env: f"Generating manifest {target[0]}", + ) + env["BUILDERS"]["NVDAManifest"] = Builder( + action=manifestAction, + suffix=".ini", + src_siffix=".ini.tpl" + ) + + translatedManifestAction = env.Action( + lambda target, source, env: generateTranslatedManifest( + source[1].abspath, + target[0].abspath, + mo=source[0].abspath, + addon_info=env["addon_info"], + brailleTables=env["brailleTables"], + symbolDictionaries=env["symbolDictionaries"], + ) and None, + lambda target, source, env: f"Generating translated manifest {target[0]}", + ) + + env["BUILDERS"]["NVDATranslatedManifest"] = Builder( + action=translatedManifestAction, + suffix=".ini", + src_siffix=".ini.tpl" + ) + + env.SetDefault(mdExtensions = {}) + + mdAction = env.Action( + lambda target, source, env: md2html( + source[0].path, + target[0].path, + moFile=env["moFile"].path if env["moFile"] else None, + mdExtensions=env["mdExtensions"], + addon_info=env["addon_info"], + ) and None, + lambda target, source, env: f"Generating {target[0]}", + ) + env["BUILDERS"]["md2html"] = env.Builder( + action=mdAction, + suffix=".html", + src_suffix=".md", + ) + + +def exists(): + return True diff --git a/site_scons/site_tools/NVDATool/addon.py b/site_scons/site_tools/NVDATool/addon.py new file mode 100644 index 0000000..42e8d0e --- /dev/null +++ b/site_scons/site_tools/NVDATool/addon.py @@ -0,0 +1,24 @@ +import zipfile +from collections.abc import Iterable +from pathlib import Path + + + +def matchesNoPatterns(path: Path, patterns: Iterable[str]) -> bool: + """Checks if the path, the first argument, does not match any of the patterns passed as the second argument.""" + return not any((path.match(pattern) for pattern in patterns)) + + +def createAddonBundleFromPath(path: str | Path, dest: str, excludePatterns: Iterable[str]): + """Creates a bundle from a directory that contains an addon manifest file.""" + if isinstance(path, str): + path = Path(path) + basedir = path.absolute() + with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z: + for p in basedir.rglob("*"): + if p.is_dir(): + continue + pathInBundle = p.relative_to(basedir) + if matchesNoPatterns(pathInBundle, excludePatterns): + z.write(p, pathInBundle) + return dest diff --git a/site_scons/site_tools/NVDATool/docs.py b/site_scons/site_tools/NVDATool/docs.py new file mode 100644 index 0000000..abccd41 --- /dev/null +++ b/site_scons/site_tools/NVDATool/docs.py @@ -0,0 +1,61 @@ + +import gettext +from pathlib import Path + +import markdown + +from .typings import AddonInfo + + + +def md2html( + source: str | Path, + dest: str | Path, + *, + moFile: str | Path|None, + mdExtensions: list[str], + addon_info: AddonInfo + ): + if isinstance(source, str): + source = Path(source) + if isinstance(dest, str): + dest = Path(dest) + if isinstance(moFile, str): + moFile = Path(moFile) + + try: + with moFile.open("rb") as f: + _ = gettext.GNUTranslations(f).gettext + except Exception: + summary = addon_info["addon_summary"] + else: + summary = _(addon_info["addon_summary"]) + version = addon_info["addon_version"] + title = f"{summary} {version}" + lang = source.parent.name.replace("_", "-") + headerDic = { + '[[!meta title="': "# ", + '"]]': " #", + } + with source.open("r", encoding="utf-8") as f: + mdText = f.read() + for k, v in headerDic.items(): + mdText = mdText.replace(k, v, 1) + htmlText = markdown.markdown(mdText, extensions=mdExtensions) + # Optimization: build resulting HTML text in one go instead of writing parts separately. + docText = "\n".join( + ( + "", + f'', + "", + '', + '', + '', + f"{title}", + "\n", + htmlText, + "\n", + ) + ) + with dest.open("w", encoding="utf-8") as f: + f.write(docText) # type: ignore diff --git a/site_scons/site_tools/NVDATool/manifests.py b/site_scons/site_tools/NVDATool/manifests.py new file mode 100644 index 0000000..1e38348 --- /dev/null +++ b/site_scons/site_tools/NVDATool/manifests.py @@ -0,0 +1,69 @@ + +import codecs +import gettext +from functools import partial + +from .typings import AddonInfo, BrailleTables, SymbolDictionaries +from .utils import format_nested_section + + + +def generateManifest( + source: str, + dest: str, + addon_info: AddonInfo, + brailleTables: BrailleTables, + symbolDictionaries: SymbolDictionaries, + ): + # Prepare the root manifest section + with codecs.open(source, "r", "utf-8") as f: + manifest_template = f.read() + manifest = manifest_template.format(**addon_info) + # Add additional manifest sections such as custom braile tables + # Custom braille translation tables + if brailleTables: + manifest += format_nested_section("brailleTables", brailleTables) + + # Custom speech symbol dictionaries + if symbolDictionaries: + manifest += format_nested_section("symbolDictionaries", symbolDictionaries) + + with codecs.open(dest, "w", "utf-8") as f: + f.write(manifest) + + +def generateTranslatedManifest( + source: str, + dest: str, + *, + mo: str, + addon_info: AddonInfo, + brailleTables: BrailleTables, + symbolDictionaries: SymbolDictionaries, + ): + with open(mo, "rb") as f: + _ = gettext.GNUTranslations(f).gettext + vars: dict[str, str] = {} + for var in ("addon_summary", "addon_description", "addon_changelog"): + vars[var] = _(addon_info[var]) + with codecs.open(source, "r", "utf-8") as f: + manifest_template = f.read() + manifest = manifest_template.format(**vars) + + _format_section_only_with_displayName = partial( + format_nested_section, + include_only_keys = ("displayName",), + _ = _, + ) + + # Add additional manifest sections such as custom braile tables + # Custom braille translation tables + if brailleTables: + manifest += _format_section_only_with_displayName("brailleTables", brailleTables) + + # Custom speech symbol dictionaries + if symbolDictionaries: + manifest += _format_section_only_with_displayName("symbolDictionaries", symbolDictionaries) + + with codecs.open(dest, "w", "utf-8") as f: + f.write(manifest) diff --git a/site_scons/site_tools/NVDATool/typings.py b/site_scons/site_tools/NVDATool/typings.py new file mode 100644 index 0000000..6b1b3f5 --- /dev/null +++ b/site_scons/site_tools/NVDATool/typings.py @@ -0,0 +1,39 @@ +from typing import TypedDict, Protocol + + + +class AddonInfo(TypedDict): + addon_name: str + addon_summary: str + addon_description: str + addon_version: str + addon_changelog: str + addon_author: str + addon_url: str | None + addon_sourceURL: str | None + addon_docFileName: str + addon_minimumNVDAVersion: str | None + addon_lastTestedNVDAVersion: str | None + addon_updateChannel: str | None + addon_license: str | None + addon_licenseURL: str | None + + +class BrailleTableAttributes(TypedDict): + displayName: str + contracted: bool + output: bool + input: bool + + +class SymbolDictionaryAttributes(TypedDict): + displayName: str + mandatory: bool + + +BrailleTables = dict[str, BrailleTableAttributes] +SymbolDictionaries = dict[str, SymbolDictionaryAttributes] + + +class Strable(Protocol): + def __str__(self) -> str: ... diff --git a/site_scons/site_tools/NVDATool/utils.py b/site_scons/site_tools/NVDATool/utils.py new file mode 100644 index 0000000..0cc833c --- /dev/null +++ b/site_scons/site_tools/NVDATool/utils.py @@ -0,0 +1,28 @@ +from collections.abc import Callable, Container, Mapping + +from .typings import Strable + + + +def _(arg: str) -> str: + """ + A function that passes the string to it without doing anything to it. + Needed for recognizing strings for translation by Gettext. + """ + return arg + + +def format_nested_section( + section_name: str, + data: Mapping[str, Mapping[str, Strable]], + include_only_keys: Container[str] | None = None, + _: Callable[[str], str] = _, +) -> str: + lines = [f"\n[{section_name}]"] + for item_name, inner_dict in data.items(): + lines.append(f"[[{item_name}]]") + for key, val in inner_dict.items(): + if include_only_keys and key not in include_only_keys: + continue + lines.append(f"{key} = {_(str(val))}") + return "\n".join(lines) + "\n" From 46bc75c2df338dd24af5424e1ef6a8b368963124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:15:04 +0000 Subject: [PATCH 2/2] Bump actions/upload-artifact from 4 to 6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_addon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 65a5bed..6ca0a68 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -40,7 +40,7 @@ jobs: - name: building addon run: scons && scons pot - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: packaged_addon path: |