From b37d4fbf71db3ac80766b4fb1d6370d58a154a41 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 7 Aug 2026 10:00:36 -0700 Subject: [PATCH 1/2] Avoid exceptions in environment diagnostics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/debugpy/common/log.py | 32 ++++++++++++-------------------- tests/debugpy/test_log.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/debugpy/common/log.py b/src/debugpy/common/log.py index 099e93c7..fda91e1c 100644 --- a/src/debugpy/common/log.py +++ b/src/debugpy/common/log.py @@ -5,6 +5,7 @@ import atexit import contextlib import functools +from importlib import metadata as importlib_metadata import inspect import io import os @@ -16,7 +17,6 @@ import debugpy from debugpy.common import json, timestamp, util - LEVELS = ("debug", "info", "warning", "error") """Logging levels, lowest to highest importance. """ @@ -284,6 +284,7 @@ def get_environment_description(header): import site # noqa result = [header, "\n\n"] + missing = object() def report(s, *args, **kwargs): result.append(s.format(*args, **kwargs)) @@ -308,6 +309,10 @@ def report_paths(get_paths, label=None): ) return + if paths is missing: + report("{0}\n", prefix) + return + if not isinstance(paths, (list, tuple)): paths = [paths] @@ -325,7 +330,7 @@ def report_paths(get_paths, label=None): report_paths("sys.executable") report_paths("sys.prefix") report_paths("sys.base_prefix") - report_paths("sys.real_prefix") + report_paths(lambda: getattr(sys, "real_prefix", missing), "sys.real_prefix") report_paths("site.getsitepackages()") report_paths("site.getusersitepackages()") @@ -345,25 +350,12 @@ def report_paths(get_paths, label=None): report_paths("debugpy.__file__") report("\n") - importlib_metadata = None + report("Installed packages:\n") try: - import importlib_metadata - except ImportError: # pragma: no cover - try: - from importlib import metadata as importlib_metadata - except ImportError: - pass - if importlib_metadata is None: # pragma: no cover - report("Cannot enumerate installed packages - missing importlib_metadata.") - else: - report("Installed packages:\n") - try: - for pkg in importlib_metadata.distributions(): - report(" {0}=={1}\n", pkg.name, pkg.version) - except Exception: # pragma: no cover - swallow_exception( - "Error while enumerating installed packages.", level="info" - ) + for pkg in importlib_metadata.distributions(): + report(" {0}=={1}\n", pkg.name, pkg.version) + except Exception: # pragma: no cover + swallow_exception("Error while enumerating installed packages.", level="info") return "".join(result).rstrip("\n") diff --git a/tests/debugpy/test_log.py b/tests/debugpy/test_log.py index 6100474e..706d7629 100644 --- a/tests/debugpy/test_log.py +++ b/tests/debugpy/test_log.py @@ -3,12 +3,42 @@ # for license information. import contextlib +import os +import sys + import pytest +import debugpy +from debugpy.common import log from tests import debug from tests.debug import runners, targets +def test_environment_description_does_not_raise_internal_exceptions(monkeypatch): + from importlib import metadata as importlib_metadata + + monkeypatch.delattr(sys, "real_prefix", raising=False) + monkeypatch.setattr(importlib_metadata, "distributions", lambda: ()) + + debugpy_root = os.path.dirname(debugpy.__file__) + exceptions = [] + + def trace(frame, event, arg): + if event == "exception" and frame.f_code.co_filename.startswith(debugpy_root): + exceptions.append(arg[1]) + return trace + + previous_trace = sys.gettrace() + sys.settrace(trace) + try: + description = log.get_environment_description("Environment:") + finally: + sys.settrace(previous_trace) + + assert "sys.real_prefix: " in description + assert exceptions == [] + + @contextlib.contextmanager def check_logs(tmpdir, run): # For attach_pid, there's ptvsd.server process that performs the injection, From 0cb628f671b54e3724b030fe9894a825386c55a7 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 7 Aug 2026 10:40:24 -0700 Subject: [PATCH 2/2] Narrow real_prefix regression test to targeted exception Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/debugpy/test_log.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/debugpy/test_log.py b/tests/debugpy/test_log.py index 706d7629..5c1dcdc9 100644 --- a/tests/debugpy/test_log.py +++ b/tests/debugpy/test_log.py @@ -21,11 +21,18 @@ def test_environment_description_does_not_raise_internal_exceptions(monkeypatch) monkeypatch.setattr(importlib_metadata, "distributions", lambda: ()) debugpy_root = os.path.dirname(debugpy.__file__) - exceptions = [] + real_prefix_exceptions = [] def trace(frame, event, arg): + # Only record AttributeErrors that mention ``real_prefix``. ``exception`` + # events fire for every exception raised in a debugpy frame, including + # ones that unrelated environment probes intentionally raise and catch + # (e.g. ``site.getsitepackages()``), so an unfiltered assertion would be + # environment-dependent. Narrow it to the behavior this test targets. if event == "exception" and frame.f_code.co_filename.startswith(debugpy_root): - exceptions.append(arg[1]) + exc = arg[1] + if isinstance(exc, AttributeError) and "real_prefix" in str(exc): + real_prefix_exceptions.append(exc) return trace previous_trace = sys.gettrace() @@ -36,7 +43,7 @@ def trace(frame, event, arg): sys.settrace(previous_trace) assert "sys.real_prefix: " in description - assert exceptions == [] + assert real_prefix_exceptions == [] @contextlib.contextmanager