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..5c1dcdc9 100644 --- a/tests/debugpy/test_log.py +++ b/tests/debugpy/test_log.py @@ -3,12 +3,49 @@ # 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__) + 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): + exc = arg[1] + if isinstance(exc, AttributeError) and "real_prefix" in str(exc): + real_prefix_exceptions.append(exc) + 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 real_prefix_exceptions == [] + + @contextlib.contextmanager def check_logs(tmpdir, run): # For attach_pid, there's ptvsd.server process that performs the injection,