From 9cf2ad0b55e49483c66b90a989867ce5d9b0b451 Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Sat, 1 Oct 2016 12:08:25 -0700 Subject: [PATCH 1/3] Monkey-patch a weakref set in Task to be a no-op. (#3639) * Monkey-patch a weakref set in Task to be a no-op. * Fix linting issues --- homeassistant/__main__.py | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index a7b2027963f..30dfa6b6db0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -19,6 +19,49 @@ from homeassistant.const import ( from homeassistant.util.async import run_callback_threadsafe +def monkey_patch_asyncio(): + """Replace weakref.WeakSet to address Python 3 bug. + + Under heavy threading operations that schedule calls into + the asyncio event loop, Task objects are created. Due to + a bug in Python, GC may have an issue when switching between + the threads and objects with __del__ (which various components + in HASS have). + + This monkey-patch removes the weakref.Weakset, and replaces it + with an object that ignores the only call utilizing it (the + Task.__init__ which calls _all_tasks.add(self)). It also removes + the __del__ which could trigger the future objects __del__ at + unpredictable times. + + The side-effect of this manipulation of the Task is that + Task.all_tasks() is no longer accurate, and there will be no + warning emitted if a Task is GC'd while in use. + + On Python 3.6, after the bug is fixed, this monkey-patch can be + disabled. + + See https://bugs.python.org/issue26617 for details of the Python + bug. + """ + # pylint: disable=no-self-use, too-few-public-methods, protected-access + # pylint: disable=bare-except + import asyncio.tasks + + class IgnoreCalls: + """Ignore add calls.""" + + def add(self, other): + """No-op add.""" + return + + asyncio.tasks.Task._all_tasks = IgnoreCalls() + try: + del asyncio.tasks.Task.__del__ + except: + pass + + def validate_python() -> None: """Validate we're running the right Python version.""" if sys.version_info[:3] < REQUIRED_PYTHON_VER: @@ -308,6 +351,8 @@ def try_to_restart() -> None: def main() -> int: """Start Home Assistant.""" + monkey_patch_asyncio() + validate_python() args = get_arguments() From ef0e018cbbfb6e1048b932cee8ab251cfe6e01d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Sep 2016 23:26:15 -0700 Subject: [PATCH 2/3] Service config calls will no longer mutate original config (#3628) --- homeassistant/helpers/service.py | 26 +++++++++++--------------- homeassistant/helpers/template.py | 6 ++++++ tests/helpers/test_service.py | 29 +++++++++++++++++------------ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 092d5983308..665e22404c6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -62,22 +62,18 @@ def call_from_config(hass, config, blocking=False, variables=None, domain, service_name = domain_service.split('.', 1) service_data = dict(config.get(CONF_SERVICE_DATA, {})) - def _data_template_creator(value): - """Recursive template creator helper function.""" - if isinstance(value, list): - for idx, element in enumerate(value): - value[idx] = _data_template_creator(element) - return value - if isinstance(value, dict): - for key, element in value.items(): - value[key] = _data_template_creator(element) - return value - value.hass = hass - return value.render(variables) - if CONF_SERVICE_DATA_TEMPLATE in config: - for key, value in config[CONF_SERVICE_DATA_TEMPLATE].items(): - service_data[key] = _data_template_creator(value) + def _data_template_creator(value): + """Recursive template creator helper function.""" + if isinstance(value, list): + return [_data_template_creator(item) for item in value] + elif isinstance(value, dict): + return {key: _data_template_creator(item) + for key, item in value.items()} + value.hass = hass + return value.render(variables) + service_data.update(_data_template_creator( + config[CONF_SERVICE_DATA_TEMPLATE])) if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d8005858a1e..b2d0a5942ee 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -159,6 +159,12 @@ class Template(object): return self._compiled + def __eq__(self, other): + """Compare template with another.""" + return (self.__class__ == other.__class__ and + self.template == other.template and + self.hass == other.hass) + class AllStates(object): """Class to expose all HA states as attributes.""" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index d9fe3ff9c15..38af2178340 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,4 +1,5 @@ """Test service helpers.""" +from copy import deepcopy import unittest from unittest.mock import patch @@ -6,7 +7,8 @@ from unittest.mock import patch import homeassistant.components # noqa from homeassistant import core as ha, loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID -from homeassistant.helpers import service +from homeassistant.helpers import service, template +import homeassistant.helpers.config_validation as cv from tests.common import get_test_home_assistant, mock_service @@ -97,22 +99,25 @@ class TestServiceHelpers(unittest.TestCase): def test_not_mutate_input(self): """Test for immutable input.""" - orig = { + config = cv.SERVICE_SCHEMA({ 'service': 'test_domain.test_service', 'entity_id': 'hello.world, sensor.beer', 'data': { 'hello': 1, }, - } - service.call_from_config(self.hass, orig) - self.hass.block_till_done() - self.assertEqual({ - 'service': 'test_domain.test_service', - 'entity_id': 'hello.world, sensor.beer', - 'data': { - 'hello': 1, - }, - }, orig) + 'data_template': { + 'nested': { + 'value': '{{ 1 + 1 }}' + } + } + }) + orig = deepcopy(config) + + # Only change after call is each template getting hass attached + template.attach(self.hass, orig) + + service.call_from_config(self.hass, config, validate_config=False) + assert orig == config @patch('homeassistant.helpers.service._LOGGER.error') def test_fail_silently_if_no_service(self, mock_log): From 756f23f0b459d394cbdbf31d8057ee5659079508 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Oct 2016 12:10:00 -0700 Subject: [PATCH 3/3] Version bump to 0.29.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b5869d2d3c0..037b567f05a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 29 -PATCH_VERSION = '5' +PATCH_VERSION = '6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)