From 8b6829ed8011d169fa008133751a80cadf08632f Mon Sep 17 00:00:00 2001 From: Shuang Wu Date: Fri, 3 Mar 2023 15:26:59 -0500 Subject: [PATCH] feat: add lupdate support Fix #282 --- package.json | 53 ++++++++++++-- python/.coveragerc | 1 + python/scripts/designer.py | 2 - python/scripts/lupdate.py | 20 ++++++ python/scripts/qml.py | 2 - python/scripts/qmlls.py | 2 - python/scripts/rcc.py | 2 - python/scripts/uic.py | 2 - .../assets/linguist/{widget => }/.gitignore | 0 .../linguist/{widget/main.py => sample.py} | 0 python/tests/test_lupdate.py | 35 +++++++++ src/commands.ts | 5 ++ src/lupdate/lupdate.ts | 31 ++++++++ src/test/suite/linguist/e2e.test.ts | 71 +++++++++++++++++++ src/test/suite/linguist/lupdate.test.ts | 12 ++++ src/test/suite/uic/compile-ui.test.ts | 4 +- src/types.ts | 8 ++- 17 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 python/scripts/lupdate.py rename python/tests/assets/linguist/{widget => }/.gitignore (100%) rename python/tests/assets/linguist/{widget/main.py => sample.py} (100%) create mode 100644 python/tests/test_lupdate.py create mode 100644 src/lupdate/lupdate.ts create mode 100644 src/test/suite/linguist/e2e.test.ts create mode 100644 src/test/suite/linguist/lupdate.test.ts diff --git a/package.json b/package.json index fb32293..2a423f7 100644 --- a/package.json +++ b/package.json @@ -106,27 +106,32 @@ "commands": [ { "command": "qtForPython.compileResource", - "title": "Compile Qt Resource File", + "title": "Compile Qt Resource File (rcc)", "category": "Qt for Python" }, { "command": "qtForPython.compileUi", - "title": "Compile Qt UI File", + "title": "Compile Qt UI File (uic)", "category": "Qt for Python" }, { "command": "qtForPython.createUi", - "title": "Create Qt UI File", + "title": "Create Qt UI File (designer)", "category": "Qt for Python" }, { "command": "qtForPython.editUi", - "title": "Edit Qt UI File", + "title": "Edit Qt UI File (designer)", "category": "Qt for Python" }, { "command": "qtForPython.previewQml", - "title": "Preview QML File", + "title": "Preview QML File (qml)", + "category": "Qt for Python" + }, + { + "command": "qtForPython.lupdate", + "title": "Update Qt Translation File (lupdate)", "category": "Qt for Python" } ], @@ -156,6 +161,11 @@ "command": "qtForPython.previewQml", "when": "resourceLangId == qml", "group": "qtForPython" + }, + { + "command": "qtForPython.lupdate", + "when": "resourceLangId == python || resourceLangId == qml || (resourceLangId == xml && resourceExtname == .ui)", + "group": "qtForPython" } ], "explorer/context": [ @@ -183,6 +193,11 @@ "command": "qtForPython.previewQml", "when": "resourceLangId == qml", "group": "qtForPython" + }, + { + "command": "qtForPython.lupdate", + "when": "resourceLangId == python || resourceLangId == qml || (resourceLangId == xml && resourceExtname == .ui)", + "group": "qtForPython" } ], "editor/title": [ @@ -210,6 +225,11 @@ "command": "qtForPython.previewQml", "when": "resourceLangId == qml", "group": "qtForPython" + }, + { + "command": "qtForPython.lupdate", + "when": "resourceLangId == python || resourceLangId == qml || (resourceLangId == xml && resourceExtname == .ui)", + "group": "qtForPython" } ], "editor/context": [ @@ -237,6 +257,11 @@ "command": "qtForPython.previewQml", "when": "resourceLangId == qml", "group": "qtForPython" + }, + { + "command": "qtForPython.lupdate", + "when": "resourceLangId == python || resourceLangId == qml || (resourceLangId == xml && resourceExtname == .ui)", + "group": "qtForPython" } ] }, @@ -352,6 +377,24 @@ "default": [], "markdownDescription": "The options passed to `pyside6-qml` executable for QML preview. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", "scope": "resource" + }, + "qtForPython.lupdate.path": { + "type": "string", + "default": "", + "markdownDescription": "The path to Qt `lupdate` executable. Set to empty string to automatically resolve from the installed Python package. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", + "scope": "resource" + }, + "qtForPython.lupdate.options": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "-ts", + "${resourceDirname}${pathSeparator}${resourceBasenameNoExtension}.ts" + ], + "markdownDescription": "The options passed to Qt `lupdate` executable. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", + "scope": "resource" } } }, diff --git a/python/.coveragerc b/python/.coveragerc index fc43754..61eb213 100644 --- a/python/.coveragerc +++ b/python/.coveragerc @@ -6,6 +6,7 @@ omit = scripts/uic.py scripts/designer.py scripts/qml.py + scripts/lupdate.py [report] fail_under = 100 diff --git a/python/scripts/designer.py b/python/scripts/designer.py index 257ceb1..f3555f7 100644 --- a/python/scripts/designer.py +++ b/python/scripts/designer.py @@ -1,6 +1,5 @@ # pylint: disable=import-error -import re import sys from utils import is_installed @@ -12,5 +11,4 @@ from PySide2.scripts.pyside_tool import designer else: sys.exit("No rcc can be found in current Python environment.") - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(designer()) diff --git a/python/scripts/lupdate.py b/python/scripts/lupdate.py new file mode 100644 index 0000000..7149ee0 --- /dev/null +++ b/python/scripts/lupdate.py @@ -0,0 +1,20 @@ +# pylint: disable=import-error + +import sys + +from utils import is_installed + +if __name__ == "__main__": + if is_installed("PySide6"): + from PySide6.scripts.pyside_tool import lupdate + elif is_installed("PySide2"): + sys.argv[0] = "pyside2-lupdate" + from PySide2.scripts.pyside_tool import main as lupdate + elif is_installed("PyQt6"): + from PyQt6.lupdate.pylupdate import main as lupdate + elif is_installed("PyQt5"): + from PyQt5.pylupdate_main import main as lupdate + else: + sys.exit("No lupdate can be found in current Python environment.") + + sys.exit(lupdate()) diff --git a/python/scripts/qml.py b/python/scripts/qml.py index 0627bf9..e4e0434 100644 --- a/python/scripts/qml.py +++ b/python/scripts/qml.py @@ -1,6 +1,5 @@ # pylint: disable=import-error -import re import sys from utils import is_installed @@ -10,5 +9,4 @@ from PySide6.scripts.pyside_tool import qml else: sys.exit("No pyside6-qml can be found in current Python environment.") - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(qml()) diff --git a/python/scripts/qmlls.py b/python/scripts/qmlls.py index 2b44b2d..99865e7 100644 --- a/python/scripts/qmlls.py +++ b/python/scripts/qmlls.py @@ -1,6 +1,5 @@ # pylint: disable=import-error -import re import sys from utils import is_installed @@ -10,5 +9,4 @@ from PySide6.scripts.pyside_tool import qmlls else: sys.exit("No qmlls can be found in current Python environment.") - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(qmlls()) diff --git a/python/scripts/rcc.py b/python/scripts/rcc.py index 75da117..906f2a9 100644 --- a/python/scripts/rcc.py +++ b/python/scripts/rcc.py @@ -1,6 +1,5 @@ # pylint: disable=import-error -import re import sys from utils import is_installed @@ -14,5 +13,4 @@ from PyQt5.pyrcc_main import main as rcc else: sys.exit("No rcc can be found in current Python environment.") - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(rcc()) diff --git a/python/scripts/uic.py b/python/scripts/uic.py index 926309d..ca95d6b 100644 --- a/python/scripts/uic.py +++ b/python/scripts/uic.py @@ -1,6 +1,5 @@ # pylint: disable=import-error -import re import sys from utils import is_installed @@ -16,5 +15,4 @@ from PyQt5.uic.pyuic import main as uic else: sys.exit("No rcc can be found in current Python environment.") - sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(uic()) diff --git a/python/tests/assets/linguist/widget/.gitignore b/python/tests/assets/linguist/.gitignore similarity index 100% rename from python/tests/assets/linguist/widget/.gitignore rename to python/tests/assets/linguist/.gitignore diff --git a/python/tests/assets/linguist/widget/main.py b/python/tests/assets/linguist/sample.py similarity index 100% rename from python/tests/assets/linguist/widget/main.py rename to python/tests/assets/linguist/sample.py diff --git a/python/tests/test_lupdate.py b/python/tests/test_lupdate.py new file mode 100644 index 0000000..f32637f --- /dev/null +++ b/python/tests/test_lupdate.py @@ -0,0 +1,35 @@ +import os +import subprocess + +from tests import ASSETS_DIR, SCRIPTS_DIR + + +def test_lupdate_help(): + result = invoke_lupdate_py(["-help"]) + assert result.returncode == 0 + assert len(result.stdout.decode("utf-8")) > 0 + + +def test_lupdate_sample_py(): + filename = "sample.py" + result = invoke_lupdate_py( + [get_assets_path(filename), "-ts", get_assets_path("sample.ts")] + ) + assert result.returncode == 0 + assert len(result.stdout.decode("utf-8")) > 0 + assert os.path.exists(get_assets_path("sample.ts")) + + os.remove(get_assets_path("sample.ts")) + + +def invoke_lupdate_py(args: list[str]): + return subprocess.run( + ["poetry", "run", "python", "lupdate.py", *args], + cwd=SCRIPTS_DIR, + capture_output=True, + check=True, + ) + + +def get_assets_path(filename: str) -> str: + return os.path.join(ASSETS_DIR, "linguist", filename) diff --git a/src/commands.ts b/src/commands.ts index 8850415..7a830b2 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,6 +5,7 @@ import { URI } from 'vscode-uri' import { EXTENSION_NAMESPACE } from './constants' import { createUi } from './designer/create-ui' import { editUi } from './designer/edit-ui' +import { lupdate } from './lupdate/lupdate' import { previewQml } from './qml/preview-qml' import { compileResource } from './rcc/compile-resource' import type { ErrorResult, SuccessResult } from './types' @@ -51,6 +52,10 @@ const COMMANDS = [ name: 'previewQml', callback: previewQml, }, + { + name: 'lupdate', + callback: lupdate, + }, ] as const type CommandCallbackValue = Awaited< diff --git a/src/lupdate/lupdate.ts b/src/lupdate/lupdate.ts new file mode 100644 index 0000000..a854f25 --- /dev/null +++ b/src/lupdate/lupdate.ts @@ -0,0 +1,31 @@ +import { firstValueFrom } from 'rxjs' +import type { CommandDeps } from '../commands' +import { getTargetDocumentUri } from '../commands' +import { run } from '../run' +import { getToolCommand$ } from '../tool-utils' + +export async function lupdate({ extensionUri }: CommandDeps, ...args: any[]) { + const targetDocumentUriResult = getTargetDocumentUri(...args) + + if (targetDocumentUriResult.kind !== 'Success') return targetDocumentUriResult + + const sourceFile = targetDocumentUriResult.value + + const getToolCommandResult = await firstValueFrom( + getToolCommand$({ + tool: 'lupdate', + extensionUri, + resource: sourceFile, + }), + ) + + if (getToolCommandResult.kind !== 'Success') return getToolCommandResult + + return run({ + command: [ + ...getToolCommandResult.value.command, + sourceFile.fsPath, + ...getToolCommandResult.value.options, + ], + }) +} diff --git a/src/test/suite/linguist/e2e.test.ts b/src/test/suite/linguist/e2e.test.ts new file mode 100644 index 0000000..3d61d0d --- /dev/null +++ b/src/test/suite/linguist/e2e.test.ts @@ -0,0 +1,71 @@ +import * as assert from 'node:assert' +import * as path from 'node:path' +import { commands, window, workspace } from 'vscode' +import { URI } from 'vscode-uri' +import { EXTENSION_NAMESPACE } from '../../../constants' +import { + E2E_TIMEOUT, + forceDeleteFile, + setupE2EEnvironment, + TEST_ASSETS_PATH, + waitFor, +} from '../test-utils' + +suite('linguist/e2e', () => { + suiteSetup(async function () { + this.timeout(E2E_TIMEOUT) + await setupE2EEnvironment() + }) + + suite('command palette', () => { + suite('when a Python file is open', () => { + const sampleFilenameNoExt = 'sample' + + setup(async function () { + this.timeout(E2E_TIMEOUT) + + await removeGeneratedFile(sampleFilenameNoExt) + + const document = await workspace.openTextDocument( + URI.file( + path.resolve( + TEST_ASSETS_PATH, + 'linguist', + `${sampleFilenameNoExt}.py`, + ), + ), + ) + await window.showTextDocument(document) + }) + + teardown(async function () { + this.timeout(E2E_TIMEOUT) + await removeGeneratedFile(sampleFilenameNoExt) + }) + + test('should run lupdate command', async () => { + await commands.executeCommand(`${EXTENSION_NAMESPACE}.lupdate`) + + return waitFor(async () => { + const readResult = await workspace.fs.readFile( + URI.file( + path.resolve( + TEST_ASSETS_PATH, + 'linguist', + `${sampleFilenameNoExt}.ts`, + ), + ), + ) + + assert.ok(readResult.byteLength > 0) + }) + }).timeout(E2E_TIMEOUT) + }).timeout(E2E_TIMEOUT) + }).timeout(E2E_TIMEOUT) +}).timeout(E2E_TIMEOUT) + +async function removeGeneratedFile(sampleFilenameNoExt: string) { + return forceDeleteFile( + path.resolve(TEST_ASSETS_PATH, 'linguist', `${sampleFilenameNoExt}.ts`), + ) +} diff --git a/src/test/suite/linguist/lupdate.test.ts b/src/test/suite/linguist/lupdate.test.ts new file mode 100644 index 0000000..4523194 --- /dev/null +++ b/src/test/suite/linguist/lupdate.test.ts @@ -0,0 +1,12 @@ +import * as assert from 'node:assert' +import { commands } from 'vscode' +import { EXTENSION_NAMESPACE } from '../../../constants' + +suite('lupdate', () => { + test('should include the command', async () => + assert.ok( + (await commands.getCommands(true)).includes( + `${EXTENSION_NAMESPACE}.lupdate`, + ), + )) +}) diff --git a/src/test/suite/uic/compile-ui.test.ts b/src/test/suite/uic/compile-ui.test.ts index 8842481..e9419ae 100644 --- a/src/test/suite/uic/compile-ui.test.ts +++ b/src/test/suite/uic/compile-ui.test.ts @@ -25,7 +25,7 @@ suite('compile-ui/e2e', () => { )) suite('command palette', () => { - suite('when a ui file is open', () => { + suite('when an ui file is open', () => { const sampleFilenameNoExt = 'sample' setup(async function () { @@ -63,7 +63,7 @@ suite('compile-ui/e2e', () => { assert.ok(readResult.byteLength > 0) }) }).timeout(E2E_TIMEOUT) - }) + }).timeout(E2E_TIMEOUT) }).timeout(E2E_TIMEOUT) }).timeout(E2E_TIMEOUT) diff --git a/src/types.ts b/src/types.ts index 03622a0..3f45374 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,12 @@ // Should NOT depend on vscode -export type SupportedTool = 'qmlls' | 'rcc' | 'uic' | 'designer' | 'qml' +export type SupportedTool = + | 'qmlls' + | 'rcc' + | 'uic' + | 'designer' + | 'qml' + | 'lupdate' export type SuccessResult = { readonly kind: `${Name}Success`