Use Python 3.14 as default one (#161426)

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Robert Resch
2026-01-28 23:48:27 +01:00
committed by GitHub
parent 7368b9ca1d
commit c875b75272
24 changed files with 42 additions and 48 deletions

View File

@@ -10,12 +10,12 @@ on:
env: env:
BUILD_TYPE: core BUILD_TYPE: core
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.14.2"
PIP_TIMEOUT: 60 PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true" UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker # Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2025.12.0" BASE_IMAGE_VERSION: "2026.01.0"
ARCHITECTURES: '["amd64", "aarch64"]' ARCHITECTURES: '["amd64", "aarch64"]'
jobs: jobs:

View File

@@ -41,8 +41,8 @@ env:
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.3" HA_SHORT_VERSION: "2026.3"
DEFAULT_PYTHON: "3.13.11" DEFAULT_PYTHON: "3.14.2"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']" ALL_PYTHON_VERSIONS: "['3.14.2']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support # 10.6 is the current long-term-support

View File

@@ -10,7 +10,7 @@ on:
- "**strings.json" - "**strings.json"
env: env:
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.14.2"
jobs: jobs:
upload: upload:

View File

@@ -17,7 +17,7 @@ on:
- "script/gen_requirements_all.py" - "script/gen_requirements_all.py"
env: env:
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.14.2"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}} group: ${{ github.workflow }}-${{ github.ref_name}}

View File

@@ -1 +1 @@
3.13 3.14

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from concurrent.futures.thread import _threads_queues, _worker from concurrent.futures.thread import _threads_queues, _worker
import sys
import threading import threading
from typing import Any from typing import Any
import weakref import weakref
@@ -54,17 +53,10 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor):
) -> None: ) -> None:
q.put(None) q.put(None)
if sys.version_info >= (3, 14): additional_args = (
additional_args = ( self._create_worker_context(),
self._create_worker_context(), self._work_queue,
self._work_queue, )
)
else:
additional_args = (
self._work_queue,
self._initializer,
self._initargs,
)
num_threads = len(self._threads) num_threads = len(self._threads)
if num_threads < self._max_workers: if num_threads < self._max_workers:

View File

@@ -260,7 +260,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP:
ipv4: list[IPv4Address] = [] ipv4: list[IPv4Address] = []
if not network.async_only_default_interface_enabled(adapters): if not network.async_only_default_interface_enabled(adapters):
ipv4.extend( ipv4.extend(
address cast(IPv4Address, address)
for address in await network.async_get_enabled_source_ips(hass) for address in await network.async_get_enabled_source_ips(hass)
if address.version == 4 if address.version == 4
and not ( and not (

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from typing import cast
from homeassistant.components import network from homeassistant.components import network
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -15,5 +16,8 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A
for source_ip in await network.async_get_enabled_source_ips(hass) for source_ip in await network.async_get_enabled_source_ips(hass)
if not source_ip.is_loopback if not source_ip.is_loopback
and not source_ip.is_global and not source_ip.is_global
and ((source_ip.version == 6 and source_ip.scope_id) or source_ip.version == 4) and (
(source_ip.version == 6 and cast(IPv6Address, source_ip).scope_id)
or source_ip.version == 4
)
} }

View File

@@ -6,9 +6,9 @@ import asyncio
from collections.abc import Callable, Coroutine, Mapping from collections.abc import Callable, Coroutine, Mapping
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from ipaddress import IPv4Address from ipaddress import IPv4Address, IPv6Address
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, cast
from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.aiohttp import AiohttpSessionRequester
from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource
@@ -260,6 +260,7 @@ class Scanner:
for source_ip in await async_build_source_set(self.hass): for source_ip in await async_build_source_set(self.hass):
source_ip_str = str(source_ip) source_ip_str = str(source_ip)
if source_ip.version == 6: if source_ip.version == 6:
source_ip = cast(IPv6Address, source_ip)
assert source_ip.scope_id is not None assert source_ip.scope_id is not None
source_tuple: AddressTupleVXType = ( source_tuple: AddressTupleVXType = (
source_ip_str, source_ip_str,

View File

@@ -4,10 +4,11 @@ from __future__ import annotations
import asyncio import asyncio
from contextlib import ExitStack from contextlib import ExitStack
from ipaddress import IPv6Address
import logging import logging
import socket import socket
from time import time from time import time
from typing import Any from typing import Any, cast
from urllib.parse import urljoin from urllib.parse import urljoin
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@@ -171,6 +172,7 @@ class Server:
for source_ip in await async_build_source_set(self.hass): for source_ip in await async_build_source_set(self.hass):
source_ip_str = str(source_ip) source_ip_str = str(source_ip)
if source_ip.version == 6: if source_ip.version == 6:
source_ip = cast(IPv6Address, source_ip)
assert source_ip.scope_id is not None assert source_ip.scope_id is not None
source_tuple: AddressTupleVXType = ( source_tuple: AddressTupleVXType = (
source_ip_str, source_ip_str,

View File

@@ -165,7 +165,7 @@ def number(
attribute: str, attribute: str,
minimum: float | None = None, minimum: float | None = None,
maximum: float | None = None, maximum: float | None = None,
return_type: type[float] | type[int] = float, return_type: type[float | int] = float,
**kwargs: Any, **kwargs: Any,
) -> Callable[[Any], float | int | None]: ) -> Callable[[Any], float | int | None]:
"""Convert the result to a number (float or int). """Convert the result to a number (float or int).

View File

@@ -47,7 +47,7 @@ class VeluxEntity(Entity):
_attr_should_poll = False _attr_should_poll = False
_attr_has_entity_name = True _attr_has_entity_name = True
update_callback: Callable[["Node"], Awaitable[None]] | None = None update_callback: Callable[[Node], Awaitable[None]] | None = None
_attr_available = True _attr_available = True
_unavailable_logged = False _unavailable_logged = False

View File

@@ -20,7 +20,7 @@ MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0.dev0" PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
# Format for platform files # Format for platform files
PLATFORM_FORMAT: Final = "{platform}.{domain}" PLATFORM_FORMAT: Final = "{platform}.{domain}"

View File

@@ -173,7 +173,7 @@ class RuntimeConfig:
safe_mode: bool = False safe_mode: bool = False
class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[name-defined,misc]
"""Event loop policy for Home Assistant.""" """Event loop policy for Home Assistant."""
def __init__(self, debug: bool) -> None: def __init__(self, debug: bool) -> None:
@@ -184,7 +184,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
@property @property
def loop_name(self) -> str: def loop_name(self) -> str:
"""Return name of the loop.""" """Return name of the loop."""
return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined] return self._loop_factory.__name__ # type: ignore[no-any-return]
def new_event_loop(self) -> asyncio.AbstractEventLoop: def new_event_loop(self) -> asyncio.AbstractEventLoop:
"""Get the event loop.""" """Get the event loop."""
@@ -281,7 +281,7 @@ def run(runtime_config: RuntimeConfig) -> int:
"""Run Home Assistant.""" """Run Home Assistant."""
_enable_posix_spawn() _enable_posix_spawn()
set_open_file_descriptor_limit() set_open_file_descriptor_limit()
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) # type: ignore[deprecated]
# Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out # Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
try: try:

View File

@@ -61,7 +61,7 @@ def run(args: list[str]) -> int:
print("Aborting script, could not install dependency", req) print("Aborting script, could not install dependency", req)
return 1 return 1
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated]
return script.run(args[1:]) return script.run(args[1:])

View File

@@ -48,7 +48,7 @@ def run(args: Sequence[str] | None) -> None:
parser_change_pw.add_argument("new_password", type=str) parser_change_pw.add_argument("new_password", type=str)
parser_change_pw.set_defaults(func=change_password) parser_change_pw.set_defaults(func=change_password)
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated]
asyncio.run(run_command(parser.parse_args(args))) asyncio.run(run_command(parser.parse_args(args)))

View File

@@ -36,7 +36,7 @@ def run(args):
args = parser.parse_args() args = parser.parse_args()
bench = BENCHMARKS[args.name] bench = BENCHMARKS[args.name]
print("Using event loop:", asyncio.get_event_loop_policy().loop_name) print("Using event loop:", asyncio.get_event_loop_policy().loop_name) # type: ignore[deprecated]
with suppress(KeyboardInterrupt): with suppress(KeyboardInterrupt):
while True: while True:

View File

@@ -6,15 +6,11 @@ derived from EntityDescription and sub classes thereof.
from __future__ import annotations from __future__ import annotations
from annotationlib import Format, get_annotations
import dataclasses import dataclasses
import sys import sys
from typing import TYPE_CHECKING, Any, cast, dataclass_transform from typing import TYPE_CHECKING, Any, cast, dataclass_transform
if sys.version_info >= (3, 14):
from annotationlib import Format, get_annotations
else:
from typing_extensions import Format, get_annotations
if TYPE_CHECKING: if TYPE_CHECKING:
from _typeshed import DataclassInstance from _typeshed import DataclassInstance
@@ -103,7 +99,7 @@ class FrozenOrThawed(type):
continue continue
annotations |= get_annotations(parent, format=Format.FORWARDREF) annotations |= get_annotations(parent, format=Format.FORWARDREF)
if "__annotations__" in cls.__dict__ or sys.version_info < (3, 14): if "__annotations__" in cls.__dict__:
cls.__annotations__ = annotations cls.__annotations__ = annotations
else: else:

2
mypy.ini generated
View File

@@ -3,7 +3,7 @@
# To update, run python3 -m script.hassfest -p mypy_config # To update, run python3 -m script.hassfest -p mypy_config
[mypy] [mypy]
python_version = 3.13 python_version = 3.14
platform = linux platform = linux
plugins = pydantic.mypy, pydantic.v1.mypy plugins = pydantic.mypy, pydantic.v1.mypy
show_error_codes = true show_error_codes = true

View File

@@ -18,11 +18,10 @@ classifiers = [
"Intended Audience :: End Users/Desktop", "Intended Audience :: End Users/Desktop",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14",
"Topic :: Home Automation", "Topic :: Home Automation",
] ]
requires-python = ">=3.13.2" requires-python = ">=3.14.2"
dependencies = [ dependencies = [
"aiodns==4.0.0", "aiodns==4.0.0",
# Integrations may depend on hassio integration without listing it to # Integrations may depend on hassio integration without listing it to
@@ -102,7 +101,7 @@ include-package-data = true
include = ["homeassistant*"] include = ["homeassistant*"]
[tool.pylint.MAIN] [tool.pylint.MAIN]
py-version = "3.13" py-version = "3.14"
# Use a conservative default here; 2 should speed up most setups and not hurt # Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate. # any too bad. Override on command line as appropriate.
jobs = 2 jobs = 2

View File

@@ -80,7 +80,7 @@ WORKDIR /config
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest. _HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
# #
# To update, run python3 -m script.hassfest -p docker # To update, run python3 -m script.hassfest -p docker
FROM python:3.13-alpine FROM python:3.14-alpine
ENV \ ENV \
UV_SYSTEM_PYTHON=true \ UV_SYSTEM_PYTHON=true \

View File

@@ -1,7 +1,7 @@
# Automatically generated by hassfest. # Automatically generated by hassfest.
# #
# To update, run python3 -m script.hassfest -p docker # To update, run python3 -m script.hassfest -p docker
FROM python:3.13-alpine FROM python:3.14-alpine
ENV \ ENV \
UV_SYSTEM_PYTHON=true \ UV_SYSTEM_PYTHON=true \

View File

@@ -380,7 +380,7 @@ async def test_cloud_heater(
before_attrs: dict, before_attrs: dict,
service_name: str, service_name: str,
service_params: dict, service_params: dict,
effect: "contextlib.AbstractContextManager", effect: contextlib.AbstractContextManager,
heater_control_calls: list, heater_control_calls: list,
heater_set_temp_calls: list, heater_set_temp_calls: list,
after_state: HVACMode, after_state: HVACMode,
@@ -533,7 +533,7 @@ async def test_local_heater(
before_attrs: dict, before_attrs: dict,
service_name: str, service_name: str,
service_params: dict, service_params: dict,
effect: "contextlib.AbstractContextManager", effect: contextlib.AbstractContextManager,
heater_mode_set_individually_calls: list, heater_mode_set_individually_calls: list,
heater_mode_set_off_calls: list, heater_mode_set_off_calls: list,
heater_set_target_temperature_calls: list, heater_set_target_temperature_calls: list,

View File

@@ -139,7 +139,7 @@ async def test_sensors(hass: HomeAssistant) -> None:
class CoordinatorStub: class CoordinatorStub:
"""Coordinator stub for testing entity restoration behavior.""" """Coordinator stub for testing entity restoration behavior."""
instances: list["CoordinatorStub"] = [] instances: list[CoordinatorStub] = []
def __init__( def __init__(
self, self,