Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,4 @@ out/
*.vsix

python/VERSION
.claude
14 changes: 7 additions & 7 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Run Extension",
"type": "extensionHost",
Expand All @@ -36,6 +29,13 @@
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Python: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
69 changes: 69 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# CLAUDE.md

Write Your Python Program (WYPP): a beginner-friendly Python environment. Two parts ship together:

- **VS Code extension** (TypeScript, `src/`), which adds a RUN button and a program-flow visualization.
- **`wypp` Python package** (`python/code/wypp/`), the teaching language: records, dynamic type checking of annotations, `check(...)` tests and localized (German/English) error messages. It is also published to PyPI.

## Layout

- `src/extension.ts`: extension entry point. It runs `python/code/wypp/runYourProgram.py` in a terminal, with `python/code` on the path.
- `src/programflow-visualization/`: Python-Tutor-like visualization. See its `README.md` for the architecture.
- `backend/` spawns `pytrace-generator/main.py` to produce traces.
- `graph-model.ts` and `reachability.ts` hold pure logic (unit-tested).
- `web/` is the webview UI (ELK layout), bundled by `scripts/build-web.mjs` with esbuild.
- `python/code/wypp/`: the runtime (`runner.py`, `typecheck.py`, `records.py`, `errors.py`, `i18n.py`, ...).
- `python/code/typeguard/`: vendored copy of typeguard. Always import it through `wypp/myTypeguard.py`, never directly.
- `pytrace-generator/`: standalone tracer for the visualization, with its own tests.
- `elk-task/`, `visualization-plan.md`: design notes for ongoing visualization work.

## Commands

TypeScript (run from the repo root):

```sh
npm install
npm run build # tsc + web bundle -> out/
npm test # compile, typecheck web, eslint, mocha unit tests (out/test/unit)
npm run watch:web # rebuild the webview on change (serve out/programflow-visualization/web)
```

Python (run from `python/`; needs Python 3.12–3.14):

```sh
./allTestsForPyVersion # unit + integration + file tests
./allTestsForPyVersion --unit tests/test_record.py
python3 fileTests.py --only file-test-data/basics/foo.py
python3 fileTests.py --record file-test-data/basics/foo.py [--lang en]
./run somefile.py # run a file with wypp
python3 ../pytrace-generator/test/runTests.py
```

## Definition of done

A change is done when all of the following apply:

1. The tests for every part you touched pass:
- TypeScript (`src/`): `npm test`.
- Python runtime (`python/code/`): `./allTestsForPyVersion` in `python/`.
- Tracer (`pytrace-generator/`): `python3 pytrace-generator/test/runTests.py`.
2. New behavior has a test: a unit test, or a file test in `python/file-test-data/`. A bug fix comes with a test that reproduces the bug.
3. Changed error output has been re-recorded with `--record` in both German and English (`--lang en`), and you have reviewed the diff of the recorded files.
4. Every new user-facing message is wrapped in `tr()` from `i18n.py`, with a German translation added there.
5. You have tried visualization UI changes in a browser (`npm run watch:web`) or in the extension.
6. `ChangeLog.md` has an entry for every user-visible change. `README.md` is updated if documented behavior changed.
7. No stray debug output, scratch files or build artifacts (`out/`, `*.vsix`, `dist/`) are committed.

## Conventions

- **File tests** live in `python/file-test-data/`. Each `foo.py` has expected `foo.out`/`foo.err` files (German) and optional `foo.out_en`/`foo.err_en` files (English). If a change alters error output, re-record with `--record` and review the diff.
- A file test named `*_ok.py` is expected to exit with code 0. Every other file test is expected to exit with code 1.
- A file test that relies on forward references contains the line `# from __future__ import annotations`, commented out. Python 3.14 doesn't need the import. On Python < 3.14, `fileTestsLib.py` runs a temporary copy with the line uncommented.
- CI runs `npm test` and the Python tests on 3.12, 3.13 and 3.14.
- The version lives in `package.json`. The Python package reads it from there (see `python/setup.py` and `wypp/version.py`). Update `ChangeLog.md` when you release.
- `mkdist` packages the `.vsix` (with `vsce`) and the Python distribution. The `*.vsix` files in the root are build artifacts.

## Version control

- Do not create commits unless explicitly ask for.

4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Write Your Python Program - CHANGELOG

* unreleased
* Python 3.14: forward references in records and functions work without
`from __future__ import annotations`

* 2.3.0 (2026-04-03)
* Use relative imports in the whole code base
* 2.2.2 (2026-04-03)
Expand Down
3 changes: 2 additions & 1 deletion python/code/wypp/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def _patchDataClass(cls, mutable: bool, ns: myTypeguard.Namespaces):
fields = set(fieldNames)
info = location.RecordConstructorInfo(cls)
locs = {}
annotations = utils.getAnnotations(cls)
for name in fields:
if not name in cls.__annotations__:
if not name in annotations:
raise errors.WyppTypeError.noTypeAnnotationForRecordAttribute(name, cls.__name__)
else:
locs[name] = info.getParamSourceLocation(name)
Expand Down
2 changes: 1 addition & 1 deletion python/code/wypp/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def wrapTypecheck(cfg: dict | CheckCfg, outerInfo: Optional[location.CallableInf
else:
checkCfg = CheckCfg.fromDict(cfg)
def _wrap(f: Callable[P, T]) -> Callable[P, T]:
sig = inspect.signature(f)
sig = utils.getSignature(f)
if isEmptySignature(sig):
return f
if outerInfo is None:
Expand Down
19 changes: 19 additions & 0 deletions python/code/wypp/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from contextlib import contextmanager
import inspect
import os
import sys
from typing import *
Expand All @@ -20,6 +21,24 @@ def _call_with_next_frame_removed(
) -> T:
return f(*args, **kwargs)

# Starting with python 3.14, annotations are evaluated lazily (PEP 649). Evaluating them
# too early (e.g. when decorating a record whose fields refer to a type defined later)
# raises a NameError. We therefore fetch annotations in FORWARDREF format: names not yet defined
# become ForwardRef objects, which are resolved when the check is performed.
def getSignature(f: Callable) -> inspect.Signature:
if sys.version_info >= (3, 14):
import annotationlib
return inspect.signature(f, annotation_format=annotationlib.Format.FORWARDREF)
else:
return inspect.signature(f)

def getAnnotations(x: Any) -> dict[str, Any]:
if sys.version_info >= (3, 14):
import annotationlib
return annotationlib.get_annotations(x, format=annotationlib.Format.FORWARDREF)
else:
return getattr(x, '__annotations__', {})

def getEnv(name, conv, default):
s = os.getenv(name)
if s is None:
Expand Down
6 changes: 3 additions & 3 deletions python/file-test-data/basics/forwardRefs.err
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Traceback (most recent call last):
File "file-test-data/basics/forwardRefs.py", line 12, in <module>
File "file-test-data/basics/forwardRefs.py", line 13, in <module>
a.foo(a)

WyppTypeError: <__wypp__.A object at 0x00>
Expand All @@ -8,10 +8,10 @@ Der Aufruf der Methode `foo` der Klasse `A` erwartet einen Wert vom Typ `B` als
Aber der übergebene Wert hat den Typ `A`.

## Datei file-test-data/basics/forwardRefs.py
## Fehlerhafter Aufruf in Zeile 12:
## Fehlerhafter Aufruf in Zeile 13:

a.foo(a)

## Typ deklariert in Zeile 4:
## Typ deklariert in Zeile 5:

def foo(self, b: B):
6 changes: 3 additions & 3 deletions python/file-test-data/basics/forwardRefs.err_en
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Traceback (most recent call last):
File "file-test-data/basics/forwardRefs.py", line 12, in <module>
File "file-test-data/basics/forwardRefs.py", line 13, in <module>
a.foo(a)

WyppTypeError: <__wypp__.A object at 0x00>
Expand All @@ -8,10 +8,10 @@ The call of method `foo` of class `A` expects value of type `B` as 1st argument.
But the value given has type `A`.

## File file-test-data/basics/forwardRefs.py
## Problematic call in line 12:
## Problematic call in line 13:

a.foo(a)

## Type declared in line 4:
## Type declared in line 5:

def foo(self, b: B):
3 changes: 2 additions & 1 deletion python/file-test-data/basics/forwardRefs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

class A:
def foo(self, b: B):
Expand Down
3 changes: 2 additions & 1 deletion python/file-test-data/basics/forwardRefs_ok.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

class A:
def foo(self, b: B):
Expand Down
4 changes: 3 additions & 1 deletion python/file-test-data/basics/recursive2_ok.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *

# Ein Flussabschnitt ist entweder
Expand Down
4 changes: 2 additions & 2 deletions python/file-test-data/extras/invalidType.err
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Traceback (most recent call last):
File "file-test-data/extras/invalidType.py", line 9, in <module>
File "file-test-data/extras/invalidType.py", line 11, in <module>
foo()

WyppTypeError: ungültiger Typ `Union(list(int), list[float])`

Wolltest du `Union[list(int), list[float]]` schreiben?

## Datei file-test-data/extras/invalidType.py
## Typ deklariert in Zeile 6:
## Typ deklariert in Zeile 8:

def foo() -> Union(list(int), list[float]):
6 changes: 4 additions & 2 deletions python/file-test-data/extras/invalidType.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *
# See https://github.com/skogsbaer/write-your-python-program/issues/61

# Tests 'return'
# Tests 'return'
def foo() -> Union(list(int), list[float]):
pass

Expand Down
2 changes: 1 addition & 1 deletion python/file-test-data/extras/invalidType2.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Traceback (most recent call last):
File "file-test-data/extras/invalidType2.py", line 5, in <module>
File "file-test-data/extras/invalidType2.py", line 7, in <module>
T = Union(list(int), list[float])
TypeError: 'type' object is not iterable
6 changes: 4 additions & 2 deletions python/file-test-data/extras/invalidType2.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *
# See https://github.com/skogsbaer/write-your-python-program/issues/61

T = Union(list(int), list[float])

# Tests 'return'
# Tests 'return'
def foo() -> T:
pass

Expand Down
4 changes: 2 additions & 2 deletions python/file-test-data/extras/invalidType3.err
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Traceback (most recent call last):
File "file-test-data/extras/invalidType3.py", line 9, in <module>
File "file-test-data/extras/invalidType3.py", line 11, in <module>
foo()

WyppTypeError: ungültiger Typ `Optional(list(int), list[float])`

Wolltest du `Optional[list(int), list[float]]` schreiben?

## Datei file-test-data/extras/invalidType3.py
## Typ deklariert in Zeile 6:
## Typ deklariert in Zeile 8:

def foo() -> Optional(list(int), list[float]):
6 changes: 4 additions & 2 deletions python/file-test-data/extras/invalidType3.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *
# See https://github.com/skogsbaer/write-your-python-program/issues/61

# Tests 'return'
# Tests 'return'
def foo() -> Optional(list(int), list[float]):
pass

Expand Down
4 changes: 2 additions & 2 deletions python/file-test-data/extras/invalidType4.err
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Traceback (most recent call last):
File "file-test-data/extras/invalidType4.py", line 9, in <module>
File "file-test-data/extras/invalidType4.py", line 11, in <module>
foo()

WyppTypeError: ungültiger Typ `Optional[list[int], list[float]]`

## Datei file-test-data/extras/invalidType4.py
## Typ deklariert in Zeile 6:
## Typ deklariert in Zeile 8:

def foo() -> Optional[list[int], list[float]]:
6 changes: 4 additions & 2 deletions python/file-test-data/extras/invalidType4.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *
# See https://github.com/skogsbaer/write-your-python-program/issues/61

# Tests 'return'
# Tests 'return'
def foo() -> Optional[list[int], list[float]]:
pass

Expand Down
2 changes: 1 addition & 1 deletion python/file-test-data/extras/testABCMeta.err
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Traceback (most recent call last):
File "file-test-data/extras/testABCMeta.py", line 28, in <module>
File "file-test-data/extras/testABCMeta.py", line 30, in <module>
Circle(Point(0, 0), 1)
TypeError: Can't instantiate abstract class Circle without an implementation for abstract method 'area'
6 changes: 4 additions & 2 deletions python/file-test-data/extras/testABCMeta.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

# See: https://github.com/skogsbaer/write-your-python-program/issues/74

from wypp import *
Expand All @@ -25,4 +27,4 @@ def move(self, x: float, y: float)->None:
self.x = self.x + x
self.y = self.y + y

Circle(Point(0, 0), 1)
Circle(Point(0, 0), 1)
4 changes: 3 additions & 1 deletion python/file-test-data/extras/testClassHierarchy_ok.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *

class FileSystemEntry:
Expand Down
4 changes: 3 additions & 1 deletion python/file-test-data/extras/testClassRecursion_ok.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *

class C:
Expand Down
4 changes: 2 additions & 2 deletions python/file-test-data/extras/testHintParentheses1.err
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Traceback (most recent call last):
File "file-test-data/extras/testHintParentheses1.py", line 8, in <module>
File "file-test-data/extras/testHintParentheses1.py", line 10, in <module>
check(foo([1,2,3]), 3)

WyppTypeError: ungültiger Typ `list(int)`

Wolltest du `list[int]` schreiben?

## Datei file-test-data/extras/testHintParentheses1.py
## Typ deklariert in Zeile 5:
## Typ deklariert in Zeile 7:

def foo(l: list(int)) -> int:
4 changes: 3 additions & 1 deletion python/file-test-data/extras/testHintParentheses1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
# from __future__ import annotations
# Leave the comment, it's needed for tests with python versions <= 3.13

from wypp import *
# See https://github.com/skogsbaer/write-your-python-program/issues/61

Expand Down
Loading
Loading