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/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}') 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) 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 ) 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