diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2d..fa0a1048 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,4 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off" -} \ No newline at end of file +} diff --git a/ChangeLog.md b/ChangeLog.md index d9be4f54..a8c9f4ed 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,11 @@ # Write Your Python Program - CHANGELOG +* 2.0.3 (2025-09-24) + * More fixes +* 2.0.2 (2025-09-24) + * More fixes +* 2.0.1 (2025-09-24) + * Minor fixes * 2.0.0 (2025-09-24) * Remove wrappers, only check types at function enter/exit points * Restructure directory layout diff --git a/README.md b/README.md index fde41b84..0556e871 100644 --- a/README.md +++ b/README.md @@ -33,29 +33,14 @@ After installation, you can use the `wypp` command for running your python files, making all features explained below available. Run `wypp --help` for usage information. -## What's new? - -Here is the [Changelog](ChangeLog.md). - -* **Breaking change** in version 2.0.0 (2025-0-24): type annotations are now only - checked when entering/exiting a function. Before, certain things such as lists - or callable were put behind wrapper objects. For example, these wrappers ensured - that only ints could be appended to a list of type `list[int]`. However, these - wrappers came with several drawbacks, so they were removed in release 2.0.0 -* **Breaking change** in version 0.12.0 (2021-09-28): type annotations are now checked - dynamically when the code is executed. - This behavior can be deactivated in the settings of the extension. -* **Breaking change** in version 0.11.0 (2021-03-11): wypp is no longer automatically imported. -You need an explicit import statement such as `from wypp import *`. - ## Features Here is a screen shot: -![Screenshot](screenshot.png) +![Screenshot](screenshot.jpg) There is also a visualization mode, similar to [Python Tutor](https://pythontutor.com/): -![Screenshot](screenshot2.png) +![Screenshot](screenshot2.jpg) When hitting the RUN button, the vscode extension saves the current file, opens a terminal and executes the file with Python, staying in interactive mode after @@ -198,13 +183,27 @@ before the type being defined, for example to define recursive types or as the type of `self` inside of classes. In fact, there is no check at all to make sure that anotations refer to existing types. - ## Module name and current working directory When executing a python file with the RUN button, the current working directory is set to the directory of the file being executed. The `__name__` attribute is set to the value `'__wypp__'`. +## What's new? + +Here is the [Changelog](ChangeLog.md). + +* **Breaking change** in version 2.0.0 (2025-09-24): type annotations are now only + checked when entering/exiting a function. Before, certain things such as lists + or callable were put behind wrapper objects. For example, these wrappers ensured + that only ints could be appended to a list of type `list[int]`. However, these + wrappers came with several drawbacks, so they were removed in release 2.0.0 +* **Breaking change** in version 0.12.0 (2021-09-28): type annotations are now checked + dynamically when the code is executed. + This behavior can be deactivated in the settings of the extension. +* **Breaking change** in version 0.11.0 (2021-03-11): wypp is no longer automatically imported. +You need an explicit import statement such as `from wypp import *`. + ## Bugs & Problems Please report them in the [issue tracker](https://github.com/skogsbaer/write-your-python-program/issues). diff --git a/package.json b/package.json index 195b628a..e9bb2b10 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Write Your Python Program!", "description": "A user friendly python environment for beginners", "license": "See license in LICENSE", - "version": "2.0.0", + "version": "2.0.3", "publisher": "StefanWehr", "icon": "icon.png", "engines": { diff --git a/python/code/wypp/i18n.py b/python/code/wypp/i18n.py index a9482ef0..d25b2ef5 100644 --- a/python/code/wypp/i18n.py +++ b/python/code/wypp/i18n.py @@ -140,7 +140,25 @@ def tr(key: str, **kws) -> str: 'Constructor of record `{cls}` does not accept keyword argument `{name}`.': 'Konstruktor des Records `{cls}` akzeptiert kein Schlüsselwort-Argument `{name}`.', - 'invalid record definition': 'ungültige Record-Definition' + 'invalid record definition': 'ungültige Record-Definition', + + 'Expected {expected}, but the result is {actual}': + 'Erwartet wird {expected}, aber das Ergebnis ist {actual}', + 'ERROR: ': 'FEHLER: ', + 'ERROR in ': 'FEHLER in ', + 'File {filename}, line {lineno}: ': 'Datei {filename}, Zeile {lineno}: ', + 'Uncovered case': 'Ein Fall ist nicht abgedeckt', + 'uncovered case': 'ein Fall ist nicht abgedeckt', + 'The impossible happened!': 'Das Unmögliche ist passiert!', + 'Stop of execution': 'Abbruch der Ausführung', + '1 successful test': '1 erfolgreicher Test', + 'all succesful': 'alle erfolgreich', + 'and stop of execution': 'und Abbruch der Ausführung', + 'all successful': 'alle erfolgreich', + + 'NOTE: running the code failed, some definitions might not be available in the interactive window!': + 'ACHTUNG: der Code enthält Fehler, einige Definition sind möglicherweise in interaktiven Fenster nicht verfügbar!' + } def expectingNoReturn(cn: location.CallableName) -> str: @@ -346,3 +364,40 @@ def unknownKeywordArgument(cn: location.CallableName, name: str) -> str: return tr('Constructor of record `{cls}` does not accept keyword argument `{name}`.', cls=cls, name=name) raise ValueError(f'Unexpected: {cn}') + +def checkExpected(expected: str, actual: str) -> str: + return tr('Expected {expected}, but the result is {actual}', expected=expected,actual=actual) + +def numTests(n: int) -> str: + match getLang(): + case 'en': + if n == 0: + return 'no tests' + elif n == 1: + return '1 test' + else: + return f'{n} tests' + case 'de': + if n == 0: + return 'keine Tests' + elif n == 1: + return '1 Test' + else: + return f'{n} Tests' + +def numFailing(n: int) -> str: + match getLang(): + case 'en': + if n == 0: + return 'no errors' + elif n == 1: + return '1 error' + else: + return f'{n} errors' + case 'de': + if n == 0: + return 'keine Fehler' + elif n == 1: + return '1 Fehler' + else: + return f'{n} Fehler' diff --git a/python/code/wypp/interactive.py b/python/code/wypp/interactive.py index 6e1ef9d7..9cf55ff0 100644 --- a/python/code/wypp/interactive.py +++ b/python/code/wypp/interactive.py @@ -8,6 +8,7 @@ from constants import * from myLogging import * from exceptionHandler import handleCurrentException +import i18n def prepareInteractive(reset=True): print() @@ -41,7 +42,7 @@ def enterInteractive(userDefs: dict, checkTypes: bool, loadingFailed: bool): globals()[k] = v print() if loadingFailed: - print('NOTE: running the code failed, some definitions might not be available!') + print(i18n.tr('NOTE: running the code failed, some definitions might not be available in the interactive window!')) print() if checkTypes: consoleClass = TypecheckedInteractiveConsole diff --git a/python/code/wypp/lang.py b/python/code/wypp/lang.py index e19a7b6f..271114b4 100644 --- a/python/code/wypp/lang.py +++ b/python/code/wypp/lang.py @@ -4,7 +4,6 @@ def _langFromEnv(env: MutableMapping) -> str | None: # 1) GNU LANGUAGE: colon-separated fallbacks (e.g., "de:en_US:en") - os.getenv lng = env.get("LANGUAGE") if lng: for part in lng.split(":"): @@ -35,9 +34,11 @@ def _normLang(tag: str) -> str: def pickLanguage[T: str](supported: list[T], default: T) -> T: """Return best match like 'de' or 'de_DE' from supported codes.""" - raw = _langFromEnv(os.environ) + (raw, _) = locale.getlocale() if not raw: - return default + raw = _langFromEnv(os.environ) + if not raw: + return default want = _normLang(raw) # exact match first for s in supported: diff --git a/python/code/wypp/writeYourProgram.py b/python/code/wypp/writeYourProgram.py index 55902409..a04442db 100644 --- a/python/code/wypp/writeYourProgram.py +++ b/python/code/wypp/writeYourProgram.py @@ -9,6 +9,7 @@ import location import paths import utils +import i18n _DEBUG = False def _debug(s): @@ -147,23 +148,21 @@ def printTestResults(prefix='', loadingFailed=False): failing = _testCount['failing'] bad = '🙁' good = '😀' - tests = f'{prefix}{total} Tests' - if total == 1: - tests = f'{prefix}{total} Test' + tests = f'{prefix}' + i18n.numTests(total) if total == 0: pass elif failing == 0: if loadingFailed: - print(f'{tests}, Abbruch der Ausführung {bad}') + print(f'{tests}, {i18n.tr("Stop of execution")} {bad}') elif total == 1: - print(f'1 erfolgreicher Test {good}') + print(f'{i18n.tr("1 successful test")} {good}') else: - print(f'{tests}, alle erfolgreich {good}') + print(f'{tests}, {i18n.tr("all successful")} {good}') else: if loadingFailed: - print(f'{tests}, {failing} Fehler und Abbruch der Ausführung {bad}') + print(f'{tests}, {i18n.numFailing(failing)} and stop of execution {bad}') else: - print(f'{tests}, {failing} Fehler {bad}') + print(f'{tests}, {i18n.numFailing(failing)} {bad}') return {'total': total, 'failing': failing} def checkEq(actual, expected): @@ -197,7 +196,8 @@ def checkGeneric(actual, expected, *, structuralObjEq=True, floatEqWithDelta=Tru frame = stack[2] if len(stack) > 2 else None if frame: filename = paths.canonicalizePath(frame.filename) - caller = f"Datei {filename}, Zeile {frame.lineno}: " + caller = i18n.tr('File {filename}, line {lineno}: ', + filename=filename, lineno=frame.lineno) else: caller = "" def fmt(x): @@ -205,12 +205,11 @@ def fmt(x): return repr(x) else: return str(x) - msg = f"{caller}Erwartet wird {fmt(expected)}, aber das " \ - f"Ergebnis ist {fmt(actual)}" + msg = caller + i18n.checkExpected(fmt(expected), fmt(actual)) if _dieOnCheckFailures(): raise Exception(msg) else: - print("FEHLER in " + msg) + print(i18n.tr('ERROR in ') + msg) def checkFail(msg: str): if not _checksEnabled: @@ -220,15 +219,17 @@ def checkFail(msg: str): if _dieOnCheckFailures(): raise Exception(msg) else: - print("FEHLER: " + msg) + print(i18n.tr('ERROR: ') + msg) def uncoveredCase(): stack = inspect.stack() if len(stack) > 1: caller = stack[1] - raise Exception(f"{caller.filename}, Zeile {caller.lineno}: ein Fall ist nicht abgedeckt") + callerStr = i18n.tr('File {filename}, line {lineno}: ', + filename=caller.filename, lineno=caller.lineno) + raise Exception(callerStr + i18n.tr('uncovered case')) else: - raise Exception(f"Ein Fall ist nicht abgedeckt") + raise Exception(i18n.tr('Uncovered case')) # # Deep equality @@ -328,7 +329,8 @@ def todo(msg=None): def impossible(msg=None): if msg is None: - msg = 'Das Unmögliche ist passiert!' + msg = i18n.tr('The impossible happened!') + 'Das Unmögliche ist passiert!' raise errors.ImpossibleError(msg) # Additional functions and aliases diff --git a/python/file-test-data/basics/check2_ok.err b/python/file-test-data/basics/check2_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/check2_ok.err_en b/python/file-test-data/basics/check2_ok.err_en new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/check2_ok.out b/python/file-test-data/basics/check2_ok.out new file mode 100644 index 00000000..74642191 --- /dev/null +++ b/python/file-test-data/basics/check2_ok.out @@ -0,0 +1 @@ +2 Tests, alle erfolgreich 😀 diff --git a/python/file-test-data/basics/check2_ok.out_en b/python/file-test-data/basics/check2_ok.out_en new file mode 100644 index 00000000..d8138141 --- /dev/null +++ b/python/file-test-data/basics/check2_ok.out_en @@ -0,0 +1 @@ +2 tests, all successful 😀 diff --git a/python/file-test-data/basics/check2_ok.py b/python/file-test-data/basics/check2_ok.py new file mode 100644 index 00000000..f516326b --- /dev/null +++ b/python/file-test-data/basics/check2_ok.py @@ -0,0 +1,4 @@ +from wypp import * + +check(1, 1) +check(1, 1) diff --git a/python/file-test-data/basics/check_ok.err b/python/file-test-data/basics/check_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/check_ok.err_en b/python/file-test-data/basics/check_ok.err_en new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/check_ok.out b/python/file-test-data/basics/check_ok.out new file mode 100644 index 00000000..c90a856d --- /dev/null +++ b/python/file-test-data/basics/check_ok.out @@ -0,0 +1,2 @@ +FEHLER in Datei file-test-data/basics/check_ok.py, Zeile 4: Erwartet wird 0, aber das Ergebnis ist 1 +2 Tests, 1 Fehler 🙁 diff --git a/python/file-test-data/basics/check_ok.out_en b/python/file-test-data/basics/check_ok.out_en new file mode 100644 index 00000000..d0c34561 --- /dev/null +++ b/python/file-test-data/basics/check_ok.out_en @@ -0,0 +1,2 @@ +ERROR in File file-test-data/basics/check_ok.py, line 4: Expected 0, but the result is 1 +2 tests, 1 error 🙁 diff --git a/python/file-test-data/basics/check_ok.py b/python/file-test-data/basics/check_ok.py new file mode 100644 index 00000000..5165b8c7 --- /dev/null +++ b/python/file-test-data/basics/check_ok.py @@ -0,0 +1,4 @@ +from wypp import * + +check(1, 1) +check(1, 0) diff --git a/python/file-test-data/extras/testImpossible.err b/python/file-test-data/extras/testImpossible.err index 19e40e8d..95961a19 100644 --- a/python/file-test-data/extras/testImpossible.err +++ b/python/file-test-data/extras/testImpossible.err @@ -1,7 +1,7 @@ Traceback (most recent call last): File "file-test-data/extras/testImpossible.py", line 3, in impossible() - File "code/wypp/writeYourProgram.py", line 332, in impossible + File "code/wypp/writeYourProgram.py", line 334, in impossible raise errors.ImpossibleError(msg) Das Unmögliche ist passiert! diff --git a/python/file-test-data/extras/testTodo.err b/python/file-test-data/extras/testTodo.err index 3764a715..d4bcb7f7 100644 --- a/python/file-test-data/extras/testTodo.err +++ b/python/file-test-data/extras/testTodo.err @@ -1,7 +1,7 @@ Traceback (most recent call last): File "file-test-data/extras/testTodo.py", line 3, in todo() - File "code/wypp/writeYourProgram.py", line 327, in todo + File "code/wypp/writeYourProgram.py", line 328, in todo raise errors.TodoError(msg) TODO diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 00000000..8971ff93 Binary files /dev/null and b/screenshot.jpg differ diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 5dc05612..00000000 Binary files a/screenshot.png and /dev/null differ diff --git a/screenshot2.jpg b/screenshot2.jpg new file mode 100644 index 00000000..15724864 Binary files /dev/null and b/screenshot2.jpg differ diff --git a/screenshot2.png b/screenshot2.png deleted file mode 100644 index 4d591a8a..00000000 Binary files a/screenshot2.png and /dev/null differ diff --git a/src/extension.ts b/src/extension.ts index 81812b05..7fc9d822 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -221,15 +221,17 @@ async function fixPylanceConfig( context: vscode.ExtensionContext, folder?: vscode.WorkspaceFolder ) { - // disable warnings about wildcard imports, add wypp to pylance's extraPaths + // disable warnings about wildcard imports, set pylance's extraPaths to wypp + // turn typechecking off + // This is a quite distructive change, so we do it on first hit of the run button + // not on-load of the plugin const libDir = context.asAbsolutePath('python/code/'); - // Use the "python" section; Pylance contributes python.analysis.* const cfg = vscode.workspace.getConfiguration('python', folder?.uri); const target = folder ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - // Read existing overrides (don’t clobber other rules) + // wildcard warnings const keyOverride = 'analysis.diagnosticSeverityOverrides'; const overrides = cfg.get>(keyOverride) ?? {}; if (overrides.reportWildcardImportFromLibrary !== 'none') { @@ -244,16 +246,27 @@ async function fixPylanceConfig( ); } + // extraPaths const keyExtraPaths = 'analysis.extraPaths'; const extra = cfg.get(keyExtraPaths) ?? []; - if (!extra.includes(libDir)) { - const updated = [...extra, libDir]; + if (extra.length !== 1 || extra[0] !== libDir) { await cfg.update( keyExtraPaths, - [...extra, libDir], + [libDir], target ); - } + } + + // typechecking off + const keyMode = 'analysis.typeCheckingMode'; + const mode = cfg.get(keyMode) ?? ''; + if (mode !== 'off') { + await cfg.update( + 'analysis.typeCheckingMode', + 'off', + target + ); + } } class Location implements vscode.TerminalLink { @@ -360,7 +373,6 @@ export async function activate(context: vscode.ExtensionContext) { const outChannel = vscode.window.createOutputChannel("Write Your Python Program"); disposables.push(outChannel); - await fixPylanceConfig(context); const terminals: { [name: string]: TerminalContext } = {}; installButton("Write Your Python Program", undefined); @@ -374,6 +386,7 @@ export async function activate(context: vscode.ExtensionContext) { "run", "▶ RUN", async (cmdId) => { + await fixPylanceConfig(context); const file = (vscode.window.activeTextEditor) ? vscode.window.activeTextEditor.document.fileName :