Drop Python 3.6 support (#29978)

This commit is contained in:
Ville Skyttä 2019-12-16 08:29:19 +02:00 committed by Paulus Schoutsen
parent bfafa77016
commit 445fd15f76
15 changed files with 19 additions and 135 deletions

View File

@ -5,7 +5,6 @@ omit =
homeassistant/__main__.py homeassistant/__main__.py
homeassistant/helpers/signal.py homeassistant/helpers/signal.py
homeassistant/helpers/typing.py homeassistant/helpers/typing.py
homeassistant/monkey_patch.py
homeassistant/scripts/*.py homeassistant/scripts/*.py
homeassistant/util/async.py homeassistant/util/async.py

View File

@ -4,7 +4,7 @@ build:
image: latest image: latest
python: python:
version: 3.6 version: 3.7
setup_py_install: true setup_py_install: true
requirements_file: requirements_docs.txt requirements_file: requirements_docs.txt

View File

@ -14,15 +14,13 @@ addons:
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: "3.6.1" - python: "3.7.0"
env: TOXENV=lint env: TOXENV=lint
- python: "3.6.1" - python: "3.7.0"
env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30
- python: "3.6.1" - python: "3.7.0"
env: TOXENV=typing env: TOXENV=typing
- python: "3.6.1" - python: "3.7.0"
env: TOXENV=py36
- python: "3.7"
env: TOXENV=py37 env: TOXENV=py37
cache: cache:

View File

@ -14,8 +14,6 @@ pr:
resources: resources:
containers: containers:
- container: 36
image: homeassistant/ci-azure:3.6
- container: 37 - container: 37
image: homeassistant/ci-azure:3.7 image: homeassistant/ci-azure:3.7
repositories: repositories:
@ -25,7 +23,7 @@ resources:
endpoint: 'home-assistant' endpoint: 'home-assistant'
variables: variables:
- name: PythonMain - name: PythonMain
value: '36' value: '37'
- group: codecov - group: codecov
stages: stages:
@ -108,8 +106,6 @@ stages:
strategy: strategy:
maxParallel: 3 maxParallel: 3
matrix: matrix:
Python36:
python.container: '36'
Python37: Python37:
python.container: '37' python.container: '37'
container: $[ variables['python.container'] ] container: $[ variables['python.container'] ]

View File

@ -1,5 +1,6 @@
"""Start Home Assistant.""" """Start Home Assistant."""
import argparse import argparse
import asyncio
import os import os
import platform import platform
import subprocess import subprocess
@ -7,7 +8,6 @@ import sys
import threading import threading
from typing import TYPE_CHECKING, Any, Dict, List from typing import TYPE_CHECKING, Any, Dict, List
from homeassistant import monkey_patch
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
if TYPE_CHECKING: if TYPE_CHECKING:
@ -16,7 +16,6 @@ if TYPE_CHECKING:
def set_loop() -> None: def set_loop() -> None:
"""Attempt to use different loop.""" """Attempt to use different loop."""
import asyncio
from asyncio.events import BaseDefaultEventLoopPolicy from asyncio.events import BaseDefaultEventLoopPolicy
if sys.platform == "win32": if sys.platform == "win32":
@ -345,11 +344,6 @@ def main() -> int:
"""Start Home Assistant.""" """Start Home Assistant."""
validate_python() validate_python()
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
set_loop() set_loop()
# Run a simple daemon runner process on Windows to handle restarts # Run a simple daemon runner process on Windows to handle restarts
@ -383,13 +377,11 @@ def main() -> int:
if args.pid_file: if args.pid_file:
write_pid(args.pid_file) write_pid(args.pid_file)
from homeassistant.util.async_ import asyncio_run exit_code = asyncio.run(setup_and_run_hass(config_dir, args))
exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
if exit_code == RESTART_EXIT_CODE and not args.runner: if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart() try_to_restart()
return exit_code # type: ignore return exit_code
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,10 +4,10 @@ MINOR_VERSION = 104
PATCH_VERSION = "0.dev0" PATCH_VERSION = "0.dev0"
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 6, 1) REQUIRED_PYTHON_VER = (3, 7, 0)
# Truthy date string triggers showing related deprecation warning messages. # Truthy date string triggers showing related deprecation warning messages.
REQUIRED_NEXT_PYTHON_VER = (3, 7, 0) REQUIRED_NEXT_PYTHON_VER = (3, 8, 0)
REQUIRED_NEXT_PYTHON_DATE = "December 15, 2019" REQUIRED_NEXT_PYTHON_DATE = ""
# Format for platform files # Format for platform files
PLATFORM_FORMAT = "{platform}.{domain}" PLATFORM_FORMAT = "{platform}.{domain}"

View File

@ -1,73 +0,0 @@
"""Monkey patch Python to work around issues causing segfaults.
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.
Related Python bugs:
- https://bugs.python.org/issue26617
"""
import sys
from typing import Any
def patch_weakref_tasks() -> None:
"""Replace weakref.WeakSet to address Python 3 bug."""
# pylint: disable=no-self-use, protected-access
import asyncio.tasks
class IgnoreCalls:
"""Ignore add calls."""
def add(self, other: Any) -> None:
"""No-op add."""
return
asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore
try:
del asyncio.tasks.Task.__del__
except: # noqa: E722 pylint: disable=bare-except
pass
def disable_c_asyncio() -> None:
"""Disable using C implementation of asyncio.
Required to be able to apply the weakref monkey patch.
Requires Python 3.6+.
"""
class AsyncioImportFinder:
"""Finder that blocks C version of asyncio being loaded."""
PATH_TRIGGER = "_asyncio"
def __init__(self, path_entry: str) -> None:
if path_entry != self.PATH_TRIGGER:
raise ImportError()
def find_module(self, fullname: str, path: Any = None) -> None:
"""Find a module."""
if fullname == self.PATH_TRIGGER:
raise ModuleNotFoundError()
sys.path_hooks.append(AsyncioImportFinder)
sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER)
try:
import _asyncio # noqa: F401
except ImportError:
pass

View File

@ -7,7 +7,6 @@ async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2019.11.28 certifi>=2019.11.28
contextvars==2.4;python_version<"3.7"
cryptography==2.8 cryptography==2.8
defusedxml==0.6.0 defusedxml==0.6.0
distro==1.4.0 distro==1.4.0
@ -29,7 +28,7 @@ zeroconf==0.24.0
pycryptodome>=3.6.6 pycryptodome>=3.6.6
# Breaks Python 3.6 and is not needed for our supported Python versions # Not needed for our supported Python versions
enum34==1000000000.0.0 enum34==1000000000.0.0
# This is a old unmaintained library and is replaced with pycryptodome # This is a old unmaintained library and is replaced with pycryptodome

View File

@ -1,33 +1,14 @@
"""Asyncio backports for Python 3.6 compatibility.""" """Asyncio backports for Python 3.6 compatibility."""
import asyncio
from asyncio import coroutines, ensure_future from asyncio import coroutines, ensure_future
from asyncio.events import AbstractEventLoop from asyncio.events import AbstractEventLoop
import concurrent.futures import concurrent.futures
import logging import logging
import threading import threading
from typing import Any, Awaitable, Callable, Coroutine, TypeVar from typing import Any, Callable, Coroutine
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
try:
# pylint: disable=invalid-name
asyncio_run = asyncio.run # type: ignore
except AttributeError:
_T = TypeVar("_T")
def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T:
"""Minimal re-implementation of asyncio.run (since 3.7)."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(debug)
try:
return loop.run_until_complete(main)
finally:
asyncio.set_event_loop(None)
loop.close()
def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None:
"""Submit a coroutine object to a given event loop. """Submit a coroutine object to a given event loop.

View File

@ -1,3 +1,3 @@
[tool.black] [tool.black]
target-version = ["py36", "py37", "py38"] target-version = ["py37", "py38"]
exclude = 'generated' exclude = 'generated'

View File

@ -5,7 +5,6 @@ async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2019.11.28 certifi>=2019.11.28
contextvars==2.4;python_version<"3.7"
importlib-metadata==0.23 importlib-metadata==0.23
jinja2>=2.10.3 jinja2>=2.10.3
PyJWT==1.7.1 PyJWT==1.7.1

View File

@ -58,7 +58,7 @@ CONSTRAINT_PATH = os.path.join(
CONSTRAINT_BASE = """ CONSTRAINT_BASE = """
pycryptodome>=3.6.6 pycryptodome>=3.6.6
# Breaks Python 3.6 and is not needed for our supported Python versions # Not needed for our supported Python versions
enum34==1000000000.0.0 enum34==1000000000.0.0
# This is a old unmaintained library and is replaced with pycryptodome # This is a old unmaintained library and is replaced with pycryptodome

View File

@ -11,7 +11,6 @@ classifier =
Intended Audience :: Developers Intended Audience :: Developers
License :: OSI Approved :: Apache Software License License :: OSI Approved :: Apache Software License
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Topic :: Home Automation Topic :: Home Automation
@ -57,7 +56,7 @@ forced_separate = tests
combine_as_imports = true combine_as_imports = true
[mypy] [mypy]
python_version = 3.6 python_version = 3.7
ignore_errors = true ignore_errors = true
follow_imports = silent follow_imports = silent
ignore_missing_imports = true ignore_missing_imports = true
@ -65,7 +64,7 @@ warn_incomplete_stub = true
warn_redundant_casts = true warn_redundant_casts = true
warn_unused_configs = true warn_unused_configs = true
[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] [mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*]
ignore_errors = false ignore_errors = false
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

@ -38,7 +38,6 @@ REQUIRES = [
"attrs==19.3.0", "attrs==19.3.0",
"bcrypt==3.1.7", "bcrypt==3.1.7",
"certifi>=2019.11.28", "certifi>=2019.11.28",
'contextvars==2.4;python_version<"3.7"',
"importlib-metadata==0.23", "importlib-metadata==0.23",
"jinja2>=2.10.3", "jinja2>=2.10.3",
"PyJWT==1.7.1", "PyJWT==1.7.1",

View File

@ -1,6 +1,5 @@
"""Tests for async util methods from Python source.""" """Tests for async util methods from Python source."""
import asyncio import asyncio
import sys
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -112,11 +111,7 @@ class RunThreadsafeTests(TestCase):
"""Wait 0.05 second and return a + b.""" """Wait 0.05 second and return a + b."""
yield from asyncio.sleep(0.05, loop=self.loop) yield from asyncio.sleep(0.05, loop=self.loop)
if cancel: if cancel:
if sys.version_info[:2] >= (3, 7): asyncio.current_task(self.loop).cancel()
current_task = asyncio.current_task
else:
current_task = asyncio.tasks.Task.current_task
current_task(self.loop).cancel()
yield yield
return self.add_callback(a, b, fail, invalid) return self.add_callback(a, b, fail, invalid)