diff --git a/ChangeLog.md b/ChangeLog.md index 9c4466e5..268caa03 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,8 +1,11 @@ # Write Your Python Program - CHANGELOG +* 2.0.8 (2025-10-16) + * Fix vscode warning for literals + * Fix highlighting for files with umlauts * 2.0.7 (2025-10-14) * Fix #184 (filenames with dots) - * Support python 3.13 + * Support python 3.14 * 2.0.6 (2025-10-08) * Settings for language #180 * Only warn of settings cannot be saved #182 diff --git a/package.json b/package.json index bb6fe6f9..31c7707b 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.7", + "version": "2.0.8", "publisher": "StefanWehr", "icon": "icon.png", "engines": { diff --git a/python/code/wypp/__init__.py b/python/code/wypp/__init__.py index a25b4adc..40747695 100644 --- a/python/code/wypp/__init__.py +++ b/python/code/wypp/__init__.py @@ -5,21 +5,13 @@ import typing -# Exported names that are available for star imports (in alphabetic order) -Any = w.Any -Callable = w.Callable -Generator = w.Generator -Iterable = w.Iterable -Iterator = w.Iterator -Literal = w.Literal -Mapping = w.Mapping -Optional = w.Optional -Sequence = w.Sequence -Protocol = w.Protocol -Union = w.Union +# Exported names that are available for star imports (mostly in alphabetic order) +from typing import Any, Callable, Generator, Iterable, Iterator, Literal, Mapping, Optional, \ + Protocol, Sequence, Union +from dataclasses import dataclass + check = w.check checkFail = w.checkFail -dataclass = w.dataclass floatNegative = w.floatNegative floatNonNegative = w.floatNonNegative floatNonPositive = w.floatNonPositive diff --git a/python/code/wypp/drawingLib.py b/python/code/wypp/drawingLib.py index 8b55560c..edc940b0 100644 --- a/python/code/wypp/drawingLib.py +++ b/python/code/wypp/drawingLib.py @@ -1,6 +1,8 @@ import time import threading import writeYourProgram as _w +from typing import Literal, Sequence + # Do not import tkinter at the top-level. Someone with no installation of tkinter should # be able to user WYPP without drawing support. @@ -14,9 +16,9 @@ class Point: x: float y: float -ShapeKind = _w.Literal['ellipsis', 'rectangle'] +type ShapeKind = Literal['ellipsis', 'rectangle'] -Color = _w.Literal['red', 'green', 'blue', 'yellow', 'black', 'white'] +type Color = Literal['red', 'green', 'blue', 'yellow', 'black', 'white'] @_w.record class FixedShape: @@ -58,7 +60,7 @@ def _drawCoordinateSystem(canvas, windowSize: Size): canvas.create_line(x, 0, x, windowSize.height, dash=(4,2)) canvas.create_line(0, y, windowSize.width, y, dash=(4,2)) -def drawFixedShapes(shapes: _w.Sequence[FixedShape], +def drawFixedShapes(shapes: Sequence[FixedShape], withCoordinateSystem=False, stopAfter=None) -> None: try: diff --git a/python/code/wypp/location.py b/python/code/wypp/location.py index 12de0a0e..f3722011 100644 --- a/python/code/wypp/location.py +++ b/python/code/wypp/location.py @@ -13,6 +13,66 @@ import parsecache from parsecache import FunMatcher import paths +import tokenize +import os + +@dataclass +class EncodedBytes: + bytes: bytes + encoding: str + def __len__(self): + return len(self.bytes) + def countLeadingSpaces(self) -> int: + return len(self.bytes) - len(self.bytes.lstrip()) + def decoded(self) -> str: + return self.bytes.decode(self.encoding, errors='replace') + @overload + def __getitem__(self, key: int) -> int: ... + @overload + def __getitem__(self, key: slice) -> str: ... + def __getitem__(self, key: int | slice) -> int | str: + if isinstance(key, int): + return self.bytes[key] + else: + b = self.bytes[key] + return b.decode(self.encoding, errors='replace') + +@dataclass +class EncodedByteLines: + bytes: list[bytes] + encoding: str + +_cache: dict[str, EncodedByteLines] = {} +def getline(filename, lineno): + """ + Returns a line of some source file as a bytearray. We use byte arrays because + location offsets are byte offsets. + """ + p = os.path.normpath(os.path.abspath(filename)) + if p in _cache: + lines = _cache[p] + else: + try: + with open(filename, 'rb') as f: + byteLines = f.readlines() + except Exception: + byteLines = [] + i = 0 + def nextLine() -> bytes: + nonlocal i + if i < len(byteLines): + x = byteLines[i] + i = i + 1 + return x + else: + return b'' + encoding, _ = tokenize.detect_encoding(nextLine) + lines = EncodedByteLines(byteLines, encoding) + if 1 <= lineno <= len(lines.bytes): + x = lines.bytes[lineno - 1].rstrip(b'\n') + else: + x = b'' + return EncodedBytes(x, encoding) @dataclass class Loc: @@ -38,7 +98,7 @@ def code(self) -> Optional[str]: case (startLine, startCol, endLine, endCol): result = [] for lineNo in range(startLine, startLine+1): - line = linecache.getline(self.filename, lineNo).rstrip("\n") + line = getline(self.filename, lineNo) c1 = startCol if lineNo == startLine else 0 c2 = endCol if lineNo == endLine else len(line) result.append(line[c1:c2]) @@ -84,27 +144,27 @@ def highlight(s: str, mode: HighlightMode) -> str: @dataclass class SourceLine: - line: str # without trailing \n + line: EncodedBytes # without trailing \n span: Optional[tuple[int, int]] # (inclusive, exclusive) - def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv'): + def highlight(self, mode: HighlightMode | Literal['fromEnv'] = 'fromEnv') -> str: mode = getHighlightMode(mode) if self.span: l = self.line return l[:self.span[0]] + highlight(l[self.span[0]:self.span[1]], mode) + l[self.span[1]:] else: - return self.line + return self.line.decoded() def highlightedLines(loc: Loc) -> list[SourceLine]: match loc.fullSpan(): case None: - line = linecache.getline(loc.filename, loc.startLine).rstrip("\n") + line = getline(loc.filename, loc.startLine) return [SourceLine(line, None)] case (startLine, startCol, endLine, endCol): result = [] for lineNo in range(startLine, startLine+1): - line = linecache.getline(loc.filename, lineNo).rstrip("\n") - leadingSpaces = len(line) - len(line.lstrip()) + line = getline(loc.filename, lineNo) + leadingSpaces = line.countLeadingSpaces() c1 = startCol if lineNo == startLine else leadingSpaces c2 = endCol if lineNo == endLine else len(line) result.append(SourceLine(line, (c1, c2))) diff --git a/python/code/wypp/writeYourProgram.py b/python/code/wypp/writeYourProgram.py index a04442db..197f269d 100644 --- a/python/code/wypp/writeYourProgram.py +++ b/python/code/wypp/writeYourProgram.py @@ -1,4 +1,5 @@ import typing +from typing import Any import dataclasses import inspect import errors @@ -16,23 +17,6 @@ def _debug(s): if _DEBUG: print('[DEBUG] ' + s) -# Types -Any = typing.Any -Optional = typing.Optional -Union = typing.Union -Literal = typing.Literal -Iterable = typing.Iterable -Iterator = typing.Iterator -Sequence = typing.Sequence -Generator = typing.Generator -ForwardRef = typing.ForwardRef -Protocol = typing.Protocol - -Mapping = typing.Mapping - -Callable = typing.Callable - -dataclass = dataclasses.dataclass record = records.record intPositive = typing.Annotated[int, lambda i: i > 0, 'intPositive'] @@ -46,7 +30,7 @@ def _debug(s): floatNegative = typing.Annotated[float, lambda x: x < 0, 'floatNegative'] floatNonPositive = typing.Annotated[float, lambda x: x <= 0, 'floatNonPositive'] -class Lock(Protocol): +class Lock(typing.Protocol): def acquire(self, blocking: bool = True, timeout:int = -1) -> Any: pass def release(self) -> Any: @@ -54,7 +38,7 @@ def release(self) -> Any: def locked(self) -> Any: pass -LockFactory = typing.Annotated[Callable[[], Lock], 'LockFactory'] +LockFactory = typing.Annotated[typing.Callable[[], Lock], 'LockFactory'] T = typing.TypeVar('T') U = typing.TypeVar('U') diff --git a/python/file-test-data/basics/13.10.VL_ok.err b/python/file-test-data/basics/13.10.VL_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/13.10.VL_ok.out b/python/file-test-data/basics/13.10.VL_ok.out new file mode 100644 index 00000000..e965047a --- /dev/null +++ b/python/file-test-data/basics/13.10.VL_ok.out @@ -0,0 +1 @@ +Hello diff --git a/python/file-test-data/basics/13.10.VL_ok.py b/python/file-test-data/basics/13.10.VL_ok.py new file mode 100644 index 00000000..f163aee2 --- /dev/null +++ b/python/file-test-data/basics/13.10.VL_ok.py @@ -0,0 +1,3 @@ +from wypp import * + +print('Hello') diff --git a/python/file-test-data/basics/umlaute.err b/python/file-test-data/basics/umlaute.err new file mode 100644 index 00000000..7f1f3fd6 --- /dev/null +++ b/python/file-test-data/basics/umlaute.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/basics/umlaute.py", line 6, in + check(äüßö("1"), 1) + +WyppTypeError: "1" + +Der Aufruf der Funktion `äüßö` erwartet einen Wert vom Typ `int` als erstes Argument. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/basics/umlaute.py +## Fehlerhafter Aufruf in Zeile 6: + +check(äüßö("1"), 1) + +## Typ deklariert in Zeile 3: + +def äüßö(x: int) -> int: diff --git a/python/file-test-data/basics/umlaute.out b/python/file-test-data/basics/umlaute.out new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/umlaute.py b/python/file-test-data/basics/umlaute.py new file mode 100644 index 00000000..e686b306 --- /dev/null +++ b/python/file-test-data/basics/umlaute.py @@ -0,0 +1,6 @@ +from wypp import * + +def äüßö(x: int) -> int: + return x + 1 + +check(äüßö("1"), 1) diff --git a/python/file-test-data/basics/x.y_ok.err b/python/file-test-data/basics/x.y_ok.err new file mode 100644 index 00000000..e69de29b diff --git a/python/file-test-data/basics/x.y_ok.out b/python/file-test-data/basics/x.y_ok.out new file mode 100644 index 00000000..e965047a --- /dev/null +++ b/python/file-test-data/basics/x.y_ok.out @@ -0,0 +1 @@ +Hello diff --git a/python/file-test-data/basics/x.y_ok.py b/python/file-test-data/basics/x.y_ok.py new file mode 100644 index 00000000..f163aee2 --- /dev/null +++ b/python/file-test-data/basics/x.y_ok.py @@ -0,0 +1,3 @@ +from wypp import * + +print('Hello') diff --git a/python/file-test-data/extras/testImpossible.err b/python/file-test-data/extras/testImpossible.err index 95961a19..c35e73bb 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 334, in impossible + File "code/wypp/writeYourProgram.py", line 318, 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 d4bcb7f7..459d5648 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 328, in todo + File "code/wypp/writeYourProgram.py", line 312, in todo raise errors.TodoError(msg) TODO diff --git a/python/tests/test_misc.py b/python/tests/test_misc.py index bb4ee77f..9334883f 100644 --- a/python/tests/test_misc.py +++ b/python/tests/test_misc.py @@ -1,5 +1,6 @@ import unittest from writeYourProgram import * +from typing import * setDieOnCheckFailures(True) diff --git a/python/tests/test_record.py b/python/tests/test_record.py index d0113e9c..e9fc2229 100644 --- a/python/tests/test_record.py +++ b/python/tests/test_record.py @@ -4,6 +4,7 @@ import traceback import dataclasses import stacktrace +from typing import Literal initModule() diff --git a/src/extension.ts b/src/extension.ts index 6453793c..14963754 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -283,8 +283,9 @@ async function fixPylanceConfig( // extraPaths const keyExtraPaths = 'analysis.extraPaths'; const extra = cfg.get(keyExtraPaths) ?? []; - if (extra.length !== 1 || extra[0] !== libDir) { - await tryUpdate(keyExtraPaths, [libDir]); + if (!extra.includes(libDir)) { + const newExtra = [...extra, libDir]; + await tryUpdate(keyExtraPaths, newExtra); } // typechecking off