Skip to content
Open
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
8 changes: 6 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.9
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: "3.9"
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
4 changes: 3 additions & 1 deletion opto/trace/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def bundle(
Returns:
FunModule: The wrapped function that returns node objects.
"""
prev_f_locals = inspect.stack()[1].frame.f_locals
# Python 3.13+ returns a live FrameLocalsProxy here rather than a dict (PEP 667);
# snapshot it so the captured locals stay a plain dict on every version.
prev_f_locals = dict(inspect.stack()[1].frame.f_locals)

def decorator(fun):
fun_module = FunModule(
Expand Down
20 changes: 19 additions & 1 deletion tests/unit_tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,22 @@ def modify_global_list():
print("Running tests with trainable=False")
run(trainable=False)
print("Running tests with trainable=True")
run(trainable=True)
run(trainable=True)

def test_caller_locals_are_captured_as_dict():
# Regression test for #43. bundle() captures the calling frame's locals so a
# trainable operator can be re-executed with them. On Python 3.13+ frame.f_locals
# is a live FrameLocalsProxy rather than a dict (PEP 667), which used to trip the
# isinstance check in FunModule and made bundle unusable on 3.13.
nonlocal_marker = node("This is a nonlocal marker")

@trace.bundle()
def uses_marker(x):
return x

assert isinstance(uses_marker._ldict, dict)
assert uses_marker._ldict["nonlocal_marker"] is nonlocal_marker


print("Running caller-locals capture test")
test_caller_locals_are_captured_as_dict()
Loading