Skip to content

Commit dc9ee31

Browse files
committed
fix bug with filenames of stdlib modules
1 parent c663f34 commit dc9ee31

9 files changed

Lines changed: 105 additions & 10 deletions

File tree

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/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)
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')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File file-test-data/basics/typing.py does not exist

python/fileTests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22
from fileTestsLib import *
33
import sys
4+
import os
45

56
def pythonMinVersion(major: int, minor: int) -> bool:
67
return sys.version_info >= (major, minor)
@@ -33,7 +34,15 @@ def main():
3334

3435
globalCtx.results.finish()
3536

37+
def extraChecks():
38+
# The localTyping file does not exist, running should thus fail
39+
localTyping = 'file-test-data/basics/typing.py'
40+
if os.path.exists(localTyping):
41+
raise ValueError(f'File {localTyping} should not exist')
42+
check(localTyping, exitCode=1)
43+
3644
try:
3745
main()
46+
extraChecks()
3847
except KeyboardInterrupt:
3948
pass

python/fileTestsLib.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
import re
1313
import fnmatch
1414

15+
DEBUG = False
16+
17+
def debug(s):
18+
if DEBUG:
19+
print(f'[TESTDEBUG] {s}')
20+
1521
GLOBAL_CHECK_OUTPUTS = True
1622

1723
GLOBAL_RECORD_ALL = False # Should be False, write actual output to all expected output files
@@ -34,6 +40,7 @@ def parseArgs() -> TestOpts:
3440
)
3541

3642
# Define the command-line arguments
43+
parser.add_argument("--debug", action="store_true", help="Output debug messages")
3744
parser.add_argument("--start-at", type=str, help="Start with test in FILE")
3845
parser.add_argument("--only", type=str, help="Run only the test in FILE")
3946
parser.add_argument("--continue", action="store_true",
@@ -48,7 +55,9 @@ def parseArgs() -> TestOpts:
4855
nargs='*')
4956
# Parse the arguments
5057
args = parser.parse_args()
51-
58+
if args.debug:
59+
global DEBUG
60+
DEBUG = True
5261
scriptDir = os.path.dirname(__file__)
5362
return TestOpts(
5463
cmd=f'{scriptDir}/code/wypp/runYourProgram.py',
@@ -244,6 +253,7 @@ def _runTest(testFile: str,
244253
env = os.environ.copy()
245254
env['PYTHONPATH'] = os.pathsep.join([os.path.join(ctx.opts.baseDir, 'code')] + pythonPath)
246255
env['WYPP_UNDER_TEST'] = 'True'
256+
debug(' '.join(cmd))
247257
with open(actualStdoutFile, 'w') as stdoutFile, \
248258
open(actualStderrFile, 'w') as stderrFile:
249259
# Run the command
@@ -332,17 +342,20 @@ class WyppTestConfig:
332342
typecheck: Literal[True, False, "both"]
333343
args: list[str]
334344
pythonPath: Optional[str]
345+
exitCode: Optional[int]
335346
@staticmethod
336347
def default() -> WyppTestConfig:
337-
return WyppTestConfig(typecheck=True, args=[], pythonPath=None)
348+
return WyppTestConfig(typecheck=True, args=[], pythonPath=None, exitCode=None)
338349

339350
def readWyppTestConfig(path: str, *, max_lines: int = 5) -> WyppTestConfig:
340351
"""
341352
Read a line like `# WYPP_TEST_CONFIG: {"typecheck": false}` from the first
342353
`max_lines` lines of the file at `path` and return it as a dict.
343354
Returns {} if not present.
344355
"""
345-
validKeys = ['typecheck', 'args', 'pythonPath']
356+
validKeys = ['typecheck', 'args', 'pythonPath', 'exitCode']
357+
if not os.path.exists(path):
358+
return WyppTestConfig.default()
346359
with open(path, "r", encoding="utf-8") as f:
347360
for lineno in range(1, max_lines + 1):
348361
line = f.readline()
@@ -358,7 +371,10 @@ def readWyppTestConfig(path: str, *, max_lines: int = 5) -> WyppTestConfig:
358371
typecheck = j.get('typecheck', True)
359372
args = j.get('args', [])
360373
pythonPath = j.get('pythonPath')
361-
return WyppTestConfig(typecheck=typecheck, args=args, pythonPath=pythonPath)
374+
exitCode = j.get('exitCode')
375+
cfg = WyppTestConfig(typecheck=typecheck, args=args, pythonPath=pythonPath, exitCode=exitCode)
376+
debug(f'Config for {path}: {cfg}')
377+
return cfg
362378
return WyppTestConfig.default()
363379

364380
def checkNoConfig(testFile: str,
@@ -370,8 +386,6 @@ def checkNoConfig(testFile: str,
370386
checkOutputs: bool = True,
371387
ctx: TestContext = globalCtx,
372388
what: str = ''):
373-
if guessExitCode(testFile) == 0:
374-
exitCode = 0
375389
status = _check(testFile, exitCode, typecheck, args, pythonPath, minVersion, checkOutputs, ctx, what)
376390
ctx.results.storeTestResult(testFile, status)
377391
if status == 'failed':
@@ -396,6 +410,10 @@ def check(testFile: str,
396410
pythonPath = []
397411
if cfg.pythonPath:
398412
pythonPath = cfg.pythonPath.split(':')
413+
if cfg.exitCode is not None:
414+
exitCode = cfg.exitCode
415+
elif guessExitCode(testFile) == 0:
416+
exitCode = 0
399417
if cfg.typecheck == 'both':
400418
checkNoConfig(testFile, exitCode, typecheck=True, args=args,
401419
pythonPath=pythonPath, minVersion=minVersion, checkOutputs=checkOutputs,

0 commit comments

Comments
 (0)