diff --git a/src/python/pants/backend/python/goals/pytest_runner.py b/src/python/pants/backend/python/goals/pytest_runner.py index 094124b4c65..bf1b43c3071 100644 --- a/src/python/pants/backend/python/goals/pytest_runner.py +++ b/src/python/pants/backend/python/goals/pytest_runner.py @@ -68,7 +68,6 @@ from pants.engine.unions import UnionMembership, UnionRule, union from pants.option.global_options import GlobalOptions from pants.util.logging import LogLevel -from pants.util.strutil import softwrap logger = logging.getLogger() @@ -410,15 +409,6 @@ async def debugpy_python_test( pytest: PyTest, ) -> TestDebugAdapterRequest: debugpy_pex = await Get(Pex, PexRequest, debugpy.to_pex_request()) - if pytest.main.spec != "pytest": - logger.warning( - softwrap( - """ - Ignoring custom [pytest].console_script/entry_point when using the debug adapter. - `debugpy` (Python's Debug Adapter) doesn't support this use-case yet. - """ - ) - ) setup = await Get( TestSetup, @@ -426,16 +416,7 @@ async def debugpy_python_test( field_set, is_debug=True, main=debugpy.main, - prepend_argv=( - "--listen", - f"{debug_adapter.host}:{debug_adapter.port}", - "--wait-for-client", - # @TODO: Techincally we should use `pytest.main`, however `debugpy` doesn't support - # launching an entry_point. - # https://github.com/microsoft/debugpy/issues/955 - "-m", - "pytest", - ), + prepend_argv=debugpy.get_args(debug_adapter, pytest.main), additional_pexes=(debugpy_pex,), ), ) diff --git a/src/python/pants/backend/python/goals/pytest_runner_integration_test.py b/src/python/pants/backend/python/goals/pytest_runner_integration_test.py index 23b3c45e0ba..716942ff225 100644 --- a/src/python/pants/backend/python/goals/pytest_runner_integration_test.py +++ b/src/python/pants/backend/python/goals/pytest_runner_integration_test.py @@ -5,6 +5,7 @@ import os import re +import unittest.mock from textwrap import dedent import pytest @@ -610,7 +611,7 @@ def test_debug_adaptor_request_argv(rule_runner: RuleRunner) -> None: "--listen", "127.0.0.1:5678", "--wait-for-client", - "-m", - "pytest", + "-c", + unittest.mock.ANY, "tests/python/pants_test/test_foo.py", ) diff --git a/src/python/pants/backend/python/subsystems/debugpy.py b/src/python/pants/backend/python/subsystems/debugpy.py index a4aa946ef7e..648e79524f1 100644 --- a/src/python/pants/backend/python/subsystems/debugpy.py +++ b/src/python/pants/backend/python/subsystems/debugpy.py @@ -7,15 +7,18 @@ from pants.backend.python.goals.lockfile import GeneratePythonLockfile from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup -from pants.backend.python.target_types import EntryPoint +from pants.backend.python.target_types import ConsoleScript, EntryPoint, MainSpecification from pants.core.goals.generate_lockfiles import GenerateToolLockfileSentinel +from pants.core.subsystems.debug_adapter import DebugAdapterSubsystem from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule +from pants.option.option_types import ArgsListOption from pants.util.docutil import git_url class DebugPy(PythonToolBase): options_scope = "debugpy" + name = options_scope help = "An implementation of the Debug Adapter Protocol for Python (https://github.com/microsoft/debugpy)." default_version = "debugpy==1.6.0" @@ -29,6 +32,37 @@ class DebugPy(PythonToolBase): default_lockfile_path = "src/python/pants/backend/python/subsystems/debugpy.lock" default_lockfile_url = git_url(default_lockfile_path) + args = ArgsListOption(example="--log-to-stderr") + + @staticmethod + def _get_main_spec_args(main: MainSpecification) -> tuple[str, ...]: + if isinstance(main, EntryPoint): + if main.function: + return ("-c", f"import {main.module};{main.module}.{main.function}();") + return ("-m", main.module) + + assert isinstance(main, ConsoleScript) + # NB: This is only necessary while debugpy supports Py 3.7, as `importlib.metadata` was + # added in Py 3.8 and would allow us to resolve the console_script using just the stdlib. + return ( + "-c", + ( + "from pex.third_party.pkg_resources import working_set;" + + f"(next(working_set.iter_entry_points('console_scripts', '{main.name}')).resolve())()" + ), + ) + + def get_args( + self, debug_adapter: DebugAdapterSubsystem, main: MainSpecification + ) -> tuple[str, ...]: + return ( + "--listen", + f"{debug_adapter.host}:{debug_adapter.port}", + "--wait-for-client", + *self.args, + *self._get_main_spec_args(main), + ) + class DebugPyLockfileSentinel(GenerateToolLockfileSentinel): resolve_name = DebugPy.options_scope