From aeb4e63950fae893ba78d5bd0dc901ae2f65c034 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Thu, 22 May 2025 03:21:43 +0200 Subject: [PATCH] update minimal python version to 3.10 (#8850) --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 9 +-- .github/workflows/release.yml | 2 +- .pre-commit-config.yaml | 4 +- .../components/dashboard_import/__init__.py | 3 +- esphome/components/esp32/__init__.py | 7 +-- esphome/components/esp32/gpio.py | 3 +- .../components/esp32_ble_tracker/__init__.py | 4 +- esphome/components/libretiny/const.py | 2 +- esphome/components/lvgl/automation.py | 3 +- esphome/components/lvgl/lv_validation.py | 4 +- esphome/components/lvgl/lvcode.py | 15 +++-- esphome/components/lvgl/widgets/__init__.py | 4 +- esphome/components/opentherm/generate.py | 6 +- esphome/components/opentherm/schema.py | 26 ++++----- esphome/components/opentherm/validate.py | 2 +- esphome/components/text/__init__.py | 20 +++---- esphome/components/time/__init__.py | 3 +- esphome/components/uart/__init__.py | 9 ++- esphome/config.py | 4 +- esphome/core/__init__.py | 26 ++++----- esphome/coroutine.py | 7 +-- esphome/cpp_generator.py | 58 +++++++++---------- esphome/dashboard/core.py | 4 +- esphome/dashboard/web_server.py | 4 +- esphome/git.py | 8 +-- esphome/helpers.py | 11 ++-- esphome/loader.py | 15 ++--- esphome/platformio_api.py | 5 +- esphome/types.py | 25 ++++---- esphome/util.py | 3 +- esphome/writer.py | 3 +- esphome/yaml_util.py | 3 +- esphome/zeroconf.py | 2 +- pyproject.toml | 6 +- script/lint-python | 2 +- 36 files changed, 148 insertions(+), 166 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 511ec55f3e..8a14dba5eb 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.9" + python-version: "3.10" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2bdc51387..c35488d96b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ permissions: contents: read env: - DEFAULT_PYTHON: "3.9" - PYUPGRADE_TARGET: "--py39-plus" + DEFAULT_PYTHON: "3.10" + PYUPGRADE_TARGET: "--py310-plus" concurrency: # yamllint disable-line rule:line-length @@ -173,7 +173,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" @@ -192,16 +191,12 @@ jobs: os: windows-latest - python-version: "3.10" os: windows-latest - - python-version: "3.9" - os: windows-latest - python-version: "3.13" os: macOS-latest - python-version: "3.12" os: macOS-latest - python-version: "3.10" os: macOS-latest - - python-version: "3.9" - os: macOS-latest runs-on: ${{ matrix.os }} needs: - common diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8569443881..a310b7f083 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.9" + python-version: "3.10" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d08ca10407..a76d5dd9b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,10 +28,10 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.19.1 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 hooks: diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index acaadab544..dbe5532902 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -2,7 +2,6 @@ import base64 from pathlib import Path import re import secrets -from typing import Optional import requests from ruamel.yaml import YAML @@ -84,7 +83,7 @@ async def to_code(config): def import_config( path: str, name: str, - friendly_name: Optional[str], + friendly_name: str | None, project_name: str, import_url: str, network: str = CONF_WIFI, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 438225bdfa..85319a755e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3,7 +3,6 @@ import itertools import logging import os from pathlib import Path -from typing import Optional, Union from esphome import git import esphome.codegen as cg @@ -189,7 +188,7 @@ class RawSdkconfigValue: value: str -SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue] +SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): @@ -206,8 +205,8 @@ def add_idf_component( ref: str = None, path: str = None, refresh: TimePeriod = None, - components: Optional[list[str]] = None, - submodules: Optional[list[str]] = None, + components: list[str] | None = None, + submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 2bb10ce6ec..4258b160bc 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,6 +1,7 @@ +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import Any, Callable +from typing import Any from esphome import pins import esphome.codegen as cg diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index a4425b9680..b7eddeb0dd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,8 +1,8 @@ from __future__ import annotations -from collections.abc import MutableMapping +from collections.abc import Callable, MutableMapping import logging -from typing import Any, Callable +from typing import Any from esphome import automation import esphome.codegen as cg diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 525d8b7786..362609df44 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable import esphome.codegen as cg diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 5fea9bfdb1..f49356604b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from esphome import automation import esphome.codegen as cg diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 3755d35d27..92fe74eb52 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,5 +1,3 @@ -from typing import Union - import esphome.codegen as cg from esphome.components import image from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw @@ -361,7 +359,7 @@ lv_image_list = LValidator( lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) -def lv_pct(value: Union[int, float]): +def lv_pct(value: int | float): if isinstance(value, float): value = int(value * 100) return literal(f"lv_pct({value})") diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 67a87d24bf..7a5c35f896 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,5 +1,4 @@ import abc -from typing import Union from esphome import codegen as cg from esphome.config import Config @@ -75,7 +74,7 @@ class CodeContext(abc.ABC): code_context = None @abc.abstractmethod - def add(self, expression: Union[Expression, Statement]): + def add(self, expression: Expression | Statement): pass @staticmethod @@ -89,13 +88,13 @@ class CodeContext(abc.ABC): CodeContext.append(RawStatement("}")) @staticmethod - def append(expression: Union[Expression, Statement]): + def append(expression: Expression | Statement): if CodeContext.code_context is not None: CodeContext.code_context.add(expression) return expression def __init__(self): - self.previous: Union[CodeContext | None] = None + self.previous: CodeContext | None = None self.indent_level = 0 async def __aenter__(self): @@ -121,7 +120,7 @@ class MainContext(CodeContext): Code generation into the main() function """ - def add(self, expression: Union[Expression, Statement]): + def add(self, expression: Expression | Statement): return cg.add(self.indented_statement(expression)) @@ -144,7 +143,7 @@ class LambdaContext(CodeContext): self.capture = capture self.where = where - def add(self, expression: Union[Expression, Statement]): + def add(self, expression: Expression | Statement): self.code_list.append(self.indented_statement(expression)) return expression @@ -186,7 +185,7 @@ class LvContext(LambdaContext): async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) - def add(self, expression: Union[Expression, Statement]): + def add(self, expression: Expression | Statement): cg.add(expression) return expression @@ -303,7 +302,7 @@ lvgl_static = MockObj("LvglComponent", "::") # equivalent to cg.add() for the current code context -def lv_add(expression: Union[Expression, Statement]): +def lv_add(expression: Expression | Statement): return CodeContext.append(expression) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index ccad45bdc6..9d53c0df26 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Union +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.config_validation import Invalid @@ -262,7 +262,7 @@ async def wait_for_widgets(): await FakeAwaitable(widgets_wait_generator()) -async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: +async def get_widgets(config: dict | list, id: str = CONF_ID) -> list[Widget]: if not config: return [] if not isinstance(config, list): diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index a97754d52c..4e6f3b0a12 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -1,5 +1,5 @@ -from collections.abc import Awaitable -from typing import Any, Callable, Optional +from collections.abc import Awaitable, Callable +from typing import Any import esphome.codegen as cg from esphome.const import CONF_ID @@ -103,7 +103,7 @@ def define_setting_readers(component_type: str, keys: list[str]) -> None: def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): - messages: dict[str, tuple[bool, Optional[int]]] = {} + messages: dict[str, tuple[bool, int | None]] = {} for key in keys: messages[schemas[key].message] = ( schemas[key].keep_updated, diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py index 791ba215e0..f70c8e24db 100644 --- a/esphome/components/opentherm/schema.py +++ b/esphome/components/opentherm/schema.py @@ -2,7 +2,7 @@ # inputs of the OpenTherm component. from dataclasses import dataclass -from typing import Any, Optional, TypeVar +from typing import Any, TypeVar import esphome.config_validation as cv from esphome.const import ( @@ -61,11 +61,11 @@ TSchema = TypeVar("TSchema", bound=EntitySchema) class SensorSchema(EntitySchema): accuracy_decimals: int state_class: str - unit_of_measurement: Optional[str] = None - icon: Optional[str] = None - device_class: Optional[str] = None + unit_of_measurement: str | None = None + icon: str | None = None + device_class: str | None = None disabled_by_default: bool = False - order: Optional[int] = None + order: int | None = None SENSORS: dict[str, SensorSchema] = { @@ -461,9 +461,9 @@ SENSORS: dict[str, SensorSchema] = { @dataclass class BinarySensorSchema(EntitySchema): - icon: Optional[str] = None - device_class: Optional[str] = None - order: Optional[int] = None + icon: str | None = None + device_class: str | None = None + order: int | None = None BINARY_SENSORS: dict[str, BinarySensorSchema] = { @@ -654,7 +654,7 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = { @dataclass class SwitchSchema(EntitySchema): - default_mode: Optional[str] = None + default_mode: str | None = None SWITCHES: dict[str, SwitchSchema] = { @@ -721,9 +721,9 @@ class InputSchema(EntitySchema): unit_of_measurement: str step: float range: tuple[int, int] - icon: Optional[str] = None - auto_max_value: Optional[AutoConfigure] = None - auto_min_value: Optional[AutoConfigure] = None + icon: str | None = None + auto_max_value: AutoConfigure | None = None + auto_min_value: AutoConfigure | None = None INPUTS: dict[str, InputSchema] = { @@ -834,7 +834,7 @@ class SettingSchema(EntitySchema): backing_type: str validation_schema: cv.Schema default_value: Any - order: Optional[int] = None + order: int | None = None SETTINGS: dict[str, SettingSchema] = { diff --git a/esphome/components/opentherm/validate.py b/esphome/components/opentherm/validate.py index 2b80e59f7b..998bcde57f 100644 --- a/esphome/components/opentherm/validate.py +++ b/esphome/components/opentherm/validate.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable from voluptuous import Schema diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 1cc9283e45..a864a0ba4f 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -1,5 +1,3 @@ -from typing import Optional - from esphome import automation import esphome.codegen as cg from esphome.components import mqtt, web_server @@ -92,9 +90,9 @@ async def setup_text_core_( var, config, *, - min_length: Optional[int], - max_length: Optional[int], - pattern: Optional[str], + min_length: int | None, + max_length: int | None, + pattern: str | None, ): await setup_entity(var, config) @@ -121,9 +119,9 @@ async def register_text( var, config, *, - min_length: Optional[int] = 0, - max_length: Optional[int] = 255, - pattern: Optional[str] = None, + min_length: int | None = 0, + max_length: int | None = 255, + pattern: str | None = None, ): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) @@ -136,9 +134,9 @@ async def register_text( async def new_text( config, *, - min_length: Optional[int] = 0, - max_length: Optional[int] = 255, - pattern: Optional[str] = None, + min_length: int | None = 0, + max_length: int | None = 255, + pattern: str | None = None, ): var = cg.new_Pvariable(config[CONF_ID]) await register_text( diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6a3368ca73..6b3ff6f4d3 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,6 +1,5 @@ from importlib import resources import logging -from typing import Optional import tzlocal @@ -40,7 +39,7 @@ SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Co TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) -def _load_tzdata(iana_key: str) -> Optional[bytes]: +def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples try: package_loc, resource = iana_key.rsplit("/", 1) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index bee037774f..a0908a299c 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,5 +1,4 @@ import re -from typing import Optional from esphome import automation, pins import esphome.codegen as cg @@ -322,12 +321,12 @@ def final_validate_device_schema( name: str, *, uart_bus: str = CONF_UART_ID, - baud_rate: Optional[int] = None, + baud_rate: int | None = None, require_tx: bool = False, require_rx: bool = False, - data_bits: Optional[int] = None, - parity: Optional[str] = None, - stop_bits: Optional[int] = None, + data_bits: int | None = None, + parity: str | None = None, + stop_bits: int | None = None, ): def validate_baud_rate(value): if value != baud_rate: diff --git a/esphome/config.py b/esphome/config.py index 4b26b33c78..c6351cdabd 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -7,7 +7,7 @@ import functools import heapq import logging import re -from typing import Any, Union +from typing import Any import voluptuous as vol @@ -63,7 +63,7 @@ def iter_component_configs(config): yield p_name, platform, p_config -ConfigPath = list[Union[str, int]] +ConfigPath = list[str | int] path_context = contextvars.ContextVar("Config path") diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 3a02c95c82..bf61307021 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -2,7 +2,7 @@ import logging import math import os import re -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from esphome.const import ( CONF_COMMENT, @@ -326,7 +326,7 @@ class ID: else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type: Optional[MockObjClass] = type + self.type: MockObjClass | None = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -476,20 +476,20 @@ class EsphomeCore: # True if command is run from vscode api self.vscode = False # The name of the node - self.name: Optional[str] = None + self.name: str | None = None # The friendly name of the node - self.friendly_name: Optional[str] = None + self.friendly_name: str | None = None # The area / zone of the node - self.area: Optional[str] = None + self.area: str | None = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} # The relative path to the configuration YAML - self.config_path: Optional[str] = None + self.config_path: str | None = None # The relative path to where all build files are stored - self.build_path: Optional[str] = None + self.build_path: str | None = None # The validated configuration, this is None until the config has been validated - self.config: Optional[ConfigType] = None + self.config: ConfigType | None = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -509,7 +509,7 @@ class EsphomeCore: # A set of defines to set for the compile process in esphome/core/defines.h self.defines: set[Define] = set() # A map of all platformio options to apply - self.platformio_options: dict[str, Union[str, list[str]]] = {} + self.platformio_options: dict[str, str | list[str]] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared @@ -546,7 +546,7 @@ class EsphomeCore: PIN_SCHEMA_REGISTRY.reset() @property - def address(self) -> Optional[str]: + def address(self) -> str | None: if self.config is None: raise ValueError("Config has not been loaded yet") @@ -559,7 +559,7 @@ class EsphomeCore: return None @property - def web_port(self) -> Optional[int]: + def web_port(self) -> int | None: if self.config is None: raise ValueError("Config has not been loaded yet") @@ -572,7 +572,7 @@ class EsphomeCore: return None @property - def comment(self) -> Optional[str]: + def comment(self) -> str | None: if self.config is None: raise ValueError("Config has not been loaded yet") @@ -773,7 +773,7 @@ class EsphomeCore: _LOGGER.debug("Adding define: %s", define) return define - def add_platformio_option(self, key: str, value: Union[str, list[str]]) -> None: + def add_platformio_option(self, key: str, value: str | list[str]) -> None: new_val = value old_val = self.platformio_options.get(key) if isinstance(old_val, list): diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 30ebb8147e..8d952246f3 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -42,14 +42,13 @@ Here everything is combined in `yield` expressions. You await other coroutines u the last `yield` expression defines what is returned. """ -import collections -from collections.abc import Awaitable, Generator, Iterator +from collections.abc import Awaitable, Callable, Generator, Iterator import functools import heapq import inspect import logging import types -from typing import Any, Callable +from typing import Any _LOGGER = logging.getLogger(__name__) @@ -126,7 +125,7 @@ def _flatten_generator(gen: Generator[Any, Any, Any]): ret = to_send if e.value is None else e.value return ret - if isinstance(val, collections.abc.Awaitable): + if isinstance(val, Awaitable): # yielded object that is awaitable (like `yield some_new_style_method()`) # yield from __await__() like actual coroutines would. to_send = yield from val.__await__() diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 93ebb4cb95..e7d6195915 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,9 +1,9 @@ import abc -from collections.abc import Sequence +from collections.abc import Callable, Sequence import inspect import math import re -from typing import Any, Callable, Optional, Union +from typing import Any from esphome.core import ( CORE, @@ -35,19 +35,19 @@ class Expression(abc.ABC): """ -SafeExpType = Union[ - Expression, - bool, - str, - str, - int, - float, - TimePeriod, - type[bool], - type[int], - type[float], - Sequence[Any], -] +SafeExpType = ( + Expression + | bool + | str + | str + | int + | float + | TimePeriod + | type[bool] + | type[int] + | type[float] + | Sequence[Any] +) class RawExpression(Expression): @@ -90,7 +90,7 @@ class VariableDeclarationExpression(Expression): class ExpressionList(Expression): __slots__ = ("args",) - def __init__(self, *args: Optional[SafeExpType]): + def __init__(self, *args: SafeExpType | None): # Remove every None on end args = list(args) while args and args[-1] is None: @@ -139,7 +139,7 @@ class CallExpression(Expression): class StructInitializer(Expression): __slots__ = ("base", "args") - def __init__(self, base: Expression, *args: tuple[str, Optional[SafeExpType]]): + def __init__(self, base: Expression, *args: tuple[str, SafeExpType | None]): self.base = base # TODO: args is always a Tuple, is this check required? if not isinstance(args, OrderedDict): @@ -197,9 +197,7 @@ class ParameterExpression(Expression): class ParameterListExpression(Expression): __slots__ = ("parameters",) - def __init__( - self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]] - ): + def __init__(self, *parameters: ParameterExpression | tuple[SafeExpType, str]): self.parameters = [] for parameter in parameters: if not isinstance(parameter, ParameterExpression): @@ -362,7 +360,7 @@ def safe_exp(obj: SafeExpType) -> Expression: return IntLiteral(int(obj.total_seconds)) if isinstance(obj, TimePeriodMinutes): return IntLiteral(int(obj.total_minutes)) - if isinstance(obj, (tuple, list)): + if isinstance(obj, tuple | list): return ArrayInitializer(*[safe_exp(o) for o in obj]) if obj is bool: return bool_ @@ -461,7 +459,7 @@ def static_const_array(id_, rhs) -> "MockObj": return obj -def statement(expression: Union[Expression, Statement]) -> Statement: +def statement(expression: Expression | Statement) -> Statement: """Convert expression into a statement unless is already a statement.""" if isinstance(expression, Statement): return expression @@ -579,7 +577,7 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: return Pvariable(id_, rhs) -def add(expression: Union[Expression, Statement]): +def add(expression: Expression | Statement): """Add an expression to the codegen section. After this is called, the given given expression will @@ -588,12 +586,12 @@ def add(expression: Union[Expression, Statement]): CORE.add(expression) -def add_global(expression: Union[SafeExpType, Statement], prepend: bool = False): +def add_global(expression: SafeExpType | Statement, prepend: bool = False): """Add an expression to the codegen global storage (above setup()).""" CORE.add_global(expression, prepend) -def add_library(name: str, version: Optional[str], repository: Optional[str] = None): +def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') @@ -619,7 +617,7 @@ def add_define(name: str, value: SafeExpType = None): CORE.add_define(Define(name, safe_exp(value))) -def add_platformio_option(key: str, value: Union[str, list[str]]): +def add_platformio_option(key: str, value: str | list[str]): CORE.add_platformio_option(key, value) @@ -654,7 +652,7 @@ async def process_lambda( parameters: list[tuple[SafeExpType, str]], capture: str = "=", return_type: SafeExpType = None, -) -> Union[LambdaExpression, None]: +) -> LambdaExpression | None: """Process the given lambda value into a LambdaExpression. This is a coroutine because lambdas can depend on other IDs, @@ -711,8 +709,8 @@ def is_template(value): async def templatable( value: Any, args: list[tuple[SafeExpType, str]], - output_type: Optional[SafeExpType], - to_exp: Union[Callable, dict] = None, + output_type: SafeExpType | None, + to_exp: Callable | dict = None, ): """Generate code for a templatable config option. @@ -821,7 +819,7 @@ class MockObj(Expression): assert self.op == "::" return MockObj(f"using namespace {self.base}") - def __getitem__(self, item: Union[str, Expression]) -> "MockObj": + def __getitem__(self, item: str | Expression) -> "MockObj": next_op = "." if isinstance(item, str) and item.startswith("P"): item = item[1:] diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index 416442c426..410ef0c29d 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine import contextlib from dataclasses import dataclass from functools import partial @@ -9,7 +9,7 @@ import json import logging from pathlib import Path import threading -from typing import Any, Callable +from typing import Any from esphome.storage_json import ignored_devices_storage_path diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 6196e01760..a297885782 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio import base64 -from collections.abc import Iterable +from collections.abc import Callable, Iterable import datetime import functools import gzip @@ -17,7 +17,7 @@ import shutil import subprocess import threading import time -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from urllib.parse import urlparse import tornado diff --git a/esphome/git.py b/esphome/git.py index 144c160b20..005bcae702 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import hashlib @@ -5,7 +6,6 @@ import logging from pathlib import Path import re import subprocess -from typing import Callable, Optional import urllib.parse import esphome.config_validation as cv @@ -45,12 +45,12 @@ def clone_or_update( *, url: str, ref: str = None, - refresh: Optional[TimePeriodSeconds], + refresh: TimePeriodSeconds | None, domain: str, username: str = None, password: str = None, - submodules: Optional[list[str]] = None, -) -> tuple[Path, Optional[Callable[[], None]]]: + submodules: list[str] | None = None, +) -> tuple[Path, Callable[[], None] | None]: key = f"{url}@{ref}" if username is not None and password is not None: diff --git a/esphome/helpers.py b/esphome/helpers.py index b649465d69..d95546ac94 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -7,7 +7,6 @@ from pathlib import Path import platform import re import tempfile -from typing import Union from urllib.parse import urlparse _LOGGER = logging.getLogger(__name__) @@ -219,8 +218,8 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]: int, int, int, - Union[str, None], - Union[tuple[str, int], tuple[str, int, int, int]], + str | None, + tuple[str, int] | tuple[str, int, int, int], ] ] = [] for addr in address_list: @@ -282,7 +281,7 @@ def read_file(path): raise EsphomeError(f"Error reading file {path}: {err}") from err -def _write_file(path: Union[Path, str], text: Union[str, bytes]): +def _write_file(path: Path | str, text: str | bytes): """Atomically writes `text` to the given path. Automatically creates all parent directories. @@ -315,7 +314,7 @@ def _write_file(path: Union[Path, str], text: Union[str, bytes]): _LOGGER.error("Write file cleanup failed: %s", err) -def write_file(path: Union[Path, str], text: str): +def write_file(path: Path | str, text: str): try: _write_file(path, text) except OSError as err: @@ -324,7 +323,7 @@ def write_file(path: Union[Path, str], text: str): raise EsphomeError(f"Could not write file at {path}") from err -def write_file_if_changed(path: Union[Path, str], text: str) -> bool: +def write_file_if_changed(path: Path | str, text: str) -> bool: """Write text to the given path, but not if the contents match already. Returns true if the file was changed. diff --git a/esphome/loader.py b/esphome/loader.py index dbaa2ac661..79a1d7f576 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from contextlib import AbstractContextManager from dataclasses import dataclass import importlib @@ -8,7 +9,7 @@ import logging from pathlib import Path import sys from types import ModuleType -from typing import Any, Callable, Optional +from typing import Any from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE @@ -57,7 +58,7 @@ class ComponentManifest: return getattr(self.module, "IS_TARGET_PLATFORM", False) @property - def config_schema(self) -> Optional[Any]: + def config_schema(self) -> Any | None: return getattr(self.module, "CONFIG_SCHEMA", None) @property @@ -69,7 +70,7 @@ class ComponentManifest: return getattr(self.module, "MULTI_CONF_NO_DEFAULT", False) @property - def to_code(self) -> Optional[Callable[[Any], None]]: + def to_code(self) -> Callable[[Any], None] | None: return getattr(self.module, "to_code", None) @property @@ -96,7 +97,7 @@ class ComponentManifest: return getattr(self.module, "INSTANCE_TYPE", None) @property - def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]: + def final_validate_schema(self) -> Callable[[ConfigType], None] | None: """Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called after the main validation. In that function checks across components can be made. @@ -129,7 +130,7 @@ class ComponentManifest: class ComponentMetaFinder(importlib.abc.MetaPathFinder): def __init__( - self, components_path: Path, allowed_components: Optional[list[str]] = None + self, components_path: Path, allowed_components: list[str] | None = None ) -> None: self._allowed_components = allowed_components self._finders = [] @@ -140,7 +141,7 @@ class ComponentMetaFinder(importlib.abc.MetaPathFinder): continue self._finders.append(finder) - def find_spec(self, fullname: str, path: Optional[list[str]], target=None): + def find_spec(self, fullname: str, path: list[str] | None, target=None): if not fullname.startswith("esphome.components."): return None parts = fullname.split(".") @@ -167,7 +168,7 @@ def clear_component_meta_finders(): def install_meta_finder( - components_path: Path, allowed_components: Optional[list[str]] = None + components_path: Path, allowed_components: list[str] | None = None ): sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index ed95fa125e..808db03231 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,7 +5,6 @@ import os from pathlib import Path import re import subprocess -from typing import Union from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError @@ -73,7 +72,7 @@ FILTER_PLATFORMIO_LINES = [ ] -def run_platformio_cli(*args, **kwargs) -> Union[str, int]: +def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path()) os.environ.setdefault( @@ -93,7 +92,7 @@ def run_platformio_cli(*args, **kwargs) -> Union[str, int]: return run_external_command(platformio.__main__.main, *cmd, **kwargs) -def run_platformio_cli_run(config, verbose, *args, **kwargs) -> Union[str, int]: +def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: command = ["run", "-d", CORE.build_path] if verbose: command += ["-v"] diff --git a/esphome/types.py b/esphome/types.py index 4e69e3cbd7..f68f503993 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,19 +1,18 @@ """This helper module tracks commonly used types in the esphome python codebase.""" -from typing import Union - from esphome.core import ID, EsphomeCore, Lambda -ConfigFragmentType = Union[ - str, - int, - float, - None, - dict[Union[str, int], "ConfigFragmentType"], - list["ConfigFragmentType"], - ID, - Lambda, -] +ConfigFragmentType = ( + str + | int + | float + | None + | dict[str | int, "ConfigFragmentType"] + | list["ConfigFragmentType"] + | ID + | Lambda +) + ConfigType = dict[str, ConfigFragmentType] CoreType = EsphomeCore -ConfigPathType = Union[str, int] +ConfigPathType = str | int diff --git a/esphome/util.py b/esphome/util.py index 32fd90cd25..ba26b8adc1 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -6,7 +6,6 @@ from pathlib import Path import re import subprocess import sys -from typing import Union from esphome import const @@ -162,7 +161,7 @@ class RedirectText: def run_external_command( func, *cmd, capture_stdout: bool = False, filter_lines: str = None -) -> Union[int, str]: +) -> int | str: """ Run a function from an external package that acts like a main method. diff --git a/esphome/writer.py b/esphome/writer.py index 39423db64c..0452098e24 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -3,7 +3,6 @@ import logging import os from pathlib import Path import re -from typing import Union from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -132,7 +131,7 @@ def update_storage_json(): new.save(path) -def format_ini(data: dict[str, Union[str, list[str]]]) -> str: +def format_ini(data: dict[str, str | list[str]]) -> str: content = "" for key, value in sorted(data.items()): if isinstance(value, list): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index cbe3fef272..02778a6de9 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable import fnmatch import functools import inspect @@ -8,7 +9,7 @@ from ipaddress import _BaseAddress import logging import math import os -from typing import Any, Callable +from typing import Any import uuid import yaml diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index c6a143a42f..fa496b3488 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,9 +1,9 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import Callable from zeroconf import ( AddressResolver, diff --git a/pyproject.toml b/pyproject.toml index 2d159b0160..e783799e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.9.0" +requires-python = ">=3.10.0" dynamic = ["dependencies", "optional-dependencies", "version"] @@ -62,7 +62,7 @@ addopts = [ ] [tool.pylint.MAIN] -py-version = "3.9" +py-version = "3.10" ignore = [ "api_pb2.py", ] @@ -106,7 +106,7 @@ expected-line-ending-format = "LF" [tool.ruff] required-version = ">=0.5.0" -target-version = "py39" +target-version = "py310" exclude = ['generated'] [tool.ruff.lint] diff --git a/script/lint-python b/script/lint-python index c9f1789160..2c25e4aee0 100755 --- a/script/lint-python +++ b/script/lint-python @@ -137,7 +137,7 @@ def main(): print() print("Running pyupgrade...") print() - PYUPGRADE_TARGET = "--py39-plus" + PYUPGRADE_TARGET = "--py310-plus" for files in filesets: cmd = ["pyupgrade", PYUPGRADE_TARGET] + files log = get_err(*cmd)