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
15 changes: 6 additions & 9 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
# Write Your Python Program - CHANGELOG

* unreleased
* RUN button: a program that is still running (and all processes it started)
is terminated before the new run starts. Restarting now also works with
pending input in the interpreter, a program in an endless loop or a program
waiting for `input()`
* RUN button: disables AI features (GitHub Copilot) in the workspace settings
(`chat.disableAIFeatures`)
* Requires VS Code 1.104 or newer
* 2.5.0 (2026-10-14)
* More robust termination of running interpreter when hitting RUN
* Disable AI features on RUN (`chat.disableAIFeatures`)
* Visualization fixes
* Require VS Code 1.104 or newer
* 2.4.0 (2026-09-28)
* Greatly improved visualizatioin
* Python 3.14: forward references in records and functions work without
`from __future__ import annotations`
* Support for Python 3.15
* Fix welcome message
* Fix welcome message
* 2.3.0 (2026-04-03)
* Use relative imports in the whole code base
* 2.2.2 (2026-04-03)
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.4.0",
"version": "2.5.0",
"publisher": "StefanWehr",
"icon": "icon.png",
"engines": {
Expand Down
4 changes: 4 additions & 0 deletions python/code/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,8 +933,12 @@ def check_type_internal(
stacklevel=get_stacklevel(),
)
return
# The forward reference might refer to a type alias
annotation = resolve_alias_chains(annotation)
if isinstance(annotation, str):
annotation = resolve_annotation_str(annotation, memo.globals, memo.locals)
# The string might refer to a type alias
annotation = resolve_alias_chains(annotation)

if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock):
return
Expand Down
7 changes: 4 additions & 3 deletions python/code/wypp/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class WyppError(abc.ABC):
def __init__(self, extraFrames: list[inspect.FrameInfo] = []):
self.extraFrames = extraFrames[:]

def renderLoc(loc: location.Loc) -> str:
s = '\n'.join([l.highlight() for l in location.highlightedLines(loc)])
def renderLoc(loc: location.Loc, withContext: bool = True) -> str:
s = '\n'.join([l.highlight() for l in location.highlightedLines(loc, withContext)])
return s.rstrip()

def renderStr(s: str, quote: str):
Expand All @@ -21,7 +21,8 @@ def renderStr(s: str, quote: str):

def renderGiven(givenValue: Any, givenLoc: Optional[location.Loc]) -> str:
if type(givenValue) == str and givenLoc is not None:
code = renderLoc(givenLoc)
# Only the code at the location itself determines the quotes
code = renderLoc(givenLoc, withContext=False)
single = renderStr(givenValue, "'")
double = renderStr(givenValue, '"')
if double in code and single not in code:
Expand Down
18 changes: 15 additions & 3 deletions python/code/wypp/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class Loc:
startCol: Optional[int]
endLine: Optional[int]
endCol: Optional[int]
# Range of lines (inclusive) shown around the location when rendering it. For example,
# the location of a record attribute is shown together with the whole record.
contextLines: Optional[tuple[int, int]] = None

def __post_init__(self):
self.filename = paths.canonicalizePath(self.filename)
Expand Down Expand Up @@ -156,11 +159,11 @@ def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv') -> str
else:
return self.line.decoded()

def highlightedLines(loc: Loc) -> list[SourceLine]:
def highlightedLines(loc: Loc, withContext: bool = True) -> list[SourceLine]:
match loc.fullSpan():
case None:
line = getline(loc.filename, loc.startLine)
return [SourceLine(line, None)]
result = [SourceLine(line, None)]
case (startLine, startCol, endLine, endCol):
result = []
for lineNo in range(startLine, startLine+1):
Expand All @@ -169,6 +172,14 @@ def highlightedLines(loc: Loc) -> list[SourceLine]:
c1 = startCol if lineNo == startLine else leadingSpaces
c2 = endCol if lineNo == endLine else len(line)
result.append(SourceLine(line, (c1, c2)))
match loc.contextLines:
case (first, last) if withContext:
def plain(lineNos: range) -> list[SourceLine]:
return [SourceLine(getline(loc.filename, n), None) for n in lineNos]
before = plain(range(first, loc.startLine))
after = plain(range(loc.startLine + len(result), last + 1))
return before + result + after
case _:
return result

@dataclass
Expand Down Expand Up @@ -324,7 +335,8 @@ def getParamSourceLocation(self, paramName: str) -> Optional[Loc]:
ast = parsecache.getAST(file)
node = ast.getRecordAttr(self.name, paramName)
if node:
return Loc(file, node.lineno, node.col_offset, node.end_lineno, node.end_col_offset)
return Loc(file, node.lineno, node.col_offset, node.end_lineno, node.end_col_offset,
contextLines=ast.getRecordLineRange(self.name))
else:
return None
@property
Expand Down
12 changes: 11 additions & 1 deletion python/code/wypp/myTypeguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ class MatchesTyFailure:

type MatchesTyResult = bool | MatchesTyFailure

def matchesTy(a: Any, ty: Any, ns: Namespaces) -> MatchesTyResult:
def matchesTy(a: Any, ty: Any, ns: Namespaces,
ignoreUnresolvedForwardRefs: bool = False) -> MatchesTyResult:
# Since python 3.14, annotations may contain ForwardRef objects (see utils.getAnnotations).
# If such a ForwardRef cannot be resolved (e.g. because the type is defined after its
# first use), we want an error, not just a warning. For checks performed at definition time,
# the type might legitimately be defined later, so these checks can ignore such ForwardRefs.
if ignoreUnresolvedForwardRefs:
policy = typeguard.ForwardRefPolicy.IGNORE
else:
policy = typeguard.ForwardRefPolicy.ERROR
try:
typeguard.check_type(a,
ty,
forward_ref_policy=policy,
collection_check_strategy=typeguard.CollectionCheckStrategy.ALL_ITEMS,
ns=(ns.globals, ns.locals))
return True
Expand Down
17 changes: 17 additions & 0 deletions python/code/wypp/parsecache.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ def getRecordAttr(self, clsName: str, attrName: str) -> ast.AnnAssign | None:
self.__cache[key] = annNode
return annNode

def getRecordLineRange(self, clsName: str) -> tuple[int, int] | None:
"""
Returns the range of lines (inclusive) of the toplevel class clsName, starting with
its first decorator and ending with its last attribute of the format `A: T` or
`A: T = ...`. Returns None if no such class or multiple classes are found.
"""
self.__ensureTree()
targetCls = self._findClass(clsName)
if not targetCls:
return None
firstLine = min([targetCls.lineno] + [d.lineno for d in targetCls.decorator_list])
lastLine = targetCls.lineno
for stmt in targetCls.body:
if isinstance(stmt, ast.AnnAssign) and stmt.end_lineno is not None:
lastLine = max(lastLine, stmt.end_lineno)
return (firstLine, lastLine)

def getMethodDef(self, clsName: str, m: FunMatcher) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
"""
Finds the method in class clsName that matches m. None is returned if no such method
Expand Down
12 changes: 8 additions & 4 deletions python/code/wypp/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def checkSignature(sig: inspect.Signature, info: location.CallableInfo, cfg: Che
raise errors.WyppTypeError.partialAnnotationError(location.CallableName.mk(info), name, locDecl)
if p.default is not inspect.Parameter.empty:
locDecl = lambda: info.getParamSourceLocation(name)
if not handleMatchesTyResult(matchesTy(p.default, ty, cfg.ns), locDecl):
# The type of the default value might refer to a type defined later
res = matchesTy(p.default, ty, cfg.ns, ignoreUnresolvedForwardRefs=True)
if not handleMatchesTyResult(res, locDecl):
raise errors.WyppTypeError.defaultError(location.CallableName.mk(info), name,
locDecl(), ty, p.default)

Expand Down Expand Up @@ -147,10 +149,11 @@ def checkArguments(sig: inspect.Signature, args: tuple, kwargs: dict,
def getCallLoc() -> Optional[location.Loc]:
fi = stacktrace.callerOutsideWypp()
return None if not fi else location.Loc.fromFrameInfo(fi)
# idxOrName is the index of the argument in the call (without self) or the keyword
def getLocArg(idxOrName: int | str) -> Callable[[], Optional[location.Loc]]:
def f():
fi = stacktrace.callerOutsideWypp()
return None if fi is None else location.locationOfArgument(fi, i)
return None if fi is None else location.locationOfArgument(fi, idxOrName)
return f
def raiseArgMismatch():
raise errors.WyppTypeError.argCountMismatch(cn,
Expand All @@ -177,9 +180,10 @@ def raiseArgMismatch():
if i < len(positionalNames):
name = positionalNames[i]
p = sig.parameters[name]
checkArgument(p, name, i - offset, args[i], getLocArg(i), info, cfg)
checkArgument(p, name, i - offset, args[i], getLocArg(i - offset), info, cfg)
elif varPositionalParam is not None:
checkArgument(varPositionalParam, varPositionalParam.name, i - offset, args[i], getLocArg(i), info, cfg)
checkArgument(varPositionalParam, varPositionalParam.name, i - offset, args[i],
getLocArg(i - offset), info, cfg)
else:
raiseArgMismatch()
# Check keyword args
Expand Down
17 changes: 17 additions & 0 deletions python/file-test-data/basics/argLocation1.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation1.py", line 6, in <module>
x = C().m(1, 'zwei', 3)

WyppTypeError: 'zwei'

Der Aufruf der Methode `m` der Klasse `C` erwartet einen Wert vom Typ `int` als zweites Argument.
Aber der übergebene Wert hat den Typ `str`.

## Datei file-test-data/basics/argLocation1.py
## Fehlerhafter Aufruf in Zeile 6:

x = C().m(1, 'zwei', 3)

## Typ deklariert in Zeile 2:

def m(self, a: int, b: int, c: int) -> int:
17 changes: 17 additions & 0 deletions python/file-test-data/basics/argLocation1.err_en
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation1.py", line 6, in <module>
x = C().m(1, 'zwei', 3)

WyppTypeError: 'zwei'

The call of method `m` of class `C` expects value of type `int` as 2nd argument.
But the value given has type `str`.

## File file-test-data/basics/argLocation1.py
## Problematic call in line 6:

x = C().m(1, 'zwei', 3)

## Type declared in line 2:

def m(self, a: int, b: int, c: int) -> int:
6 changes: 6 additions & 0 deletions python/file-test-data/basics/argLocation1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class C:
def m(self, a: int, b: int, c: int) -> int:
return a + b + c

# The second argument must be highlighted
x = C().m(1, 'zwei', 3)
21 changes: 21 additions & 0 deletions python/file-test-data/basics/argLocation2.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation2.py", line 10, in <module>
p = Point(1, z=3, y='zwei')

WyppTypeError: 'zwei'

Der Aufruf des Konstruktors des Records `Point` erwartet einen Wert vom Typ `int` als Argument `y`.
Aber der übergebene Wert hat den Typ `str`.

## Datei file-test-data/basics/argLocation2.py
## Fehlerhafter Aufruf in Zeile 10:

p = Point(1, z=3, y='zwei')

## Typ deklariert in Zeile 6:

@record
class Point:
x: int
y: int
z: int
21 changes: 21 additions & 0 deletions python/file-test-data/basics/argLocation2.err_en
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation2.py", line 10, in <module>
p = Point(1, z=3, y='zwei')

WyppTypeError: 'zwei'

The call of the constructor of record `Point` expects value of type `int` as argument `y`.
But the value given has type `str`.

## File file-test-data/basics/argLocation2.py
## Problematic call in line 10:

p = Point(1, z=3, y='zwei')

## Type declared in line 6:

@record
class Point:
x: int
y: int
z: int
10 changes: 10 additions & 0 deletions python/file-test-data/basics/argLocation2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from wypp import *

@record
class Point:
x: int
y: int
z: int

# The keyword argument y must be highlighted
p = Point(1, z=3, y='zwei')
17 changes: 17 additions & 0 deletions python/file-test-data/basics/argLocation3.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation3.py", line 5, in <module>
z = foo(y=2, x='eins')

WyppTypeError: 'eins'

Der Aufruf der Funktion `foo` erwartet einen Wert vom Typ `int` als Argument `x`.
Aber der übergebene Wert hat den Typ `str`.

## Datei file-test-data/basics/argLocation3.py
## Fehlerhafter Aufruf in Zeile 5:

z = foo(y=2, x='eins')

## Typ deklariert in Zeile 1:

def foo(x: int, y: int) -> int:
17 changes: 17 additions & 0 deletions python/file-test-data/basics/argLocation3.err_en
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/basics/argLocation3.py", line 5, in <module>
z = foo(y=2, x='eins')

WyppTypeError: 'eins'

The call of function `foo` expects value of type `int` as argument `x`.
But the value given has type `str`.

## File file-test-data/basics/argLocation3.py
## Problematic call in line 5:

z = foo(y=2, x='eins')

## Type declared in line 1:

def foo(x: int, y: int) -> int:
5 changes: 5 additions & 0 deletions python/file-test-data/basics/argLocation3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def foo(x: int, y: int) -> int:
return x + y

# Only keyword arguments, the keyword argument x must be highlighted
z = foo(y=2, x='eins')
2 changes: 1 addition & 1 deletion python/file-test-data/basics/constructor.err
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Aber der übergebene Wert hat den Typ `str`.
## Datei file-test-data/basics/constructor.py
## Fehlerhafter Aufruf in Zeile 5:

c = C("1")
c = C("1")

## Typ deklariert in Zeile 2:

Expand Down
2 changes: 1 addition & 1 deletion python/file-test-data/basics/constructor.err_en
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ But the value given has type `str`.
## File file-test-data/basics/constructor.py
## Problematic call in line 5:

c = C("1")
c = C("1")

## Type declared in line 2:

Expand Down
13 changes: 13 additions & 0 deletions python/file-test-data/basics/forwardRefs2.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Traceback (most recent call last):
File "file-test-data/basics/forwardRefs2.py", line 11, in <module>
paula = Papagei('mittel', "Let's go to the punkrock show")

WyppTypeError: ungültiger Typ `Gewichtsklasse`

## Datei file-test-data/basics/forwardRefs2.py
## Typ deklariert in Zeile 8:

@record
class Papagei:
gewichtsklasse: Gewichtsklasse
satz: str
13 changes: 13 additions & 0 deletions python/file-test-data/basics/forwardRefs2.err_en
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Traceback (most recent call last):
File "file-test-data/basics/forwardRefs2.py", line 11, in <module>
paula = Papagei('mittel', "Let's go to the punkrock show")

WyppTypeError: invalid type `Gewichtsklasse`

## File file-test-data/basics/forwardRefs2.py
## Type declared in line 8:

@record
class Papagei:
gewichtsklasse: Gewichtsklasse
satz: str
Loading
Loading