diff --git a/python/code/wypp/instrument.py b/python/code/wypp/instrument.py index 2b7a8c0..0658f12 100644 --- a/python/code/wypp/instrument.py +++ b/python/code/wypp/instrument.py @@ -140,10 +140,11 @@ def find_spec( target: types.ModuleType | None = None, ) -> ModuleSpec | None: - debug(f'Consulting InstrumentingFinder.find_spec for fullname={fullname}') + debug(f'Consulting InstrumentingFinder.find_spec for fullname={fullname}, path={path}, target={target}') # 1) The fullname is the name of the main module. This might be a dotted name such as x.y.z.py # so we have special logic here fp = os.path.join(self.modDir, f"{fullname}.py") + debug(f'fullPath: {fp}') if self.mainModName == fullname and os.path.isfile(fp): loader = InstrumentingLoader(fullname, fp) spec = spec_from_file_location(fullname, fp, loader=loader) diff --git a/python/code/wypp/runCode.py b/python/code/wypp/runCode.py index 69ad622..80056f7 100644 --- a/python/code/wypp/runCode.py +++ b/python/code/wypp/runCode.py @@ -3,6 +3,7 @@ import importlib import runpy from dataclasses import dataclass +from typing import Optional # local imports from constants import * @@ -33,24 +34,29 @@ def __init__(self, mod, properlyImported): @dataclass class RunSetup: - def __init__(self, pathDir: str, args: list[str]): + def __init__(self, pathDir: str, args: Optional[list[str]] = None, installProfile: bool = True): self.pathDir = os.path.abspath(pathDir) self.args = args self.sysPathInserted = False self.oldArgs = sys.argv + self.installProfile = installProfile def __enter__(self): if self.pathDir not in sys.path: sys.path.insert(0, self.pathDir) self.sysPathInserted = True - sys.argv = self.args - self.originalProfile = sys.getprofile() - stacktrace.installProfileHook() + if self.args is not None: + sys.argv = self.args + if self.installProfile: + self.originalProfile = sys.getprofile() + stacktrace.installProfileHook() def __exit__(self, exc_type, value, traceback): - sys.setprofile(self.originalProfile) + if self.installProfile: + sys.setprofile(self.originalProfile) if self.sysPathInserted: sys.path.remove(self.pathDir) self.sysPathInserted = False - sys.argv = self.oldArgs + if self.args is not None: + sys.argv = self.oldArgs def prepareLib(onlyCheckRunnable, enableTypeChecking): libDefs = None @@ -108,6 +114,7 @@ def runTestsInFile(testFile, globals, libDefs, doTypecheck=True, extraDirs=[]): printStderr() printStderr(f"Running tutor's tests in {testFile}") libDefs.resetTestCount() + runCode(testFile, globals, doTypecheck=doTypecheck, extraDirs=extraDirs) try: runCode(testFile, globals, doTypecheck=doTypecheck, extraDirs=extraDirs) except: @@ -123,7 +130,9 @@ def performChecks(check, testFile, globals, libDefs, doTypecheck=True, extraDirs if check: testResultsInstr = {'total': 0, 'failing': 0} if testFile: - testResultsInstr = runTestsInFile(testFile, globals, libDefs, doTypecheck=doTypecheck, - extraDirs=extraDirs) + testDir = os.path.dirname(testFile) + with RunSetup(testDir): + testResultsInstr = runTestsInFile(testFile, globals, libDefs, doTypecheck=doTypecheck, + extraDirs=extraDirs) failingSum = testResultsStudent['failing'] + testResultsInstr['failing'] utils.die(0 if failingSum < 1 else 1) diff --git a/python/code/wypp/typecheck.py b/python/code/wypp/typecheck.py index 9cb90bf..e4c2f2a 100644 --- a/python/code/wypp/typecheck.py +++ b/python/code/wypp/typecheck.py @@ -83,6 +83,33 @@ def checkArgument(p: inspect.Parameter, name: str, idx: Optional[int], a: Any, locArg: Optional[location.Loc], info: location.CallableInfo, cfg: CheckCfg): t = p.annotation if not isEmptyAnnotation(t): + if p.kind == inspect.Parameter.VAR_POSITIONAL: + argT = None + # For *args annotated as tuple[X, ...], extract the element type X + origin = getattr(t, '__origin__', None) + if origin is tuple: + args = getattr(t, '__args__', None) + if args: + argT = args[0] + elif t is tuple: + # bare `tuple` without type parameters, nothing to check + return + else: + raise ValueError(f'Invalid type for rest argument: {t}') + t = argT + elif p.kind == inspect.Parameter.VAR_KEYWORD: + valT = None + # For **kwargs annotated as dict[str, X], extract the value type X + origin = getattr(t, '__origin__', None) + if origin is dict: + type_args = getattr(t, '__args__', None) + if type_args and len(type_args) >= 2: + valT = type_args[1] + elif t is dict: + return + else: + raise ValueError(f'Invalid type for keyword argument: {t}') + t = valT locDecl = info.getParamSourceLocation(name) if not handleMatchesTyResult(matchesTy(a, t, cfg.ns), locDecl): cn = location.CallableName.mk(info) @@ -109,20 +136,42 @@ def raiseArgMismatch(): len(paramNames) - offset, mandatory - offset, len(args) - offset) + # Classify parameters by kind + varPositionalParam: Optional[inspect.Parameter] = None + varKeywordParam: Optional[inspect.Parameter] = None + positionalNames: list[str] = [] + for pName in paramNames: + p = sig.parameters[pName] + if p.kind == inspect.Parameter.VAR_POSITIONAL: + varPositionalParam = p + elif p.kind == inspect.Parameter.VAR_KEYWORD: + varKeywordParam = p + elif p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD): + positionalNames.append(pName) if len(args) + len(kwargs) < mandatory: raiseArgMismatch() + # Check positional args for i in range(len(args)): - if i >= len(paramNames): - raiseArgMismatch() - name = paramNames[i] - p = sig.parameters[name] locArg = None if fi is None else location.locationOfArgument(fi, i) - checkArgument(p, name, i - offset, args[i], locArg, info, cfg) + if i < len(positionalNames): + name = positionalNames[i] + p = sig.parameters[name] + checkArgument(p, name, i - offset, args[i], locArg, info, cfg) + elif varPositionalParam is not None: + checkArgument(varPositionalParam, varPositionalParam.name, i - offset, args[i], locArg, info, cfg) + else: + raiseArgMismatch() + # Check keyword args for name in kwargs: - if name not in sig.parameters: - raise errors.WyppTypeError.unknownKeywordArgument(cn, callLoc, name) locArg = None if fi is None else location.locationOfArgument(fi, name) - checkArgument(sig.parameters[name], name, None, kwargs[name], locArg, info, cfg) + if name in sig.parameters and sig.parameters[name].kind not in ( + inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD + ): + checkArgument(sig.parameters[name], name, None, kwargs[name], locArg, info, cfg) + elif varKeywordParam is not None: + checkArgument(varKeywordParam, name, None, kwargs[name], locArg, info, cfg) + else: + raise errors.WyppTypeError.unknownKeywordArgument(cn, callLoc, name) def checkReturn(sig: inspect.Signature, returnFrame: Optional[inspect.FrameInfo], result: Any, info: location.CallableInfo, cfg: CheckCfg) -> None: diff --git a/python/file-test-data/extras/args.err b/python/file-test-data/extras/args.err new file mode 100644 index 0000000..01bfb1a --- /dev/null +++ b/python/file-test-data/extras/args.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/args.py", line 9, in + f(1, 2, '3', 4) + +WyppTypeError: '3' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als drittes Argument. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args.py +## Fehlerhafter Aufruf in Zeile 9: + +f(1, 2, '3', 4) + +## Typ deklariert in Zeile 3: + +def f(x: int, *rest: tuple[int,...]): diff --git a/python/file-test-data/extras/args.out b/python/file-test-data/extras/args.out new file mode 100644 index 0000000..afda666 --- /dev/null +++ b/python/file-test-data/extras/args.out @@ -0,0 +1,3 @@ +x=1, rest=() +x=1, rest=(2,) +x=1, rest=(2, 3) diff --git a/python/file-test-data/extras/args.py b/python/file-test-data/extras/args.py new file mode 100644 index 0000000..3330f08 --- /dev/null +++ b/python/file-test-data/extras/args.py @@ -0,0 +1,9 @@ +from wypp import * + +def f(x: int, *rest: tuple[int,...]): + print(f'x={x}, rest={rest}') + +f(1) +f(1, 2) +f(1, 2, 3) +f(1, 2, '3', 4) diff --git a/python/file-test-data/extras/args2.err b/python/file-test-data/extras/args2.err new file mode 100644 index 0000000..979ffa2 --- /dev/null +++ b/python/file-test-data/extras/args2.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/args2.py", line 9, in + f(1, *[2, '3', 4]) + +WyppTypeError: '3' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als drittes Argument. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args2.py +## Fehlerhafter Aufruf in Zeile 9: + +f(1, *[2, '3', 4]) + +## Typ deklariert in Zeile 3: + +def f(x: int, *rest: tuple[int,...]): diff --git a/python/file-test-data/extras/args2.out b/python/file-test-data/extras/args2.out new file mode 100644 index 0000000..afda666 --- /dev/null +++ b/python/file-test-data/extras/args2.out @@ -0,0 +1,3 @@ +x=1, rest=() +x=1, rest=(2,) +x=1, rest=(2, 3) diff --git a/python/file-test-data/extras/args2.py b/python/file-test-data/extras/args2.py new file mode 100644 index 0000000..e84bbd3 --- /dev/null +++ b/python/file-test-data/extras/args2.py @@ -0,0 +1,9 @@ +from wypp import * + +def f(x: int, *rest: tuple[int,...]): + print(f'x={x}, rest={rest}') + +f(1) +f(1, 2) +f(1, 2, 3) +f(1, *[2, '3', 4]) diff --git a/python/file-test-data/extras/args2_ok.err b/python/file-test-data/extras/args2_ok.err new file mode 100644 index 0000000..e69de29 diff --git a/python/file-test-data/extras/args2_ok.out b/python/file-test-data/extras/args2_ok.out new file mode 100644 index 0000000..169de80 --- /dev/null +++ b/python/file-test-data/extras/args2_ok.out @@ -0,0 +1,4 @@ +x=1, kw={} +x=1, kw={'y': 2} +x=1, kw={'y': 2, 'z': 3} +x=1, kw={'y': 2, 'z': 3} diff --git a/python/file-test-data/extras/args2_ok.py b/python/file-test-data/extras/args2_ok.py new file mode 100644 index 0000000..4505857 --- /dev/null +++ b/python/file-test-data/extras/args2_ok.py @@ -0,0 +1,9 @@ +from wypp import * + +def f(x: int, **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, y=2) +f(1, y=2, z=3) +f(1, **{'y': 2, 'z': 3}) diff --git a/python/file-test-data/extras/args3.err b/python/file-test-data/extras/args3.err new file mode 100644 index 0000000..87e4ec9 --- /dev/null +++ b/python/file-test-data/extras/args3.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/args3.py", line 8, in + f(1, y=2, z='3') + +WyppTypeError: '3' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als Argument `z`. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args3.py +## Fehlerhafter Aufruf in Zeile 8: + +f(1, y=2, z='3') diff --git a/python/file-test-data/extras/args3.out b/python/file-test-data/extras/args3.out new file mode 100644 index 0000000..f93e21b --- /dev/null +++ b/python/file-test-data/extras/args3.out @@ -0,0 +1,2 @@ +x=1, kw={} +x=1, kw={'y': 2} diff --git a/python/file-test-data/extras/args3.py b/python/file-test-data/extras/args3.py new file mode 100644 index 0000000..97f635e --- /dev/null +++ b/python/file-test-data/extras/args3.py @@ -0,0 +1,8 @@ +from wypp import * + +def f(x: int, **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, y=2) +f(1, y=2, z='3') diff --git a/python/file-test-data/extras/args3_ok.err b/python/file-test-data/extras/args3_ok.err new file mode 100644 index 0000000..e69de29 diff --git a/python/file-test-data/extras/args3_ok.out b/python/file-test-data/extras/args3_ok.out new file mode 100644 index 0000000..9cdadd6 --- /dev/null +++ b/python/file-test-data/extras/args3_ok.out @@ -0,0 +1,3 @@ +x=1, kw={} +x=1, kw={'y': 2, 'z': 3} +x=1, kw={'y': 2, 'z': 3} diff --git a/python/file-test-data/extras/args3_ok.py b/python/file-test-data/extras/args3_ok.py new file mode 100644 index 0000000..eb3ccfd --- /dev/null +++ b/python/file-test-data/extras/args3_ok.py @@ -0,0 +1,8 @@ +from wypp import * + +def f(x: int, *rest: tuple[int], **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, 10, 11, y=2, z=3) +f(1, *[10, 11], **{'y': 2, 'z': 3}) diff --git a/python/file-test-data/extras/args4.err b/python/file-test-data/extras/args4.err new file mode 100644 index 0000000..11bf336 --- /dev/null +++ b/python/file-test-data/extras/args4.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/args4.py", line 9, in + f(1, **{'y': '2', 'z': 3}) + +WyppTypeError: '2' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als Argument `y`. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args4.py +## Fehlerhafter Aufruf in Zeile 9: + +f(1, **{'y': '2', 'z': 3}) diff --git a/python/file-test-data/extras/args4.out b/python/file-test-data/extras/args4.out new file mode 100644 index 0000000..b69dbab --- /dev/null +++ b/python/file-test-data/extras/args4.out @@ -0,0 +1,3 @@ +x=1, kw={} +x=1, kw={'y': 2} +x=1, kw={'y': 2, 'z': 3} diff --git a/python/file-test-data/extras/args4.py b/python/file-test-data/extras/args4.py new file mode 100644 index 0000000..a32eb7e --- /dev/null +++ b/python/file-test-data/extras/args4.py @@ -0,0 +1,9 @@ +from wypp import * + +def f(x: int, **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, y=2) +f(1, y=2, z=3) +f(1, **{'y': '2', 'z': 3}) diff --git a/python/file-test-data/extras/args4_ok.err b/python/file-test-data/extras/args4_ok.err new file mode 100644 index 0000000..e69de29 diff --git a/python/file-test-data/extras/args4_ok.out b/python/file-test-data/extras/args4_ok.out new file mode 100644 index 0000000..de954bc --- /dev/null +++ b/python/file-test-data/extras/args4_ok.out @@ -0,0 +1,3 @@ +x=1, kw={} +x=1, kw={'y': 2, 'z': '3'} +x=1, kw={'y': '2', 'z': 3} diff --git a/python/file-test-data/extras/args4_ok.py b/python/file-test-data/extras/args4_ok.py new file mode 100644 index 0000000..c9be162 --- /dev/null +++ b/python/file-test-data/extras/args4_ok.py @@ -0,0 +1,8 @@ +from wypp import * + +def f(x: int, *rest: tuple, **kw: dict): + print(f'x={x}, kw={kw}') + +f(1) +f(1, 10, '11', y=2, z='3') +f(1, *[10, '11'], **{'y': '2', 'z': 3}) diff --git a/python/file-test-data/extras/args5.err b/python/file-test-data/extras/args5.err new file mode 100644 index 0000000..588e348 --- /dev/null +++ b/python/file-test-data/extras/args5.err @@ -0,0 +1,17 @@ +Traceback (most recent call last): + File "file-test-data/extras/args5.py", line 7, in + f(1, 10, '11', y=2, z=3) + +WyppTypeError: '11' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als drittes Argument. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args5.py +## Fehlerhafter Aufruf in Zeile 7: + +f(1, 10, '11', y=2, z=3) + +## Typ deklariert in Zeile 3: + +def f(x: int, *rest: tuple[int], **kw: dict[str, int]): diff --git a/python/file-test-data/extras/args5.out b/python/file-test-data/extras/args5.out new file mode 100644 index 0000000..c29dede --- /dev/null +++ b/python/file-test-data/extras/args5.out @@ -0,0 +1 @@ +x=1, kw={} diff --git a/python/file-test-data/extras/args5.py b/python/file-test-data/extras/args5.py new file mode 100644 index 0000000..70aa57a --- /dev/null +++ b/python/file-test-data/extras/args5.py @@ -0,0 +1,7 @@ +from wypp import * + +def f(x: int, *rest: tuple[int], **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, 10, '11', y=2, z=3) diff --git a/python/file-test-data/extras/args6.err b/python/file-test-data/extras/args6.err new file mode 100644 index 0000000..4abd155 --- /dev/null +++ b/python/file-test-data/extras/args6.err @@ -0,0 +1,13 @@ +Traceback (most recent call last): + File "file-test-data/extras/args6.py", line 7, in + f(1, *[10, 11], **{'y': 2, 'z': '3'}) + +WyppTypeError: '3' + +Der Aufruf der Funktion `f` erwartet einen Wert vom Typ `int` als Argument `z`. +Aber der übergebene Wert hat den Typ `str`. + +## Datei file-test-data/extras/args6.py +## Fehlerhafter Aufruf in Zeile 7: + +f(1, *[10, 11], **{'y': 2, 'z': '3'}) diff --git a/python/file-test-data/extras/args6.out b/python/file-test-data/extras/args6.out new file mode 100644 index 0000000..c29dede --- /dev/null +++ b/python/file-test-data/extras/args6.out @@ -0,0 +1 @@ +x=1, kw={} diff --git a/python/file-test-data/extras/args6.py b/python/file-test-data/extras/args6.py new file mode 100644 index 0000000..f05c7a3 --- /dev/null +++ b/python/file-test-data/extras/args6.py @@ -0,0 +1,7 @@ +from wypp import * + +def f(x: int, *rest: tuple[int], **kw: dict[str, int]): + print(f'x={x}, kw={kw}') + +f(1) +f(1, *[10, 11], **{'y': 2, 'z': '3'}) diff --git a/python/file-test-data/extras/args_ok.err b/python/file-test-data/extras/args_ok.err new file mode 100644 index 0000000..e69de29 diff --git a/python/file-test-data/extras/args_ok.out b/python/file-test-data/extras/args_ok.out new file mode 100644 index 0000000..a0a126d --- /dev/null +++ b/python/file-test-data/extras/args_ok.out @@ -0,0 +1,4 @@ +x=1, rest=() +x=1, rest=(2,) +x=1, rest=(2, 3) +x=1, rest=(2, 3, 4) diff --git a/python/file-test-data/extras/args_ok.py b/python/file-test-data/extras/args_ok.py new file mode 100644 index 0000000..8617ec1 --- /dev/null +++ b/python/file-test-data/extras/args_ok.py @@ -0,0 +1,9 @@ +from wypp import * + +def f(x: int, *rest: tuple[int,...]): + print(f'x={x}, rest={rest}') + +f(1) +f(1, 2) +f(1, 2, 3) +f(1, *[2,3,4])