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
3 changes: 2 additions & 1 deletion python/code/wypp/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 17 additions & 8 deletions python/code/wypp/runCode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import importlib
import runpy
from dataclasses import dataclass
from typing import Optional

# local imports
from constants import *
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
65 changes: 57 additions & 8 deletions python/code/wypp/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions python/file-test-data/extras/args.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/extras/args.py", line 9, in <module>
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,...]):
3 changes: 3 additions & 0 deletions python/file-test-data/extras/args.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
x=1, rest=()
x=1, rest=(2,)
x=1, rest=(2, 3)
9 changes: 9 additions & 0 deletions python/file-test-data/extras/args.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 17 additions & 0 deletions python/file-test-data/extras/args2.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/extras/args2.py", line 9, in <module>
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,...]):
3 changes: 3 additions & 0 deletions python/file-test-data/extras/args2.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
x=1, rest=()
x=1, rest=(2,)
x=1, rest=(2, 3)
9 changes: 9 additions & 0 deletions python/file-test-data/extras/args2.py
Original file line number Diff line number Diff line change
@@ -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])
Empty file.
4 changes: 4 additions & 0 deletions python/file-test-data/extras/args2_ok.out
Original file line number Diff line number Diff line change
@@ -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}
9 changes: 9 additions & 0 deletions python/file-test-data/extras/args2_ok.py
Original file line number Diff line number Diff line change
@@ -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})
13 changes: 13 additions & 0 deletions python/file-test-data/extras/args3.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Traceback (most recent call last):
File "file-test-data/extras/args3.py", line 8, in <module>
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')
2 changes: 2 additions & 0 deletions python/file-test-data/extras/args3.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
x=1, kw={}
x=1, kw={'y': 2}
8 changes: 8 additions & 0 deletions python/file-test-data/extras/args3.py
Original file line number Diff line number Diff line change
@@ -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')
Empty file.
3 changes: 3 additions & 0 deletions python/file-test-data/extras/args3_ok.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
x=1, kw={}
x=1, kw={'y': 2, 'z': 3}
x=1, kw={'y': 2, 'z': 3}
8 changes: 8 additions & 0 deletions python/file-test-data/extras/args3_ok.py
Original file line number Diff line number Diff line change
@@ -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})
13 changes: 13 additions & 0 deletions python/file-test-data/extras/args4.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Traceback (most recent call last):
File "file-test-data/extras/args4.py", line 9, in <module>
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})
3 changes: 3 additions & 0 deletions python/file-test-data/extras/args4.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
x=1, kw={}
x=1, kw={'y': 2}
x=1, kw={'y': 2, 'z': 3}
9 changes: 9 additions & 0 deletions python/file-test-data/extras/args4.py
Original file line number Diff line number Diff line change
@@ -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})
Empty file.
3 changes: 3 additions & 0 deletions python/file-test-data/extras/args4_ok.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
x=1, kw={}
x=1, kw={'y': 2, 'z': '3'}
x=1, kw={'y': '2', 'z': 3}
8 changes: 8 additions & 0 deletions python/file-test-data/extras/args4_ok.py
Original file line number Diff line number Diff line change
@@ -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})
17 changes: 17 additions & 0 deletions python/file-test-data/extras/args5.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Traceback (most recent call last):
File "file-test-data/extras/args5.py", line 7, in <module>
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]):
1 change: 1 addition & 0 deletions python/file-test-data/extras/args5.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x=1, kw={}
7 changes: 7 additions & 0 deletions python/file-test-data/extras/args5.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions python/file-test-data/extras/args6.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Traceback (most recent call last):
File "file-test-data/extras/args6.py", line 7, in <module>
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'})
1 change: 1 addition & 0 deletions python/file-test-data/extras/args6.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x=1, kw={}
7 changes: 7 additions & 0 deletions python/file-test-data/extras/args6.py
Original file line number Diff line number Diff line change
@@ -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'})
Empty file.
4 changes: 4 additions & 0 deletions python/file-test-data/extras/args_ok.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
x=1, rest=()
x=1, rest=(2,)
x=1, rest=(2, 3)
x=1, rest=(2, 3, 4)
9 changes: 9 additions & 0 deletions python/file-test-data/extras/args_ok.py
Original file line number Diff line number Diff line change
@@ -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])
Loading