diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da424f516f..99a4f40201 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.15 + rev: v0.16.0 hooks: # Run the linter. - id: ruff diff --git a/AGENTS.md b/AGENTS.md index 75a9cdb2bf..0c98e5fe8e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,11 +191,14 @@ This document provides essential context for AI models interacting with this pro my_component_ns = cg.esphome_ns.namespace("my_component") MyComponent = my_component_ns.class_("MyComponent", cg.Component) - CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MyComponent), - cv.Required(CONF_KEY): cv.string, - cv.Optional(CONF_PARAM, default=42): cv.int_, - }).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Required(CONF_KEY): cv.string, + cv.Optional(CONF_PARAM, default=42): cv.int_, + } + ).extend(cv.COMPONENT_SCHEMA) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -229,7 +232,12 @@ This document provides essential context for AI models interacting with this pro - **Sensor:** ```python from esphome.components import sensor - CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s")) + + CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend( + cv.polling_component_schema("60s") + ) + + async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) @@ -238,7 +246,10 @@ This document provides essential context for AI models interacting with this pro - **Binary Sensor:** ```python from esphome.components import binary_sensor - CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... }) + + CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...}) + + async def to_code(config): var = await binary_sensor.new_binary_sensor(config) ``` @@ -246,7 +257,10 @@ This document provides essential context for AI models interacting with this pro - **Switch:** ```python from esphome.components import switch - CONFIG_SCHEMA = switch.switch_schema().extend({ ... }) + + CONFIG_SCHEMA = switch.switch_schema().extend({...}) + + async def to_code(config): var = await switch.new_switch(config) ``` @@ -263,10 +277,13 @@ This document provides essential context for AI models interacting with this pro ```python from esphome import automation - CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(MyComponent), - cv.Optional(CONF_ON_STATE): automation.validate_automation({}), - }).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + } + ).extend(cv.COMPONENT_SCHEMA) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -316,11 +333,14 @@ This document provides essential context for AI models interacting with this pro ```python TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template()) - CONFIG_SCHEMA = cv.Schema({ - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} - ), - }) + CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} + ), + } + ) + async def to_code(config): for conf in config.get(CONF_ON_TURN_ON, []): @@ -617,6 +637,7 @@ This document provides essential context for AI models interacting with this pro _component_state = [] _use_feature = None + def enable_feature(): global _use_feature _use_feature = True @@ -636,20 +657,24 @@ This document provides essential context for AI models interacting with this pro DOMAIN = "my_component" + @dataclass class MyComponentData: feature_enabled: bool = False item_count: int = 0 items: list[str] = field(default_factory=list) + def _get_data() -> MyComponentData: if DOMAIN not in CORE.data: CORE.data[DOMAIN] = MyComponentData() return CORE.data[DOMAIN] + def request_feature() -> None: _get_data().feature_enabled = True + def add_item(item: str) -> None: _get_data().items.append(item) ``` @@ -707,7 +732,9 @@ This document provides essential context for AI models interacting with this pro ```python # Remove before 2026.6.0 if CONF_OLD_KEY in config: - _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0") + _LOGGER.warning( + f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0" + ) config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate ``` ## 9. English Language diff --git a/pyproject.toml b/pyproject.toml index f38633b4ae..d38918a0a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,7 @@ ignore = [ "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR0917", # Too many positional arguments ({c_pos} > {max_pos}) "PLW1641", # Object does not implement `__hash__` method "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target diff --git a/requirements_test.txt b/requirements_test.txt index 9a9b7adff5..e7de8eb5e7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.6 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.22 # also change in .pre-commit-config.yaml when updating +ruff==0.16.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/ci-custom.py b/script/ci-custom.py index 4b16734ebe..90748a13b9 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -345,9 +345,11 @@ def lint_const_ordered(fname, content): ( mi, 1, - f"Constant {highlight(mline)} is not ordered, please make sure all " - f"constants are ordered. See line {mi} (should go to line {target}, " - f"{target_text})", + ( + f"Constant {highlight(mline)} is not ordered, please make sure all " + f"constants are ordered. See line {mi} (should go to line {target}, " + f"{target_text})" + ), ) ) return errs @@ -990,12 +992,14 @@ def lint_log_multiline_continuation(fname, content): ( lineno, col, - "Multi-line log message has a continuation line that does " - "not start with a space. The log viewer uses leading " - "whitespace to detect continuation lines and re-add the " - f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" - "Either start the continuation with a space/indent, or " - "split into separate ESP_LOG* calls.", + ( + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls." + ), ) ) return errs @@ -1073,10 +1077,12 @@ def lint_test_package_key_matches_bus(fname, content): ( lineno, 1, - f"Package key {highlight(pkg_key)} does not match bus directory " - f"{highlight(bus_dir)}. The package key must match the directory " - f"name under tests/test_build_components/common/. " - f"Change {highlight(pkg_key)} to {highlight(bus_dir)}.", + ( + f"Package key {highlight(pkg_key)} does not match bus directory " + f"{highlight(bus_dir)}. The package key must match the directory " + f"name under tests/test_build_components/common/. " + f"Change {highlight(pkg_key)} to {highlight(bus_dir)}." + ), ) ) return errs diff --git a/tests/components/README.md b/tests/components/README.md index 145a3440d2..be5e887767 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -28,6 +28,7 @@ create an `__init__.py` in your component's test directory and define `override_ ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: # Re-enable the component's own to_code (needed when the component must # emit C++ setup code that the test binary depends on at link time). @@ -39,6 +40,7 @@ Or supply a lightweight stub instead of the real `to_code`: ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: async def to_code_testing(config): # Only emit what the C++ tests actually need @@ -54,6 +56,7 @@ e.g. `tests/components/my_sensor/sensor/__init__.py`): ```python from tests.testing_helpers import ComponentManifestOverride + def override_manifest(manifest: ComponentManifestOverride) -> None: manifest.enable_codegen() ``` diff --git a/tests/integration/README.md b/tests/integration/README.md index 4de08777b0..44d9e0d644 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -187,6 +187,7 @@ loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} state_future: asyncio.Future[EntityState] = loop.create_future() + def on_state(state: EntityState) -> None: """This callback only receives NEW state changes, not initial states.""" states[state.key] = state @@ -195,6 +196,7 @@ def on_state(state: EntityState) -> None: if not state_future.done(): state_future.set_result(state) + # Get entities and set up state synchronization entities, services = await client.list_entities_services() initial_state_helper = InitialStateHelper(entities) @@ -228,6 +230,7 @@ loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} state_future: asyncio.Future[EntityState] = loop.create_future() + def on_state(state: EntityState) -> None: states[state.key] = state # Check for specific condition using isinstance @@ -235,6 +238,7 @@ def on_state(state: EntityState) -> None: if not state_future.done(): state_future.set_result(state) + client.subscribe_states(on_state) # Wait for state with timeout @@ -263,11 +267,13 @@ entity_count = 50 received_states: set[int] = set() all_states_future: asyncio.Future[bool] = loop.create_future() + def on_state(state: EntityState) -> None: received_states.add(state.key) if len(received_states) >= entity_count and not all_states_future.done(): all_states_future.set_result(True) + client.subscribe_states(on_state) await asyncio.wait_for(all_states_future, timeout=10.0) ``` @@ -367,6 +373,7 @@ service_future = loop.create_future() connected_pattern = re.compile(r"Client .* connected from") service_pattern = re.compile(r"Service called") + def check_output(line: str) -> None: """Check log output for expected messages.""" if not connected_future.done() and connected_pattern.search(line): @@ -374,6 +381,7 @@ def check_output(line: str) -> None: elif not service_future.done() and service_pattern.search(line): service_future.set_result(True) + async with run_compiled(yaml_config, line_callback=check_output): async with api_client_connected() as client: # Wait for specific log message diff --git a/tests/script/test_docker_build.py b/tests/script/test_docker_build.py index 34bcc4e714..06dc21a92e 100644 --- a/tests/script/test_docker_build.py +++ b/tests/script/test_docker_build.py @@ -71,10 +71,12 @@ def test_branch_manifest_targets_ghcr_only( ) assert commands == [ - "docker buildx imagetools create " - "--tag ghcr.io/esphome/esphome-hassio:my-branch " - "ghcr.io/esphome/esphome-hassio-amd64:my-branch " - "ghcr.io/esphome/esphome-hassio-aarch64:my-branch" + ( + "docker buildx imagetools create " + "--tag ghcr.io/esphome/esphome-hassio:my-branch " + "ghcr.io/esphome/esphome-hassio-amd64:my-branch " + "ghcr.io/esphome/esphome-hassio-aarch64:my-branch" + ) ] diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index b8aa19d6ae..08751879c2 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -1433,8 +1433,10 @@ def test_importing_framework_helpers_does_not_import_requests() -> None: [ sys.executable, "-c", - "import sys\nimport esphome.framework_helpers\n" - "print('\\n'.join(sys.modules))", + ( + "import sys\nimport esphome.framework_helpers\n" + "print('\\n'.join(sys.modules))" + ), ], capture_output=True, text=True,