Skip to content

Run -c code as the __main__ module - #2080

Merged
Rich Chiodo (rchiodo) merged 1 commit into
microsoft:mainfrom
Om-singhaI:fix/run-code-as-main
Oct 6, 2026
Merged

Rich Chiodo (rchiodo) merged 1 commit into
microsoft:mainfrom
Om-singhaI:fix/run-code-as-main

Conversation

@Om-singhaI

Copy link
Copy Markdown
Contributor

Fixes #2079.

run_code() in src/debugpy/server/cli.py ran the code with eval(code, {}). That globals dict has no __name__, so the name resolves through builtins as "builtins" and an if __name__ == "__main__": block never runs. Classes the code defines get __module__ == "builtins", so pickle can't find them either. A "code" launch goes through the same function, because the adapter turns it into -c for the debugpy CLI in the debuggee.

The code now runs in the namespace of a runpy._TempModule("__main__"), the helper run_path() already uses for a file target. While the code runs, __name__ is "__main__" and sys.modules["__main__"] is the module it runs in. The original __main__ is put back afterwards, the same as for a file target.

The new test test_run_code_runs_as_main in tests/debugpy/server/test_cli.py calls run_code() in process with start_debugging stubbed out. It checks __name__, that sys.modules["__main__"] is the module the code runs in, that a class the code defines round trips through pickle, and that the original __main__ is restored. Without the change to cli.py it fails with PicklingError: Can't pickle <class 'Point'>: attribute lookup Point on builtins failed.

Testing on macOS 26.6.2 with CPython 3.10.6, one worker:

  • tests/debugpy/server/test_cli.py: 96 passed.
  • The code target cases of test_run, test_nodebug, test_args, test_shell_expansion and test_break_api that go through the CLI: 14 passed, 1 skipped (shell expansion is not supported for internalConsole).
  • The repro from the issue now matches python -c on 3.10.6 and 3.14.3, and a "code" launch prints __name__ is __main__ and main block ran.
  • ruff check passes on both files.

run_code() evaluated the code with an empty globals dict, so __name__
resolved to the builtins module name and "if __name__ == '__main__'"
blocks never ran. Classes defined in the code also could not be pickled.
Run the code in a temporary __main__ module, the same way run_path()
does for a file target.
@Om-singhaI
om singhal (Om-singhaI) requested a review from a team as a code owner October 6, 2026 00:10
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@rchiodo

Rich Chiodo (rchiodo) commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

@rchiodo

Copy link
Copy Markdown
Contributor

Result: ⚠️ partially-verified

Verification details

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Summary: On CPython 3.13.16, targeted code-launch, argument, shell-expansion, and breakpoint tests produced 13 passes and 2 expected skips. A real debugpy CLI reproduction also passed, matching native `python -c` for the main guard and class/function pickle round trips. The full CLI test file timed out after 180 seconds without returning results, so the PR&#x27;s new regression test could not be confirmed. Verification is partial; no assertion failures were observed.

Test runs: 4 passed, 1 not run

  • ⚠️ Not run | CLI test file, including the new regression test | PYTHONPATH="$PWD/src:$PWD" DEBUGPY_TEST=1 python -Xfrozen_modules=off -m pytest tests/debugpy/server/test_cli.py -n0 -q
  • ✅ Passed | Code launch and no-debug internal-console tests | PYTHONPATH="$PWD/src:$PWD" DEBUGPY_TEST=1 python -Xfrozen_modules=off -m pytest tests/debugpy/test_run.py::test_run tests/debugpy/test_run.py::test_nodebug -k 'code and internalConsole' -n0 -q > /tmp/debugpy-code-launch.log 2>&1; status=$?; tail -n 40 /tmp/debugpy-code-launch.log; exit "$status"
  • ✅ Passed | Code-target CLI, arguments, shell expansion, and breakpoint integration tests | PYTHONPATH="$PWD/src:$PWD" DEBUGPY_TEST=1 python -Xfrozen_modules=off -m pytest tests/debugpy/test_run.py::test_run tests/debugpy/test_args.py::test_args tests/debugpy/test_args.py::test_shell_expansion tests/debugpy/test_breakpoints.py::test_break_api -k 'code and (cli or internalConsole or test_break_api) and not (test_run and internalConsole)' -n0 -q > /tmp/debugpy-code-integration.log 2>&1; status=$?; tail -n 40 /tmp/debugpy-code-integration.log; exit "$status"
  • ✅ Passed | Dependency and test discovery | printf 'AUTOMATION_SANDBOX_PROFILE=%s\nPIP_FIND_LINKS=%s\n' "$AUTOMATION_SANDBOX_PROFILE" "$PIP_FIND_LINKS"; python --version; git diff --stat HEAD~1 HEAD; python - <<'PY'
    import importlib.util
    from importlib import metadata
    from pathlib import Path
    for name in ['pytest', 'pytest_xdist', 'pytest_timeout', 'pytest_retry', 'pytest_cov', 'py', 'psutil', 'untangle', 'importlib_metadata', 'debugpy']:
    module = 'xdist' if name == 'pytest_xdist' else name
    spec = importlib.util.find_spec(module)
    try:
    version = metadata.version(name.replace('', '-'))
    except metadata.PackageNotFoundError:
    version = 'not installed'
    print(name, version, spec.origin if spec else 'MISSING')
    print('uv.lock:', Path('uv.lock').exists())
    print('venv:', Path('.venv/bin/python').exists())
    print('test files:', [str(p) for p in Path('tests/debugpy').glob('test
    *.py') if p.name in ['test_run.py', 'test_args.py', 'test_breakpoints.py']])
    PY
  • ✅ Passed | Real debugpy -c main-module and pickle reproduction | PYTHONPATH="$PWD/src:$PWD" python -Xfrozen_modules=off - <<'PY'
    import subprocess
    import sys
    import debugpy
    print('Source under test:', debugpy.file)
    code = """import pickle, sys
    import main
    class Point: pass
    def answer(): return 42
    assert name == 'main'
    assert main.dict is globals()
    assert Point.module == 'main'
    assert type(pickle.loads(pickle.dumps(Point()))) is Point
    assert pickle.loads(pickle.dumps(answer)) is answer
    if name == 'main':
    print('main block ran')
    print('class and function pickle round-trips passed')
    """
    native = subprocess.run([sys.executable, '-Xfrozen_modules=off', '-c', code], capture_output=True, text=True, check=True, timeout=30)
    debugged = subprocess.run([sys.executable, '-Xfrozen_modules=off', '-m', 'debugpy', '--listen', '127.0.0.1:0', '-c', code], capture_output=True, text=True, check=True, timeout=30)
    assert debugged.stdout == native.stdout, (debugged.stdout, native.stdout)
    print(debugged.stdout, end='')
    print('Real debugpy CLI output matches native python -c')
    PY
⚠️ CLI test file, including the new regression test diagnostic output
[error] command timed out after 180s; the sandbox container was destroyed. No test results were returned.

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) added the review-auto:approved Automated review: no blocking findings (approval posted). label Oct 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@rchiodo
Rich Chiodo (rchiodo) merged commit e77356e into microsoft:main Oct 6, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code run with -c or a "code" launch gets __name__ == "builtins" instead of "__main__"

3 participants