From 714b5161767bc426f261835b4fc06519435076e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Mar 2017 00:50:53 -0700 Subject: [PATCH] aiohttp 2 (#6835) * Upgrade aiohttp2 * Fix resource caching * Fix helpers.aiohttp_client * Lint * Use static path for api error_log * Fix ClientErrors import * Remove not needed DisconnectError * Remove releasing responses code * Add timeout if stream starts non responding * More async_aiohttp_proxy_stream cleanup * Fix references to ClientError * Fix fingerprinting * Fix aiohttp stream tests * Rename aiohttp_proxy_stream * Remove impossible darksky test * Fix sleepiq requests escaping mocker * Lint * Remove deprecated parameter * Break up aiohttp_proxy_stream in 2 methods * Lint * Upgrade to aiohttp 2.0.4 * Convert connector close to a callback * Fix static fingerprinted links --- homeassistant/components/api.py | 18 +-- homeassistant/components/camera/__init__.py | 7 +- homeassistant/components/camera/amcrest.py | 4 +- homeassistant/components/camera/ffmpeg.py | 30 ++--- homeassistant/components/camera/generic.py | 8 +- homeassistant/components/camera/mjpeg.py | 12 +- homeassistant/components/camera/synology.py | 26 ++-- .../components/device_tracker/__init__.py | 7 +- .../components/device_tracker/tado.py | 8 +- .../components/device_tracker/upc_connect.py | 23 +--- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/http/__init__.py | 53 ++++---- homeassistant/components/http/static.py | 63 ++++++---- .../image_processing/openalpr_cloud.py | 8 +- .../components/media_player/__init__.py | 12 +- .../components/media_player/squeezebox.py | 14 +-- .../components/media_player/volumio.py | 8 +- homeassistant/components/microsoft_face.py | 21 ++-- homeassistant/components/rest_command.py | 15 +-- homeassistant/components/sensor/yr.py | 11 +- homeassistant/components/switch/hook.py | 25 +--- homeassistant/components/switch/rest.py | 29 ++--- homeassistant/components/tts/google.py | 7 +- homeassistant/components/tts/voicerss.py | 7 +- homeassistant/components/tts/yandextts.py | 8 +- homeassistant/helpers/aiohttp_client.py | 56 +++++---- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- tests/components/sensor/test_darksky.py | 11 +- tests/components/switch/test_rest.py | 2 +- tests/components/test_api.py | 19 --- tests/components/test_frontend.py | 116 ++++++++---------- tests/components/test_rest_command.py | 2 +- tests/components/test_sleepiq.py | 10 +- tests/helpers/test_aiohttp_client.py | 44 ++----- 36 files changed, 250 insertions(+), 442 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 1af85605874..b22bd851190 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -50,9 +50,11 @@ def setup(hass, config): hass.http.register_view(APIDomainServicesView) hass.http.register_view(APIEventForwardingView) hass.http.register_view(APIComponentsView) - hass.http.register_view(APIErrorLogView) hass.http.register_view(APITemplateView) + hass.http.register_static_path( + URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False) + return True @@ -402,20 +404,6 @@ class APIComponentsView(HomeAssistantView): return self.json(request.app['hass'].config.components) -class APIErrorLogView(HomeAssistantView): - """View to handle ErrorLog requests.""" - - url = URL_API_ERROR_LOG - name = "api:error-log" - - @asyncio.coroutine - def get(self, request): - """Serve error log.""" - resp = yield from self.file( - request, request.app['hass'].config.path(ERROR_LOG_FILENAME)) - return resp - - class APITemplateView(HomeAssistantView): """View to handle requests.""" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b531a931a7a..d3c6699f77c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -58,7 +58,6 @@ def async_get_image(hass, entity_id, timeout=10): state.attributes.get(ATTR_ENTITY_PICTURE) ) - response = None try: with async_timeout.timeout(timeout, loop=hass.loop): response = yield from websession.get(url) @@ -70,13 +69,9 @@ def async_get_image(hass, entity_id, timeout=10): image = yield from response.read() return image - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): raise HomeAssistantError("Can't connect to {0}".format(url)) - finally: - if response is not None: - yield from response.release() - @asyncio.coroutine def async_setup(hass, config): diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index f272dda4bb3..9bf89b8b3a5 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_stream) + async_get_clientsession, async_aiohttp_proxy_web) REQUIREMENTS = ['amcrest==1.1.4'] @@ -125,7 +125,7 @@ class AmcrestCam(Camera): stream_coro = websession.get( streaming_url, auth=self._token, timeout=TIMEOUT) - yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) + yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index ed8c84f90df..d4c7b54fc7f 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -8,14 +8,14 @@ import asyncio import logging import voluptuous as vol -from aiohttp import web +from homeassistant.const import CONF_NAME from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME - +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_stream) DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) @@ -69,26 +69,10 @@ class FFmpegCamera(Camera): yield from stream.open_camera( self._input, extra_cmd=self._extra_arguments) - response = web.StreamResponse() - response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' - - yield from response.prepare(request) - - try: - while True: - data = yield from stream.read(102400) - if not data: - break - response.write(data) - - except asyncio.CancelledError: - _LOGGER.debug("Close stream by frontend.") - response = None - - finally: - yield from stream.close() - if response is not None: - yield from response.write_eof() + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() @property def name(self): diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 3f50bc799c4..a330836f32b 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -107,7 +107,6 @@ class GenericCamera(Camera): None, fetch) # async else: - response = None try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): @@ -117,14 +116,9 @@ class GenericCamera(Camera): except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') return self._last_image - except (aiohttp.errors.ClientError, - aiohttp.errors.DisconnectedError, - aiohttp.errors.HttpProcessingError) as err: + except aiohttp.ClientError as err: _LOGGER.error('Error getting new camera image: %s', err) return self._last_image - finally: - if response is not None: - yield from response.release() self._last_url = url return self._last_image diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 532b91e7442..a158c36152e 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -19,7 +19,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_stream) + async_get_clientsession, async_aiohttp_proxy_web) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -93,7 +93,6 @@ class MjpegCamera(Camera): return image websession = async_get_clientsession(self.hass) - response = None try: with async_timeout.timeout(10, loop=self.hass.loop): response = yield from websession.get( @@ -105,14 +104,9 @@ class MjpegCamera(Camera): except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') - except (aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as err: + except aiohttp.ClientError as err: _LOGGER.error('Error getting new camera image: %s', err) - finally: - if response is not None: - yield from response.release() - def camera_image(self): """Return a still image response from the camera.""" if self._username and self._password: @@ -140,7 +134,7 @@ class MjpegCamera(Camera): websession = async_get_clientsession(self.hass) stream_coro = websession.get(self._mjpeg_url, auth=self._auth) - yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) + yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 2b4d72f92f8..e986d81887b 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -19,7 +19,7 @@ from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_create_clientsession, - async_aiohttp_proxy_stream) + async_aiohttp_proxy_web) import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe @@ -74,7 +74,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 'version': '1', 'query': 'SYNO.' } - query_req = None try: with async_timeout.timeout(timeout, loop=hass.loop): query_req = yield from websession_init.get( @@ -88,14 +87,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera_path = query_resp['data'][CAMERA_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path'] - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", syno_api_url) return False - finally: - if query_req is not None: - yield from query_req.release() - # Authticate to NAS to get a session id syno_auth_url = SYNO_API_URL.format( config.get(CONF_URL), WEBAPI_PATH, auth_path) @@ -128,13 +123,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): syno_camera_url, params=camera_payload ) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", syno_camera_url) return False camera_resp = yield from camera_req.json() cameras = camera_resp['data']['cameras'] - yield from camera_req.release() # add cameras devices = [] @@ -172,7 +166,6 @@ def get_session_id(hass, websession, username, password, login_url, timeout): 'session': 'SurveillanceStation', 'format': 'sid' } - auth_req = None try: with async_timeout.timeout(timeout, loop=hass.loop): auth_req = yield from websession.get( @@ -182,14 +175,10 @@ def get_session_id(hass, websession, username, password, login_url, timeout): auth_resp = yield from auth_req.json() return auth_resp['data']['sid'] - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", login_url) return False - finally: - if auth_req is not None: - yield from auth_req.release() - class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" @@ -235,12 +224,11 @@ class SynologyCamera(Camera): image_url, params=image_payload ) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): - _LOGGER.exception("Error on %s", image_url) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Error fetching %s", image_url) return None image = yield from response.read() - yield from response.release() return image @@ -260,7 +248,7 @@ class SynologyCamera(Camera): stream_coro = self._websession.get( streaming_url, params=streaming_payload) - yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) + yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 3e04f46cb50..f2a538507a0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -553,7 +553,6 @@ class Device(Entity): # bytes like 00 get truncates to 0, API needs full bytes oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) url = 'http://api.macvendors.com/' + oui - resp = None try: websession = async_get_clientsession(self.hass) @@ -570,13 +569,9 @@ class Device(Entity): # in the 'known_devices.yaml' file which only happens # the first time the device is seen. return 'unknown' - except (asyncio.TimeoutError, aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError): + except (asyncio.TimeoutError, aiohttp.ClientError): # same as above return 'unknown' - finally: - if resp is not None: - yield from resp.release() @asyncio.coroutine def async_added_to_hass(self): diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index ca0bec29706..3c21037d028 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -105,8 +105,6 @@ class TadoDeviceScanner(DeviceScanner): _LOGGER.debug("Requesting Tado") last_results = [] - response = None - tado_json = None try: with async_timeout.timeout(10, loop=self.hass.loop): @@ -127,14 +125,10 @@ class TadoDeviceScanner(DeviceScanner): tado_json = yield from response.json() - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Cannot load Tado data") return False - finally: - if response is not None: - yield from response.release() - # Without a home_id, we fetched an URL where the mobile devices can be # found under the mobileDevices key. if 'mobileDevices' in tado_json: diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index 879a645fe30..cec00221aaf 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -101,7 +101,6 @@ class UPCDeviceScanner(DeviceScanner): @asyncio.coroutine def async_login(self): """Login into firmware and get first token.""" - response = None try: # get first token with async_timeout.timeout(10, loop=self.hass.loop): @@ -109,7 +108,8 @@ class UPCDeviceScanner(DeviceScanner): "http://{}/common_page/login.html".format(self.host) ) - yield from response.text() + yield from response.text() + self.token = response.cookies['sessionToken'].value # login @@ -119,18 +119,12 @@ class UPCDeviceScanner(DeviceScanner): }) # successfull? - if data is not None: - return True - return False + return data is not None - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Can not load login page from %s", self.host) return False - finally: - if response is not None: - yield from response.release() - @asyncio.coroutine def _async_ws_function(self, function, additional_form=None): """Execute a command on UPC firmware webservice.""" @@ -142,8 +136,7 @@ class UPCDeviceScanner(DeviceScanner): if additional_form: form_data.update(additional_form) - redirects = True if function != CMD_DEVICES else False - response = None + redirects = function != CMD_DEVICES try: with async_timeout.timeout(10, loop=self.hass.loop): response = yield from self.websession.post( @@ -163,10 +156,6 @@ class UPCDeviceScanner(DeviceScanner): self.token = response.cookies['sessionToken'].value return (yield from response.text()) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error on %s", function) self.token = None - - finally: - if response is not None: - yield from response.release() diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7d95cfb20fe..b6f65d9953a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -153,7 +153,7 @@ def setup(hass, config): sw_path = "service_worker.js" hass.http.register_static_path("/service_worker.js", - os.path.join(STATIC_PATH, sw_path), 0) + os.path.join(STATIC_PATH, sw_path), False) hass.http.register_static_path("/robots.txt", os.path.join(STATIC_PATH, "robots.txt")) hass.http.register_static_path("/static", STATIC_PATH) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d6e03e76619..f99b2390bb8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -9,7 +9,6 @@ import json import logging import ssl from ipaddress import ip_network -from pathlib import Path import os import voluptuous as vol @@ -31,7 +30,8 @@ from .const import ( KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, KEY_DEVELOPMENT, KEY_AUTHENTICATED) -from .static import FILE_SENDER, CACHING_FILE_SENDER, staticresource_middleware +from .static import ( + staticresource_middleware, CachingFileResponse, CachingStaticResource) from .util import get_real_ip DOMAIN = 'http' @@ -187,7 +187,7 @@ class HomeAssistantWSGI(object): if is_ban_enabled: middlewares.insert(0, ban_middleware) - self.app = web.Application(middlewares=middlewares, loop=hass.loop) + self.app = web.Application(middlewares=middlewares) self.app['hass'] = hass self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for self.app[KEY_TRUSTED_NETWORKS] = trusted_networks @@ -255,31 +255,39 @@ class HomeAssistantWSGI(object): self.app.router.add_route('GET', url, redirect) - def register_static_path(self, url_root, path, cache_length=31): - """Register a folder to serve as a static path. - - Specify optional cache length of asset in days. - """ + def register_static_path(self, url_path, path, cache_headers=True): + """Register a folder or file to serve as a static path.""" if os.path.isdir(path): - self.app.router.add_static(url_root, path) + if cache_headers: + resource = CachingStaticResource + else: + resource = web.StaticResource + + self.app.router.register_resource(resource(url_path, path)) return - filepath = Path(path) - - @asyncio.coroutine - def serve_file(request): - """Serve file from disk.""" - res = yield from CACHING_FILE_SENDER.send(request, filepath) - return res + if cache_headers: + @asyncio.coroutine + def serve_file(request): + """Serve file from disk.""" + return CachingFileResponse(path) + else: + @asyncio.coroutine + def serve_file(request): + """Serve file from disk.""" + return web.FileResponse(path) # aiohttp supports regex matching for variables. Using that as temp # to work around cache busting MD5. # Turns something like /static/dev-panel.html into # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} - base, ext = url_root.rsplit('.', 1) - base, file = base.rsplit('/', 1) - regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext) - url_pattern = "{}/{{filename:{}}}".format(base, regex) + base, ext = os.path.splitext(url_path) + if ext: + base, file = base.rsplit('/', 1) + regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext) + url_pattern = "{}/{{filename:{}}}".format(base, regex) + else: + url_pattern = url_path self.app.router.add_route('GET', url_pattern, serve_file) @@ -318,7 +326,7 @@ class HomeAssistantWSGI(object): # re-register all redirects, views, static paths. self.app._frozen = True # pylint: disable=protected-access - self._handler = self.app.make_handler() + self._handler = self.app.make_handler(loop=self.hass.loop) try: self.server = yield from self.hass.loop.create_server( @@ -365,8 +373,7 @@ class HomeAssistantView(object): def file(self, request, fil): """Return a file.""" assert isinstance(fil, str), 'only string paths allowed' - response = yield from FILE_SENDER.send(request, Path(fil)) - return response + return web.FileResponse(fil) def register(self, router): """Register the view with a router.""" diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 6489144ec70..21e955fc968 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,14 +3,45 @@ import asyncio import re from aiohttp import hdrs -from aiohttp.file_sender import FileSender +from aiohttp.web import FileResponse +from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_urldispatcher import StaticResource +from yarl import unquote + from .const import KEY_DEVELOPMENT _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) -class CachingFileSender(FileSender): +class CachingStaticResource(StaticResource): + """Static Resource handler that will add cache headers.""" + + @asyncio.coroutine + def _handle(self, request): + filename = unquote(request.match_info['filename']) + try: + # PyLint is wrong about resolve not being a member. + # pylint: disable=no-member + filepath = self._directory.joinpath(filename).resolve() + if not self._follow_symlinks: + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError) as error: + # relatively safe + raise HTTPNotFound() from error + except Exception as error: + # perm error or other kind! + request.app.logger.exception(error) + raise HTTPNotFound() from error + + if filepath.is_dir(): + return (yield from super()._handle(request)) + elif filepath.is_file(): + return CachingFileResponse(filepath, chunk_size=self._chunk_size) + else: + raise HTTPNotFound + + +class CachingFileResponse(FileResponse): """FileSender class that caches output if not in dev mode.""" def __init__(self, *args, **kwargs): @@ -20,46 +51,34 @@ class CachingFileSender(FileSender): orig_sendfile = self._sendfile @asyncio.coroutine - def sendfile(request, resp, fobj, count): + def sendfile(request, fobj, count): """Sendfile that includes a cache header.""" if not request.app[KEY_DEVELOPMENT]: cache_time = 31 * 86400 # = 1 month - resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( + self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( cache_time) - yield from orig_sendfile(request, resp, fobj, count) + yield from orig_sendfile(request, fobj, count) # Overwriting like this because __init__ can change implementation. self._sendfile = sendfile -FILE_SENDER = FileSender() -CACHING_FILE_SENDER = CachingFileSender() - - @asyncio.coroutine def staticresource_middleware(app, handler): - """Enhance StaticResourceHandler middleware. - - Adds gzip encoding and fingerprinting matching. - """ - inst = getattr(handler, '__self__', None) - if not isinstance(inst, StaticResource): - return handler - - # pylint: disable=protected-access - inst._file_sender = CACHING_FILE_SENDER - + """Middleware to strip out fingerprint from fingerprinted assets.""" @asyncio.coroutine def static_middleware_handler(request): """Strip out fingerprints from resource names.""" + if not request.path.startswith('/static/'): + return handler(request) + fingerprinted = _FINGERPRINT.match(request.match_info['filename']) if fingerprinted: request.match_info['filename'] = \ '{}.{}'.format(*fingerprinted.groups()) - resp = yield from handler(request) - return resp + return handler(request) return static_middleware_handler diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 7f8bd83116c..1ba8dfcfbc4 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -112,8 +112,6 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): params['image_bytes'] = str(b64encode(image), 'utf-8') - data = None - request = None try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): request = yield from websession.post( @@ -127,14 +125,10 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): request.status, data.get('error')) return - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for openalpr api.") return - finally: - if request is not None: - yield from request.release() - # processing api data vehicles = 0 result = {} diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a603cb9c3e3..511cb8208a5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -864,21 +864,17 @@ def _async_fetch_image(hass, url): content, content_type = (None, None) websession = async_get_clientsession(hass) - response = None try: with async_timeout.timeout(10, loop=hass.loop): response = yield from websession.get(url) - if response.status == 200: - content = yield from response.read() - content_type = response.headers.get(CONTENT_TYPE_HEADER) + + if response.status == 200: + content = yield from response.read() + content_type = response.headers.get(CONTENT_TYPE_HEADER) except asyncio.TimeoutError: pass - finally: - if response is not None: - yield from response.release() - if not content: return (None, None) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 6db376aa073..eec8eeb8177 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -117,7 +117,6 @@ class LogitechMediaServer(object): @asyncio.coroutine def async_query(self, *command, player=""): """Abstract out the JSON-RPC connection.""" - response = None auth = None if self._username is None else aiohttp.BasicAuth( self._username, self._password) url = "http://{}:{}/jsonrpc.js".format( @@ -138,22 +137,17 @@ class LogitechMediaServer(object): data=data, auth=auth) - if response.status == 200: - data = yield from response.json() - else: + if response.status != 200: _LOGGER.error( "Query failed, response code: %s Full message: %s", response.status, response) return False - except (asyncio.TimeoutError, - aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as error: + data = yield from response.json() + + except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed communicating with LMS: %s", type(error)) return False - finally: - if response is not None: - yield from response.release() try: return data['result'] diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index bcd4ebd0c11..5944921f94f 100755 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -68,7 +68,6 @@ class Volumio(MediaPlayerDevice): def send_volumio_msg(self, method, params=None): """Send message.""" url = "http://{}:{}/api/v1/{}/".format(self.host, self.port, method) - response = None _LOGGER.debug("URL: %s params: %s", url, params) @@ -83,14 +82,9 @@ class Volumio(MediaPlayerDevice): response.status, response) return False - except (asyncio.TimeoutError, - aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as error: + except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed communicating with Volumio: %s", type(error)) return False - finally: - if response is not None: - yield from response.release() try: return data diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index dc0e9001b24..5d98f04d01e 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -359,30 +359,25 @@ class MicrosoftFace(object): else: payload = None - response = None try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): response = yield from getattr(self.websession, method)( url, data=payload, headers=headers, params=params) answer = yield from response.json() - _LOGGER.debug("Read from microsoft face api: %s", answer) - if response.status == 200 or response.status == 202: - return answer - _LOGGER.warning("Error %d microsoft face api %s", - response.status, response.url) - raise HomeAssistantError(answer['error']['message']) + _LOGGER.debug("Read from microsoft face api: %s", answer) + if response.status < 300: + return answer - except (aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError): + _LOGGER.warning("Error %d microsoft face api %s", + response.status, response.url) + raise HomeAssistantError(answer['error']['message']) + + except aiohttp.ClientError: _LOGGER.warning("Can't connect to microsoft face api") except asyncio.TimeoutError: _LOGGER.warning("Timeout from microsoft face api %s", response.url) - finally: - if response is not None: - yield from response.release() - raise HomeAssistantError("Network error on microsoft face api.") diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index abf25981af6..7084339ab42 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -81,7 +81,6 @@ def async_setup(hass, config): template_payload.async_render(variables=service.data), 'utf-8') - request = None try: with async_timeout.timeout(timeout, loop=hass.loop): request = yield from getattr(websession, method)( @@ -90,22 +89,18 @@ def async_setup(hass, config): auth=auth ) - if request.status < 400: - _LOGGER.info("Success call %s.", request.url) - return - + if request.status < 400: + _LOGGER.info("Success call %s.", request.url) + else: _LOGGER.warning( "Error %d on call %s.", request.status, request.url) + except asyncio.TimeoutError: _LOGGER.warning("Timeout call %s.", request.url) - except aiohttp.errors.ClientError: + except aiohttp.ClientError: _LOGGER.error("Client error %s.", request.url) - finally: - if request is not None: - yield from request.release() - # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 047edd0b994..4306ad68553 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -151,6 +151,8 @@ class YrData(object): @asyncio.coroutine def async_update(self, *_): """Get the latest data from yr.no.""" + import xmltodict + def try_again(err: str): """Retry in 15 minutes.""" _LOGGER.warning('Retrying in 15 minutes: %s', err) @@ -161,7 +163,6 @@ class YrData(object): nxt) if self._nextrun is None or dt_util.utcnow() >= self._nextrun: - resp = None try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): @@ -172,17 +173,11 @@ class YrData(object): return text = yield from resp.text() - except (asyncio.TimeoutError, aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as err: + except (asyncio.TimeoutError, aiohttp.ClientError) as err: try_again(err) return - finally: - if resp is not None: - yield from resp.release() - try: - import xmltodict self.data = xmltodict.parse(text)['weatherdata'] model = self.data['meta']['model'] if '@nextrun' not in model: diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index a21d9814768..33b49a24caa 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -34,7 +34,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) websession = async_get_clientsession(hass) - response = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from websession.post( @@ -43,14 +42,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 'username': username, 'password': password}) data = yield from response.json() - except (asyncio.TimeoutError, - aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as error: + except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed authentication API call: %s", error) return False - finally: - if response is not None: - yield from response.release() try: token = data['data']['token'] @@ -58,21 +52,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No token. Check username and password") return False - response = None try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): response = yield from websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": data['data']['token']}) data = yield from response.json() - except (asyncio.TimeoutError, - aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as error: + except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed getting devices: %s", error) return False - finally: - if response is not None: - yield from response.release() async_add_devices( HookSmartHome( @@ -110,7 +98,6 @@ class HookSmartHome(SwitchDevice): @asyncio.coroutine def _send(self, url): """Send the url to the Hook API.""" - response = None try: _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) @@ -119,16 +106,10 @@ class HookSmartHome(SwitchDevice): url, params={"token": self._token}) data = yield from response.json() - except (asyncio.TimeoutError, - aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError) as error: + except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed setting state: %s", error) return False - finally: - if response is not None: - yield from response.release() - _LOGGER.debug("Got: %s", data) return data['return_value'] == '1' diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 74add400850..179b3b24068 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -57,20 +57,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): body_off.hass = hass timeout = config.get(CONF_TIMEOUT) - req = None try: with async_timeout.timeout(timeout, loop=hass.loop): req = yield from websession.get(resource) + + if req.status >= 400: + _LOGGER.error('Got non-ok response from resource: %s', req.status) + return False + except (TypeError, ValueError): _LOGGER.error("Missing resource or schema in configuration. " "Add http:// or https:// to your URL") return False - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("No route to resource/endpoint: %s", resource) return False - finally: - if req is not None: - yield from req.release() async_add_devices( [RestSwitch(hass, name, resource, body_on, body_off, @@ -108,17 +109,13 @@ class RestSwitch(SwitchDevice): body_on_t = self._body_on.async_render() websession = async_get_clientsession(self.hass) - request = None try: with async_timeout.timeout(self._timeout, loop=self.hass.loop): request = yield from websession.post( self._resource, data=bytes(body_on_t, 'utf-8')) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn on %s", self._resource) return - finally: - if request is not None: - yield from request.release() if request.status == 200: self._state = True @@ -132,17 +129,13 @@ class RestSwitch(SwitchDevice): body_off_t = self._body_off.async_render() websession = async_get_clientsession(self.hass) - request = None try: with async_timeout.timeout(self._timeout, loop=self.hass.loop): request = yield from websession.post( self._resource, data=bytes(body_off_t, 'utf-8')) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn off %s", self._resource) return - finally: - if request is not None: - yield from request.release() if request.status == 200: self._state = False @@ -155,17 +148,13 @@ class RestSwitch(SwitchDevice): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(self.hass) - request = None try: with async_timeout.timeout(self._timeout, loop=self.hass.loop): request = yield from websession.get(self._resource) text = yield from request.text() - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error while fetch data.") return - finally: - if request is not None: - yield from request.release() if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index be84e0e029b..d433aef8c47 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -95,7 +95,6 @@ class GoogleProvider(Provider): 'textlen': len(part), } - request = None try: with async_timeout.timeout(10, loop=self.hass.loop): request = yield from websession.get( @@ -109,14 +108,10 @@ class GoogleProvider(Provider): return (None, None) data += yield from request.read() - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for google speech.") return (None, None) - finally: - if request is not None: - yield from request.release() - return ("mp3", data) @staticmethod diff --git a/homeassistant/components/tts/voicerss.py b/homeassistant/components/tts/voicerss.py index ee50cc30cca..e8622d3b9a1 100644 --- a/homeassistant/components/tts/voicerss.py +++ b/homeassistant/components/tts/voicerss.py @@ -123,7 +123,6 @@ class VoiceRSSProvider(Provider): form_data['src'] = message form_data['hl'] = language - request = None try: with async_timeout.timeout(10, loop=self.hass.loop): request = yield from websession.post( @@ -141,12 +140,8 @@ class VoiceRSSProvider(Provider): "Error receive %s from VoiceRSS", str(data, 'utf-8')) return (None, None) - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for VoiceRSS API") return (None, None) - finally: - if request is not None: - yield from request.release() - return (self._extension, data) diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py index b60f9cab61e..fb95faf1ecf 100644 --- a/homeassistant/components/tts/yandextts.py +++ b/homeassistant/components/tts/yandextts.py @@ -98,10 +98,8 @@ class YandexSpeechKitProvider(Provider): def async_get_tts_audio(self, message, language, options=None): """Load TTS from yandex.""" websession = async_get_clientsession(self.hass) - actual_language = language - request = None try: with async_timeout.timeout(10, loop=self.hass.loop): url_param = { @@ -123,12 +121,8 @@ class YandexSpeechKitProvider(Provider): return (None, None) data = yield from request.read() - except (asyncio.TimeoutError, aiohttp.errors.ClientError): + except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for yandex speech kit api.") return (None, None) - finally: - if request is not None: - yield from request.release() - return (self._codec, data) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 75755ad3685..8dc64105e8b 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -5,7 +5,7 @@ import sys import aiohttp from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout +from aiohttp.web_exceptions import HTTPGatewayTimeout, HTTPBadGateway import async_timeout from homeassistant.core import callback @@ -71,42 +71,46 @@ def async_create_clientsession(hass, verify_ssl=True, auto_cleanup=True, @asyncio.coroutine -def async_aiohttp_proxy_stream(hass, request, stream_coro, buffer_size=102400, - timeout=10): +def async_aiohttp_proxy_web(hass, request, web_coro, buffer_size=102400, + timeout=10): """Stream websession request to aiohttp web response.""" - response = None - stream = None - try: with async_timeout.timeout(timeout, loop=hass.loop): - stream = yield from stream_coro + req = yield from web_coro - response = web.StreamResponse() - response.content_type = stream.headers.get(CONTENT_TYPE) + except asyncio.TimeoutError as err: + raise HTTPGatewayTimeout() from err - yield from response.prepare(request) + except aiohttp.ClientError as err: + raise HTTPBadGateway() from err + yield from async_aiohttp_proxy_stream(hass, request, req.content, + req.headers.get(CONTENT_TYPE)) + + +@asyncio.coroutine +def async_aiohttp_proxy_stream(hass, request, stream, content_type, + buffer_size=102400, timeout=10): + """Stream a stream to aiohttp web response.""" + response = web.StreamResponse() + response.content_type = content_type + yield from response.prepare(request) + + try: while True: - data = yield from stream.content.read(buffer_size) + with async_timeout.timeout(timeout, loop=hass.loop): + data = yield from stream.read(buffer_size) + if not data: + yield from response.write_eof() break + response.write(data) - except asyncio.TimeoutError: - raise HTTPGatewayTimeout() - - except (aiohttp.errors.ClientError, - aiohttp.errors.ClientDisconnectedError): + except (asyncio.TimeoutError, aiohttp.ClientError): pass - except (asyncio.CancelledError, ConnectionResetError): - response = None - - finally: - if stream is not None: - stream.close() - if response is not None: - yield from response.write_eof() + yield from response.write_eof() @callback @@ -149,10 +153,10 @@ def _async_get_connector(hass, verify_ssl=True): connector = hass.data[DATA_CONNECTOR_NOTVERIFY] if is_new: - @asyncio.coroutine + @callback def _async_close_connector(event): """Close connector pool.""" - yield from connector.close() + connector.close() hass.bus.async_listen_once( EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc8280bd6c5..2b22a367121 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,5 +5,5 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==1.3.5 +aiohttp==2.0.4 async_timeout==1.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d97e830874c..29ef28c5ed7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=7.1.0 jinja2>=2.9.5 voluptuous==0.9.3 typing>=3,<4 -aiohttp==1.3.5 +aiohttp==2.0.4 async_timeout==1.2.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 0f2c6e6d876..966ae72bc88 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ REQUIRES = [ 'jinja2>=2.9.5', 'voluptuous==0.9.3', 'typing>=3,<4', - 'aiohttp==1.3.5', + 'aiohttp==2.0.4', 'async_timeout==1.2.0', ] diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 54453f42d43..7ee04b0df4c 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -36,10 +36,8 @@ class TestDarkSkySetup(unittest.TestCase): 'monitored_conditions': ['summary', 'icon', 'temperature_max'], 'update_interval': timedelta(seconds=120), } - self.lat = 37.8267 - self.lon = -122.423 - self.hass.config.latitude = self.lat - self.hass.config.longitude = self.lon + self.lat = self.hass.config.latitude = 37.8267 + self.lon = self.hass.config.longitude = -122.423 self.entities = [] def tearDown(self): # pylint: disable=invalid-name @@ -51,11 +49,6 @@ class TestDarkSkySetup(unittest.TestCase): self.assertTrue( setup_component(self.hass, 'sensor', {'darksky': self.config})) - def test_setup_no_latitude(self): - """Test that the component is not loaded without required config.""" - self.hass.config.latitude = None - self.assertFalse(darksky.setup_platform(self.hass, {}, MagicMock())) - @patch('forecastio.api.get_forecast') def test_setup_bad_api_key(self, mock_get_forecast): """Test for handling a bad API key.""" diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index a2da94b614a..5a9ce679edf 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -42,7 +42,7 @@ class TestRestSwitchSetup: def test_setup_failed_connect(self, aioclient_mock): """Test setup when connection error occurs.""" - aioclient_mock.get('http://localhost', exc=aiohttp.errors.ClientError) + aioclient_mock.get('http://localhost', exc=aiohttp.ClientError) assert not run_coroutine_threadsafe( rest.async_setup_platform(self.hass, { 'platform': 'rest', diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 2cdc359bfea..e2d93c9cce7 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,12 +1,9 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access -import asyncio from contextlib import closing import json import unittest -from unittest.mock import Mock, patch -from aiohttp import web import requests from homeassistant import setup, const @@ -247,22 +244,6 @@ class TestAPI(unittest.TestCase): headers=HA_HEADERS) self.assertEqual(hass.config.components, set(req.json())) - def test_api_get_error_log(self): - """Test the return of the error log.""" - test_string = 'Test StringĀ°' - - @asyncio.coroutine - def mock_send(): - """Mock file send.""" - return web.Response(text=test_string) - - with patch('homeassistant.components.http.HomeAssistantView.file', - Mock(return_value=mock_send())): - req = requests.get(_url(const.URL_API_ERROR_LOG), - headers=HA_HEADERS) - self.assertEqual(test_string, req.text) - self.assertIsNone(req.headers.get('expires')) - def test_api_get_event_listeners(self): """Test if we can get the list of events being listened for.""" req = requests.get(_url(const.URL_API_EVENTS), diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 0c42d05f3ae..952061be3c2 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,89 +1,69 @@ """The tests for Home Assistant frontend.""" -# pylint: disable=protected-access +import asyncio import re -import unittest -import requests +import pytest -from homeassistant import setup -from homeassistant.components import http -from homeassistant.const import HTTP_HEADER_HA_AUTH - -from tests.common import get_test_instance_port, get_test_home_assistant - -API_PASSWORD = "test1234" -SERVER_PORT = get_test_instance_port() -HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) -HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} - -hass = None +from homeassistant.setup import async_setup_component -def _url(path=""): - """Helper method to generate URLs.""" - return HTTP_BASE_URL + path +@pytest.fixture +def mock_http_client(loop, hass, test_client): + """Start the Hass HTTP component.""" + loop.run_until_complete(async_setup_component(hass, 'frontend', {})) + return loop.run_until_complete(test_client(hass.http.app)) -# pylint: disable=invalid-name -def setUpModule(): - """Initialize a Home Assistant server.""" - global hass +@asyncio.coroutine +def test_frontend_and_static(mock_http_client): + """Test if we can get the frontend.""" + resp = yield from mock_http_client.get('') + assert resp.status == 200 + assert 'cache-control' not in resp.headers - hass = get_test_home_assistant() + text = yield from resp.text() - assert setup.setup_component( - hass, http.DOMAIN, - {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: SERVER_PORT}}) + # Test we can retrieve frontend.js + frontendjs = re.search( + r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)', text) - assert setup.setup_component(hass, 'frontend') - - hass.start() + assert frontendjs is not None + resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) + assert resp.status == 200 + assert 'public' in resp.headers.get('cache-control') -# pylint: disable=invalid-name -def tearDownModule(): - """Stop everything that was started.""" - hass.stop() +@asyncio.coroutine +def test_dont_cache_service_worker(mock_http_client): + """Test that we don't cache the service worker.""" + resp = yield from mock_http_client.get('/service_worker.js') + assert resp.status == 200 + assert 'cache-control' not in resp.headers -class TestFrontend(unittest.TestCase): - """Test the frontend.""" +@asyncio.coroutine +def test_404(mock_http_client): + """Test for HTTP 404 error.""" + resp = yield from mock_http_client.get('/not-existing') + assert resp.status == 404 - def tearDown(self): - """Stop everything that was started.""" - hass.block_till_done() - def test_frontend_and_static(self): - """Test if we can get the frontend.""" - req = requests.get(_url("")) - self.assertEqual(200, req.status_code) +@asyncio.coroutine +def test_we_cannot_POST_to_root(mock_http_client): + """Test that POST is not allow to root.""" + resp = yield from mock_http_client.post('/') + assert resp.status == 405 - # Test we can retrieve frontend.js - frontendjs = re.search( - r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)', - req.text) - self.assertIsNotNone(frontendjs) - req = requests.get(_url(frontendjs.groups(0)[0])) - self.assertEqual(200, req.status_code) +@asyncio.coroutine +def test_states_routes(hass, mock_http_client): + """All served by index.""" + resp = yield from mock_http_client.get('/states') + assert resp.status == 200 - def test_404(self): - """Test for HTTP 404 error.""" - self.assertEqual(404, requests.get(_url("/not-existing")).status_code) + resp = yield from mock_http_client.get('/states/group.non_existing') + assert resp.status == 404 - def test_we_cannot_POST_to_root(self): - """Test that POST is not allow to root.""" - self.assertEqual(405, requests.post(_url("")).status_code) - - def test_states_routes(self): - """All served by index.""" - req = requests.get(_url("/states")) - self.assertEqual(200, req.status_code) - - req = requests.get(_url("/states/group.non_existing")) - self.assertEqual(404, req.status_code) - - hass.states.set('group.existing', 'on', {'view': True}) - req = requests.get(_url("/states/group.existing")) - self.assertEqual(200, req.status_code) + hass.states.async_set('group.existing', 'on', {'view': True}) + resp = yield from mock_http_client.get('/states/group.existing') + assert resp.status == 200 diff --git a/tests/components/test_rest_command.py b/tests/components/test_rest_command.py index a62bddc4a0f..0648a30c922 100644 --- a/tests/components/test_rest_command.py +++ b/tests/components/test_rest_command.py @@ -107,7 +107,7 @@ class TestRestCommandComponent(object): with assert_setup_component(4): setup_component(self.hass, rc.DOMAIN, self.config) - aioclient_mock.get(self.url, exc=aiohttp.errors.ClientError()) + aioclient_mock.get(self.url, exc=aiohttp.ClientError()) self.hass.services.call(rc.DOMAIN, 'get_test', {}) self.hass.block_till_done() diff --git a/tests/components/test_sleepiq.py b/tests/components/test_sleepiq.py index 3c7551d130c..becf897a8e9 100644 --- a/tests/components/test_sleepiq.py +++ b/tests/components/test_sleepiq.py @@ -1,5 +1,7 @@ """The tests for the SleepIQ component.""" import unittest +from unittest.mock import MagicMock, patch + import requests_mock from homeassistant import setup @@ -49,8 +51,12 @@ class TestSleepIQ(unittest.TestCase): """Test the setup.""" mock_responses(mock) - response = sleepiq.setup(self.hass, self.config) - self.assertTrue(response) + # We're mocking the load_platform discoveries or else the platforms + # will be setup during tear down when blocking till done, but the mocks + # are no longer active. + with patch( + 'homeassistant.helpers.discovery.load_platform', MagicMock()): + assert sleepiq.setup(self.hass, self.config) @requests_mock.Mocker() def test_setup_login_failed(self, mock): diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 42e2697b7a7..7aa7f6fa4d1 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -5,7 +5,7 @@ import unittest import aiohttp from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component import homeassistant.helpers.aiohttp_client as client from homeassistant.util.async import run_callback_threadsafe @@ -119,22 +119,19 @@ class TestHelpersAiohttpClient(unittest.TestCase): @asyncio.coroutine -def test_fetching_url(aioclient_mock, hass, test_client): +def test_async_aiohttp_proxy_stream(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com/mjpeg_stream', content=[ b'Frame1', b'Frame2', b'Frame3' ]) - def setup_platform(): - """Setup the platform.""" - assert setup_component(hass, 'camera', { - 'camera': { - 'name': 'config_test', - 'platform': 'mjpeg', - 'mjpeg_url': 'http://example.com/mjpeg_stream', - }}) - - yield from hass.loop.run_in_executor(None, setup_platform) + result = yield from async_setup_component(hass, 'camera', { + 'camera': { + 'name': 'config_test', + 'platform': 'mjpeg', + 'mjpeg_url': 'http://example.com/mjpeg_stream', + }}) + assert result, 'Failed to setup camera' client = yield from test_client(hass.http.app) @@ -151,29 +148,12 @@ def test_fetching_url(aioclient_mock, hass, test_client): content=[b'Frame1', b'Frame2', b'Frame3']) resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') - - assert resp.status == 200 - body = yield from resp.text() - assert body == '' + assert resp.status == 504 aioclient_mock.clear_requests() aioclient_mock.get( - 'http://example.com/mjpeg_stream', exc=asyncio.CancelledError(), + 'http://example.com/mjpeg_stream', exc=aiohttp.ClientError(), content=[b'Frame1', b'Frame2', b'Frame3']) resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') - - assert resp.status == 200 - body = yield from resp.text() - assert body == '' - - aioclient_mock.clear_requests() - aioclient_mock.get( - 'http://example.com/mjpeg_stream', exc=aiohttp.errors.ClientError(), - content=[b'Frame1', b'Frame2', b'Frame3']) - - resp = yield from client.get('/api/camera_proxy_stream/camera.config_test') - - assert resp.status == 200 - body = yield from resp.text() - assert body == '' + assert resp.status == 502