Skip to content

Commit 968827d

Browse files
authored
Merge pull request #185 from skogsbaer/sw/fixes-2025-10-13
fixes 2025-10-13
2 parents 55bb1cd + 69e0bd0 commit 968827d

File tree

5 files changed

+71
-22
lines changed

5 files changed

+71
-22
lines changed

.github/workflows/github-action-test-python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
# You need to change to branch protection rules if you change the versions here
15-
python-version: [3.12.11, 3.13.7]
15+
python-version: [3.12.11, 3.13.7, 3.14.0]
1616
steps:
1717
- uses: actions/checkout@v4
1818
- name: Set up Python ${{ matrix.python-version }}

python/allTests

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ function run()
1717

1818
PYENV_VERSION=3.12 run
1919
PYENV_VERSION=3.13 run
20+
PYENV_VERSION=3.14 run

python/code/wypp/instrument.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import importlib
55
import importlib.abc
66
from importlib.machinery import ModuleSpec, SourceFileLoader
7-
from importlib.util import decode_source
7+
import importlib.machinery
8+
from importlib.util import decode_source, spec_from_file_location
89
from collections.abc import Buffer
910
import types
1011
from os import PathLike
@@ -126,8 +127,9 @@ def source_to_code(
126127
return code
127128

128129
class InstrumentingFinder(importlib.abc.MetaPathFinder):
129-
def __init__(self, finder, modDir: str, extraDirs: list[str]):
130+
def __init__(self, finder, modDir: str, modName: str, extraDirs: list[str]):
130131
self._origFinder = finder
132+
self.mainModName = modName
131133
self.modDir = os.path.realpath(modDir) + '/'
132134
self.extraDirs = [os.path.realpath(d) for d in extraDirs]
133135

@@ -137,9 +139,27 @@ def find_spec(
137139
path: Sequence[str] | None,
138140
target: types.ModuleType | None = None,
139141
) -> ModuleSpec | None:
142+
143+
# 1) The fullname is the name of the main module. This might be a dotted name such as x.y.z.py
144+
# so we have special logic here
145+
fp = os.path.join(self.modDir, f"{fullname}.py")
146+
if self.mainModName == fullname and os.path.isfile(fp):
147+
loader = InstrumentingLoader(fullname, fp)
148+
return spec_from_file_location(fullname, fp, loader=loader)
149+
# 2) The fullname is a prefix of the main module. We want to load main modules with
150+
# dotted names such as x.y.z.py, hence we synthesize a namespace pkg
151+
# e.g. if 'x.y.z.py' exists and we're asked for 'x', return a package spec.
152+
elif self.mainModName.startswith(fullname + '.'):
153+
spec = importlib.machinery.ModuleSpec(fullname, loader=None, is_package=True)
154+
# Namespace package marker (PEP 451)
155+
spec.submodule_search_locations = []
156+
return spec
157+
# 3) Fallback: use the original PathFinder
140158
spec = self._origFinder.find_spec(fullname, path, target)
159+
debug(f'spec for {fullname}: {spec}')
141160
if spec is None:
142-
return None
161+
return spec
162+
143163
origin = os.path.realpath(spec.origin)
144164
dirs = [self.modDir] + self.extraDirs
145165
isLocalModule = False
@@ -153,7 +173,7 @@ def find_spec(
153173
return spec
154174

155175
@contextmanager
156-
def setupFinder(modDir: str, extraDirs: list[str], typechecking: bool):
176+
def setupFinder(modDir: str, modName: str, extraDirs: list[str], typechecking: bool):
157177
if not typechecking:
158178
yield
159179
else:
@@ -169,7 +189,7 @@ def setupFinder(modDir: str, extraDirs: list[str], typechecking: bool):
169189
raise RuntimeError("Cannot find a PathFinder in sys.meta_path")
170190

171191
# Create and install our custom finder
172-
instrumenting_finder = InstrumentingFinder(finder, modDir, extraDirs)
192+
instrumenting_finder = InstrumentingFinder(finder, modDir, modName, extraDirs)
173193
sys.meta_path.insert(0, instrumenting_finder)
174194

175195
try:

python/code/wypp/runCode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ def prepareLib(onlyCheckRunnable, enableTypeChecking):
6767
def runCode(fileToRun, globals, doTypecheck=True, extraDirs=None) -> dict:
6868
if not extraDirs:
6969
extraDirs = []
70-
with instrument.setupFinder(os.path.dirname(fileToRun), extraDirs, doTypecheck):
71-
modName = os.path.basename(os.path.splitext(fileToRun)[0])
70+
modName = os.path.basename(os.path.splitext(fileToRun)[0])
71+
with instrument.setupFinder(os.path.dirname(fileToRun), modName, extraDirs, doTypecheck):
7272
sys.dont_write_bytecode = True
7373
res = runpy.run_module(modName, init_globals=globals, run_name='__wypp__', alter_sys=False)
7474
return res

pytrace-generator/main.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
import types
1515
import typing
1616

17+
_DEBUG = False
18+
19+
def debug(s):
20+
if _DEBUG:
21+
eprint(s)
22+
1723
def eprint(*args, **kwargs):
1824
print(*args, file=sys.stderr, **kwargs)
1925

@@ -330,6 +336,36 @@ def push_frame(self):
330336
def pop_frame(self):
331337
self.frames.pop()
332338

339+
Skip = typing.Literal['SKIP', 'DONT_SKIP']
340+
341+
class Skipper:
342+
def __init__(self):
343+
# skipStartFile is a tuple (filename, counter) or None
344+
# If None, then we are not in skipping mode
345+
# Otherwise, we are in skipping mode. Each call event with the
346+
# same filename increases the counter by 1. Each return event
347+
# with the same filename decreases the counter by 0.
348+
# The counter starts at 0 when entering skipping event. If it
349+
# goes back to zero, we leave skipping mode
350+
self.skipStartFile: tuple[str, int] | None = None
351+
352+
def handleEvent(self, event: str, filename: str, isExternalMod: bool) -> Skip:
353+
if event == 'call':
354+
if self.skipStartFile is None and isExternalMod:
355+
# start skipping
356+
self.skipStartFile = (filename, 1)
357+
elif self.skipStartFile is not None and filename == self.skipStartFile[0]:
358+
self.skipStartFile = (self.skipStartFile[0], self.skipStartFile[1] + 1)
359+
elif event == 'return':
360+
if self.skipStartFile is not None and filename == self.skipStartFile[0]:
361+
self.skipStartFile = (self.skipStartFile[0], self.skipStartFile[1] - 1)
362+
if self.skipStartFile[1] == 0:
363+
self.skipStartFile = None
364+
return 'SKIP'
365+
if self.skipStartFile is None:
366+
return 'DONT_SKIP'
367+
else:
368+
return 'SKIP'
333369

334370
class PyTraceGenerator(bdb.Bdb):
335371
def __init__(self, trace_socket):
@@ -339,7 +375,7 @@ def __init__(self, trace_socket):
339375
self.stack_ignore = []
340376
self.init = False
341377
self.filename = ""
342-
self.skip_until = None
378+
self.skipper = Skipper()
343379
self.import_following = False
344380
self.last_step_was_class = False
345381
self.prev_num_frames = 0
@@ -348,22 +384,14 @@ def __init__(self, trace_socket):
348384
self.captured_stdout = io.StringIO()
349385
self.last_event = ""
350386

351-
def trace_dispatch(self, frame, event, arg):
387+
def trace_dispatch(self, frame, event: str, arg):
352388
filename = frame.f_code.co_filename
353389

354-
# Skip built-in modules
355-
# This might not be the best solution. Adjust if required.
356-
skip = False
357-
if self.skip_until is not None:
358-
skip = filename != self.skip_until
359-
elif not filename.startswith(os.path.dirname(self.filename)):
360-
skip = True
361-
if frame.f_back:
362-
self.skip_until = frame.f_back.f_code.co_filename
363-
if skip:
390+
skipRes = self.skipper.handleEvent(
391+
event, filename, not filename.startswith(os.path.dirname(self.filename))
392+
)
393+
if skipRes == 'SKIP':
364394
return self.trace_dispatch
365-
else:
366-
self.skip_until = None
367395

368396
line = frame.f_lineno
369397
if not self.init:

0 commit comments

Comments
 (0)