Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}
}
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
35 changes: 17 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
57 changes: 56 additions & 1 deletion python/code/wypp/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'
3 changes: 2 additions & 1 deletion python/code/wypp/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from constants import *
from myLogging import *
from exceptionHandler import handleCurrentException
import i18n

def prepareInteractive(reset=True):
print()
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions python/code/wypp/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(":"):
Expand Down Expand Up @@ -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:
Expand Down
34 changes: 18 additions & 16 deletions python/code/wypp/writeYourProgram.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import location
import paths
import utils
import i18n

_DEBUG = False
def _debug(s):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -197,20 +196,20 @@ 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):
if type(x) == str:
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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Empty file.
Empty file.
1 change: 1 addition & 0 deletions python/file-test-data/basics/check2_ok.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2 Tests, alle erfolgreich 😀
1 change: 1 addition & 0 deletions python/file-test-data/basics/check2_ok.out_en
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2 tests, all successful 😀
4 changes: 4 additions & 0 deletions python/file-test-data/basics/check2_ok.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from wypp import *

check(1, 1)
check(1, 1)
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions python/file-test-data/basics/check_ok.out
Original file line number Diff line number Diff line change
@@ -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 🙁
2 changes: 2 additions & 0 deletions python/file-test-data/basics/check_ok.out_en
Original file line number Diff line number Diff line change
@@ -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 🙁
4 changes: 4 additions & 0 deletions python/file-test-data/basics/check_ok.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from wypp import *

check(1, 1)
check(1, 0)
2 changes: 1 addition & 1 deletion python/file-test-data/extras/testImpossible.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Traceback (most recent call last):
File "file-test-data/extras/testImpossible.py", line 3, in <module>
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!
2 changes: 1 addition & 1 deletion python/file-test-data/extras/testTodo.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Traceback (most recent call last):
File "file-test-data/extras/testTodo.py", line 3, in <module>
todo()
File "code/wypp/writeYourProgram.py", line 327, in todo
File "code/wypp/writeYourProgram.py", line 328, in todo
raise errors.TodoError(msg)

TODO
Binary file added screenshot.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed screenshot.png
Binary file not shown.
Binary file added screenshot2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed screenshot2.png
Binary file not shown.
29 changes: 21 additions & 8 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>(keyOverride) ?? {};
if (overrides.reportWildcardImportFromLibrary !== 'none') {
Expand All @@ -244,16 +246,27 @@ async function fixPylanceConfig(
);
}

// extraPaths
const keyExtraPaths = 'analysis.extraPaths';
const extra = cfg.get<string[]>(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<string>(keyMode) ?? '';
if (mode !== 'off') {
await cfg.update(
'analysis.typeCheckingMode',
'off',
target
);
}
}

class Location implements vscode.TerminalLink {
Expand Down Expand Up @@ -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);
Expand All @@ -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 :
Expand Down