Skip to content

Commit 998ad47

Browse files
authored
Merge pull request #195 from skogsbaer/sw/filenames
fix bug with filenames of stdlib modules
2 parents c663f34 + 7e61a4e commit 998ad47

File tree

14 files changed

+179
-12
lines changed

14 files changed

+179
-12
lines changed

python/code/wypp/exceptionHandler.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ def handleCurrentException(exit=True, removeFirstTb=False, file=sys.stderr):
4343
frameList = (stacktrace.tbToFrameList(tb) if tb is not None else [])
4444
if frameList and removeFirstTb:
4545
frameList = frameList[1:]
46-
isBug = not isWyppError and not isinstance(val, SyntaxError) and \
47-
len(frameList) > 0 and stacktrace.isWyppFrame(frameList[-1])
46+
isSyntaxError = isinstance(val, SyntaxError)
47+
lastFrameIsWypp = len(frameList) > 0 and stacktrace.isWyppFrame(frameList[-1])
48+
isBug = not isWyppError and not isSyntaxError and lastFrameIsWypp
4849
stackSummary = stacktrace.limitTraceback(frameList, extra, not isBug and not isDebug())
4950
header = False
5051
for x in stackSummary.format():
@@ -62,6 +63,7 @@ def handleCurrentException(exit=True, removeFirstTb=False, file=sys.stderr):
6263
for x in traceback.format_exception_only(etype, val):
6364
file.write(x)
6465
if isBug:
66+
debug(f'isWyppError={isWyppError}, isSyntaxError={isSyntaxError}, lastFrameIsWypp={lastFrameIsWypp}')
6567
file.write(f'BUG: the error above is most likely a bug in WYPP!')
6668
if exit:
6769
utils.die(1)

python/code/wypp/instrument.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,19 +140,23 @@ def find_spec(
140140
target: types.ModuleType | None = None,
141141
) -> ModuleSpec | None:
142142

143+
debug(f'Consulting InstrumentingFinder.find_spec for fullname={fullname}')
143144
# 1) The fullname is the name of the main module. This might be a dotted name such as x.y.z.py
144145
# so we have special logic here
145146
fp = os.path.join(self.modDir, f"{fullname}.py")
146147
if self.mainModName == fullname and os.path.isfile(fp):
147148
loader = InstrumentingLoader(fullname, fp)
148-
return spec_from_file_location(fullname, fp, loader=loader)
149+
spec = spec_from_file_location(fullname, fp, loader=loader)
150+
debug(f'spec for {fullname}: {spec}')
151+
return spec
149152
# 2) The fullname is a prefix of the main module. We want to load main modules with
150153
# dotted names such as x.y.z.py, hence we synthesize a namespace pkg
151154
# e.g. if 'x.y.z.py' exists and we're asked for 'x', return a package spec.
152155
elif self.mainModName.startswith(fullname + '.'):
153156
spec = importlib.machinery.ModuleSpec(fullname, loader=None, is_package=True)
154157
# Namespace package marker (PEP 451)
155158
spec.submodule_search_locations = []
159+
debug(f'spec for {fullname}: {spec}')
156160
return spec
157161
# 3) Fallback: use the original PathFinder
158162
spec = self._origFinder.find_spec(fullname, path, target)
@@ -191,10 +195,19 @@ def setupFinder(modDir: str, modName: str, extraDirs: list[str], typechecking: b
191195
# Create and install our custom finder
192196
instrumenting_finder = InstrumentingFinder(finder, modDir, modName, extraDirs)
193197
sys.meta_path.insert(0, instrumenting_finder)
198+
debug(f'Installed instrument finder {instrumenting_finder}')
199+
200+
alreadyLoaded = sys.modules.get(modName)
201+
if alreadyLoaded:
202+
sys.modules.pop(modName, None)
203+
importlib.invalidate_caches()
194204

195205
try:
196206
yield
197207
finally:
208+
if alreadyLoaded:
209+
sys.modules[modName] = alreadyLoaded
210+
198211
# Remove our custom finder when exiting the context
199212
if instrumenting_finder in sys.meta_path:
200213
sys.meta_path.remove(instrumenting_finder)

python/code/wypp/location.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ def getResultTypeLocation(self) -> Optional[Loc]:
201201
@abc.abstractmethod
202202
def getParamSourceLocation(self, paramName: str) -> Optional[Loc]:
203203
pass
204+
@property
205+
@abc.abstractmethod
206+
def isAsync(self) -> bool:
207+
pass
204208

205209
class StdCallableInfo(CallableInfo):
206210
"""
@@ -276,6 +280,10 @@ def getParamSourceLocation(self, paramName: str) -> Optional[Loc]:
276280
res.end_lineno,
277281
res.end_col_offset)
278282

283+
@property
284+
def isAsync(self) -> bool:
285+
node = self._findDef()
286+
return isinstance(node, ast.AsyncFunctionDef)
279287

280288
def classFilename(cls) -> str | None:
281289
"""Best-effort path to the file that defined `cls`."""
@@ -315,6 +323,9 @@ def getParamSourceLocation(self, paramName: str) -> Optional[Loc]:
315323
return Loc(file, node.lineno, node.col_offset, node.end_lineno, node.end_col_offset)
316324
else:
317325
return None
326+
@property
327+
def isAsync(self) -> bool:
328+
return False
318329

319330
def locationOfArgument(fi: inspect.FrameInfo, idxOrName: int | str) -> Optional[Loc]:
320331
"""

python/code/wypp/runCode.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, mod, properlyImported):
3434
@dataclass
3535
class RunSetup:
3636
def __init__(self, pathDir: str, args: list[str]):
37-
self.pathDir = pathDir
37+
self.pathDir = os.path.abspath(pathDir)
3838
self.args = args
3939
self.sysPathInserted = False
4040
self.oldArgs = sys.argv
@@ -64,12 +64,30 @@ def prepareLib(onlyCheckRunnable, enableTypeChecking):
6464
quiet=onlyCheckRunnable)
6565
return libDefs
6666

67+
def debugModule(name):
68+
if name in sys.modules:
69+
m = sys.modules["copy"]
70+
print(f"Module {name} already loaded from:", getattr(m, "__file__", None))
71+
print("CWD:", os.getcwd())
72+
print("sys.path[0]:", sys.path[0])
73+
print("First few sys.path entries:")
74+
for p in sys.path[:5]:
75+
print(" ", p)
76+
77+
spec = importlib.util.find_spec(name)
78+
print("Resolved spec:", spec)
79+
if spec:
80+
print("Origin:", spec.origin)
81+
print("Loader:", type(spec.loader).__name__)
82+
6783
def runCode(fileToRun, globals, doTypecheck=True, extraDirs=None) -> dict:
6884
if not extraDirs:
6985
extraDirs = []
7086
modName = os.path.basename(os.path.splitext(fileToRun)[0])
7187
with instrument.setupFinder(os.path.dirname(fileToRun), modName, extraDirs, doTypecheck):
7288
sys.dont_write_bytecode = True
89+
if DEBUG:
90+
debugModule(modName)
7391
res = runpy.run_module(modName, init_globals=globals, run_name='__wypp__', alter_sys=False)
7492
return res
7593

python/code/wypp/runner.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,14 @@ def main(globals, argList=None):
6666
sys.exit(1)
6767

6868
fileToRun: str = args.file
69+
if not os.path.exists(fileToRun):
70+
printStderr(f'File {fileToRun} does not exist')
71+
sys.exit(1)
6972
if args.changeDir:
70-
os.chdir(os.path.dirname(fileToRun))
73+
d = os.path.dirname(fileToRun)
74+
os.chdir(d)
7175
fileToRun = os.path.basename(fileToRun)
76+
debug(f'Changed directory to {d}, fileToRun={fileToRun}')
7277

7378
isInteractive = args.interactive
7479
version = versionMod.readVersion()
@@ -81,13 +86,15 @@ def main(globals, argList=None):
8186

8287
libDefs = runCode.prepareLib(onlyCheckRunnable=args.checkRunnable, enableTypeChecking=args.checkTypes)
8388

84-
with (runCode.RunSetup(os.path.dirname(fileToRun), [fileToRun] + restArgs),
89+
runDir = os.path.dirname(fileToRun)
90+
with (runCode.RunSetup(runDir, [fileToRun] + restArgs),
8591
paths.projectDir(os.path.abspath(os.getcwd()))):
8692
globals['__name__'] = '__wypp__'
8793
sys.modules['__wypp__'] = sys.modules['__main__']
8894
loadingFailed = False
8995
try:
9096
verbose(f'running code in {fileToRun}')
97+
debug(f'sys.path: {sys.path}')
9198
globals['__file__'] = fileToRun
9299
globals = runCode.runStudentCode(fileToRun, globals, args.checkRunnable,
93100
doTypecheck=args.checkTypes, extraDirs=args.extraDirs)

python/code/wypp/typecheck.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def raiseArgMismatch():
126126

127127
def checkReturn(sig: inspect.Signature, returnFrame: Optional[inspect.FrameInfo],
128128
result: Any, info: location.CallableInfo, cfg: CheckCfg) -> None:
129+
if info.isAsync:
130+
return
129131
t = sig.return_annotation
130132
if isEmptyAnnotation(t):
131133
t = None
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
coroutine object
2+
At start of meaningOfLife: a
3+
At end of meaningOfLife: a
4+
42
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import *
2+
import types
3+
import asyncio
4+
from datetime import datetime
5+
6+
def debug(msg):
7+
print(msg)
8+
9+
async def meaningOfLife(x: str) -> int:
10+
debug(f'At start of meaningOfLife: {x}')
11+
await asyncio.sleep(0.0001)
12+
debug(f'At end of meaningOfLife: {x}')
13+
return 42
14+
15+
async def run() -> None:
16+
a = meaningOfLife('a')
17+
debug('coroutine object')
18+
n = await a
19+
debug(n)
20+
21+
def main():
22+
asyncio.run(run())
23+
24+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello copy
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# WYPP_TEST_CONFIG: {"args": ["--change-directory"], "exitCode": 0}
2+
3+
# This file intentionally has the same name as a file from the stdlib
4+
print('Hello copy')

0 commit comments

Comments
 (0)