From f9990f8a1bb40591e771ccd199b3edfe8e263127 Mon Sep 17 00:00:00 2001 From: Stefan Wehr Date: Tue, 17 Mar 2026 10:49:51 +0100 Subject: [PATCH 1/4] export error classes --- python/code/wypp/__init__.py | 3 +++ python/code/wypp/writeYourProgram.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/python/code/wypp/__init__.py b/python/code/wypp/__init__.py index 4074769..024dd33 100644 --- a/python/code/wypp/__init__.py +++ b/python/code/wypp/__init__.py @@ -74,3 +74,6 @@ def record(cls=None, mutable=False, globals={}, locals={}): resetTestCount = w.resetTestCount deepEq = w.deepEq wrapTypecheck = w.wrapTypecheck +WyppTypeError = w.WyppTypeError +WyppAttributeError = w.WyppAttributeError +WyppError = w.WyppError diff --git a/python/code/wypp/writeYourProgram.py b/python/code/wypp/writeYourProgram.py index 197f269..e617799 100644 --- a/python/code/wypp/writeYourProgram.py +++ b/python/code/wypp/writeYourProgram.py @@ -324,3 +324,7 @@ def impossible(msg=None): math = moduleMath wrapTypecheck = typecheck.wrapTypecheck + +WyppTypeError = errors.WyppTypeError +WyppAttributeError = errors.WyppAttributeError +WyppError = errors.WyppError From 557a6f1fd9dc54e4d5478d23f13ce9b8c8ea62dc Mon Sep 17 00:00:00 2001 From: Stefan Wehr Date: Tue, 17 Mar 2026 10:50:53 +0100 Subject: [PATCH 2/4] fix replTester --- python/code/wypp/ansi.py | 6 ++++++ python/code/wypp/errors.py | 5 ----- python/code/wypp/replTester.py | 28 +++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/python/code/wypp/ansi.py b/python/code/wypp/ansi.py index b260837..8a49e9c 100644 --- a/python/code/wypp/ansi.py +++ b/python/code/wypp/ansi.py @@ -1,3 +1,5 @@ +import re + RESET = "\u001b[0;0m" BOLD = "\u001b[1m" REVERSE = "\u001b[2m" @@ -30,3 +32,7 @@ def red(s): def blue(s): return color(s, BLUE + BOLD) + +_ANSI_ESCAPE_RE = re.compile(r'\u001b\[[0-9;]*m') +def stripAnsi(s: str) -> str: + return _ANSI_ESCAPE_RE.sub('', s) diff --git a/python/code/wypp/errors.py b/python/code/wypp/errors.py index 332488c..b8b4996 100644 --- a/python/code/wypp/errors.py +++ b/python/code/wypp/errors.py @@ -57,7 +57,6 @@ class WyppTypeError(TypeError, WyppError): def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): WyppError.__init__(self, extraFrames) self.msg = msg - self.add_note(msg) def __str__(self): return f'WyppTypeError: {self.msg}' @@ -290,7 +289,6 @@ class WyppAttributeError(AttributeError, WyppError): def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): WyppError.__init__(self, extraFrames) self.msg = msg - self.add_note(msg) @staticmethod def unknownAttr(clsName: str, attrName: str) -> WyppAttributeError: @@ -301,12 +299,9 @@ class TodoError(Exception, WyppError): def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): WyppError.__init__(self, extraFrames) self.msg = msg - self.add_note(msg) - class ImpossibleError(Exception, WyppError): def __init__(self, msg: str, extraFrames: list[inspect.FrameInfo] = []): WyppError.__init__(self, extraFrames) self.msg = msg - self.add_note(msg) diff --git a/python/code/wypp/replTester.py b/python/code/wypp/replTester.py index a56b009..6f94813 100644 --- a/python/code/wypp/replTester.py +++ b/python/code/wypp/replTester.py @@ -1,5 +1,7 @@ import sys import doctest +from contextlib import contextmanager +import ansi from myLogging import * # We use our own DocTestParser to replace exception names in stacktraces @@ -8,7 +10,9 @@ def rewriteLines(lines: list[str]): """ Each line has exactly one of the following four kinds: - - COMMENT: if it starts with '#' (leading whitespace stripped) + - COMMENT: if it starts with '#' but not with '##' (leading whitespace stripped) + Rationale: lines starting with '##' are error messages from wypp, lines starting only with + '#' are real comments in the file defining the doctest. - PROMPT: if it starts with '>>>' (leading whitespace stripped) - EMPTY: if it contains only whitespace - OUTPUT: otherwise @@ -22,7 +26,7 @@ def get_line_kind(line: str) -> str: stripped = line.lstrip() if not stripped: return 'EMPTY' - elif stripped.startswith('#'): + elif stripped.startswith('#') and not stripped.startswith('##'): return 'COMMENT' elif stripped.startswith('>>>'): return 'PROMPT' @@ -56,7 +60,6 @@ def find_next_non_empty(idx: int) -> tuple[int, str]: if prev_kind in ['PROMPT', 'OUTPUT'] and next_kind == 'OUTPUT': lines[i] = '' - class MyDocTestParser(doctest.DocTestParser): def get_examples(self, string, name=''): """ @@ -76,10 +79,25 @@ def get_examples(self, string, name=''): x = super().get_examples(string, name) return x +class MyOutputChecker(doctest.OutputChecker): + def check_output(self, want, got, optionflags): + got = ansi.stripAnsi(got) + return super().check_output(want, got, optionflags) + +@contextmanager +def patchOutputChecker(): + _Original = doctest.OutputChecker + doctest.OutputChecker = MyOutputChecker + try: + yield + finally: + doctest.OutputChecker = _Original + def testRepl(repl: str, defs: dict) -> tuple[int, int]: doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS - (failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False, - optionflags=doctestOptions, parser=MyDocTestParser()) + with patchOutputChecker(): + (failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False, + optionflags=doctestOptions, parser=MyDocTestParser()) if failures == 0: if tests == 0: print(f'No tests in {repl}') From f4b36c76c44fe732e237b7f540a6976a4dfbb38b Mon Sep 17 00:00:00 2001 From: Stefan Wehr Date: Tue, 17 Mar 2026 10:51:08 +0100 Subject: [PATCH 3/4] fix return tracker for multiple threads --- python/code/wypp/stacktrace.py | 11 ++++++++++- python/code/wypp/typecheck.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/python/code/wypp/stacktrace.py b/python/code/wypp/stacktrace.py index fcec06e..231c3ed 100644 --- a/python/code/wypp/stacktrace.py +++ b/python/code/wypp/stacktrace.py @@ -2,10 +2,12 @@ import traceback import utils import inspect +import threading from typing import Optional, Any import os import sys from collections import deque +from myLogging import * def tbToFrameList(tb: types.TracebackType) -> list[types.FrameType]: cur = tb @@ -130,5 +132,12 @@ def getReturnTracker(): obj = sys.getprofile() if isinstance(obj, ReturnTracker): return obj + elif obj is None: + if threading.current_thread() is threading.main_thread(): + raise ValueError(f'No ReturnTracker set, must use installProfileHook before') + else: + debug('ReturnTracker not available in threads') + return None else: - raise ValueError(f'No ReturnTracker set, must use installProfileHook before') + debug(f'Profiling set to some custom profiler: {obj}') + return None diff --git a/python/code/wypp/typecheck.py b/python/code/wypp/typecheck.py index b1431d3..c1261fa 100644 --- a/python/code/wypp/typecheck.py +++ b/python/code/wypp/typecheck.py @@ -253,7 +253,7 @@ def wrapped(*args, **kwargs) -> T: utils._call_with_frames_removed(checkArguments, sig, args, kwargs, info, checkCfg) returnTracker = stacktrace.getReturnTracker() result = utils._call_with_next_frame_removed(f, *args, **kwargs) - ft = returnTracker.getReturnFrameType(0) + ft = returnTracker.getReturnFrameType(0) if returnTracker else None utils._call_with_frames_removed( checkReturn, sig, ft, result, info, checkCfg ) From 21305617e2ba85a62c41d0f0c3f63a5fefeca1b3 Mon Sep 17 00:00:00 2001 From: Stefan Wehr Date: Tue, 17 Mar 2026 10:59:33 +0100 Subject: [PATCH 4/4] allow running without main file --- python/code/wypp/runner.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python/code/wypp/runner.py b/python/code/wypp/runner.py index f42b3a4..41a9433 100644 --- a/python/code/wypp/runner.py +++ b/python/code/wypp/runner.py @@ -65,7 +65,14 @@ def main(globals, argList=None): printStderr(f'Unsupported language {args.lang}. Supported: ' + ', '.join(i18n.allLanguages)) sys.exit(1) - fileToRun: str = args.file + isInteractive = args.interactive + version = versionMod.readVersion() + fileToRun: str|None = args.file + if fileToRun is None: + if args.repls: + import replTester + replTester.testRepls(args.repls, globals) + return if not os.path.exists(fileToRun): printStderr(f'File {fileToRun} does not exist') sys.exit(1) @@ -75,11 +82,6 @@ def main(globals, argList=None): fileToRun = os.path.basename(fileToRun) debug(f'Changed directory to {d}, fileToRun={fileToRun}') - isInteractive = args.interactive - version = versionMod.readVersion() - - if fileToRun is None: - return if not args.checkRunnable and (not args.quiet or args.verbose): printWelcomeString(fileToRun, version, doTypecheck=args.checkTypes)