From b47be05efc20c7be1b8389c04d95da63fc997e0b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Jun 2020 15:17:59 +0200 Subject: [PATCH] Add new Remote Python Debugger integration (#36960) --- CODEOWNERS | 1 + homeassistant/bootstrap.py | 2 +- homeassistant/components/debugpy/__init__.py | 79 +++++++++++++++++++ .../components/debugpy/manifest.json | 8 ++ .../components/debugpy/services.yaml | 3 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/debugpy/__init__.py | 1 + tests/components/debugpy/test_init.py | 61 ++++++++++++++ 9 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/debugpy/__init__.py create mode 100644 homeassistant/components/debugpy/manifest.json create mode 100644 homeassistant/components/debugpy/services.yaml create mode 100644 tests/components/debugpy/__init__.py create mode 100644 tests/components/debugpy/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 81cdb289295..04711d45db7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -86,6 +86,7 @@ homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cups/* @fabaff homeassistant/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff +homeassistant/components/debugpy/* @frenck homeassistant/components/deconz/* @Kane610 homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a73cebce085..94ec33f4e1a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -40,7 +40,7 @@ DATA_LOGGING = "logging" LOG_SLOW_STARTUP_INTERVAL = 60 -DEBUGGER_INTEGRATIONS = {"ptvsd"} +DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") LOGGING_INTEGRATIONS = { # Set log levels diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py new file mode 100644 index 00000000000..caa691b2369 --- /dev/null +++ b/homeassistant/components/debugpy/__init__.py @@ -0,0 +1,79 @@ +"""The Remote Python Debugger integration.""" +from asyncio import Event +import logging +from threading import Thread +from typing import Optional + +import debugpy +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import ConfigType + +DOMAIN = "debugpy" +CONF_WAIT = "wait" +CONF_START = "start" +SERVICE_START = "start" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default="0.0.0.0"): cv.string, + vol.Optional(CONF_PORT, default=5678): cv.port, + vol.Optional(CONF_START, default=True): cv.boolean, + vol.Optional(CONF_WAIT, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Remote Python Debugger component.""" + conf = config[DOMAIN] + + async def debug_start( + call: Optional[ServiceCall] = None, *, wait: bool = True + ) -> None: + """Start the debugger.""" + debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) + + wait = conf[CONF_WAIT] + if wait: + _LOGGER.warning( + "Waiting for remote debug connection on %s:%s", + conf[CONF_HOST], + conf[CONF_PORT], + ) + ready = Event() + + def waitfor(): + debugpy.wait_for_client() + hass.loop.call_soon_threadsafe(ready.set) + + Thread(target=waitfor).start() + + await ready.wait() + else: + _LOGGER.warning( + "Listening for remote debug connection on %s:%s", + conf[CONF_HOST], + conf[CONF_PORT], + ) + + async_register_admin_service( + hass, DOMAIN, SERVICE_START, debug_start, schema=vol.Schema({}) + ) + + # If set to start the debugger on startup, do so + if conf[CONF_START]: + await debug_start(wait=conf[CONF_WAIT]) + + return True diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json new file mode 100644 index 00000000000..c27e7411de2 --- /dev/null +++ b/homeassistant/components/debugpy/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "debugpy", + "name": "Remote Python Debugger", + "documentation": "https://www.home-assistant.io/integrations/debugpy", + "requirements": ["debugpy==1.0.0b11"], + "codeowners": ["@frenck"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/debugpy/services.yaml b/homeassistant/components/debugpy/services.yaml new file mode 100644 index 00000000000..4e3c19dd0d7 --- /dev/null +++ b/homeassistant/components/debugpy/services.yaml @@ -0,0 +1,3 @@ +# Describes the format for available Remote Python Debugger services +start: + description: Start the Remote Python Debugger. diff --git a/requirements_all.txt b/requirements_all.txt index f7175fa2dd8..33ac932e565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,6 +456,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.5 +# homeassistant.components.debugpy +debugpy==1.0.0b11 + # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a610fb0115..c22db62b9ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,6 +211,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.5 +# homeassistant.components.debugpy +debugpy==1.0.0b11 + # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect diff --git a/tests/components/debugpy/__init__.py b/tests/components/debugpy/__init__.py new file mode 100644 index 00000000000..4b816141921 --- /dev/null +++ b/tests/components/debugpy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Remote Python Debugger integration.""" diff --git a/tests/components/debugpy/test_init.py b/tests/components/debugpy/test_init.py new file mode 100644 index 00000000000..1de8da9ac9a --- /dev/null +++ b/tests/components/debugpy/test_init.py @@ -0,0 +1,61 @@ +"""Tests for the Remote Python Debugger integration.""" +import pytest + +from homeassistant.components.debugpy import ( + CONF_HOST, + CONF_PORT, + CONF_START, + CONF_WAIT, + DOMAIN, + SERVICE_START, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +@pytest.fixture +def mock_debugpy(): + """Mock debugpy lib.""" + with patch("homeassistant.components.debugpy.debugpy") as mocked_debugpy: + yield mocked_debugpy + + +async def test_default(hass: HomeAssistant, mock_debugpy) -> None: + """Test if the default settings work.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + mock_debugpy.listen.assert_called_once_with(("0.0.0.0", 5678)) + mock_debugpy.wait_for_client.assert_not_called() + assert len(mock_debugpy.method_calls) == 1 + + +async def test_wait_on_startup(hass: HomeAssistant, mock_debugpy) -> None: + """Test if the waiting for client is called.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_WAIT: True}}) + + mock_debugpy.listen.assert_called_once_with(("0.0.0.0", 5678)) + mock_debugpy.wait_for_client.assert_called_once() + assert len(mock_debugpy.method_calls) == 2 + + +async def test_on_demand(hass: HomeAssistant, mock_debugpy) -> None: + """Test on-demand debugging using a service call.""" + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {CONF_START: False, CONF_HOST: "127.0.0.1", CONF_PORT: 80}}, + ) + + mock_debugpy.listen.assert_not_called() + mock_debugpy.wait_for_client.assert_not_called() + assert len(mock_debugpy.method_calls) == 0 + + await hass.services.async_call( + DOMAIN, SERVICE_START, blocking=True, + ) + + mock_debugpy.listen.assert_called_once_with(("127.0.0.1", 80)) + mock_debugpy.wait_for_client.assert_not_called() + assert len(mock_debugpy.method_calls) == 1