From 1c1fc8a22caafd10599cb5614fdc2e08c9193775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 8 Jun 2024 22:56:28 +0200 Subject: [PATCH] Code format updates (#166) * Code format updates * usa alias for all --- .github/workflows/lint.yml | 5 ++- .ruff.toml | 37 +++------------- .../integration_blueprint/__init__.py | 43 +++++++++++++------ .../integration_blueprint/api.py | 40 +++++++++++------ .../integration_blueprint/binary_sensor.py | 23 +++++++--- .../integration_blueprint/config_flow.py | 13 +++--- .../integration_blueprint/const.py | 2 - .../integration_blueprint/coordinator.py | 25 ++++++----- .../integration_blueprint/data.py | 25 +++++++++++ .../integration_blueprint/entity.py | 20 ++++++--- .../integration_blueprint/sensor.py | 25 ++++++++--- .../integration_blueprint/switch.py | 33 +++++++++----- scripts/lint | 1 + 13 files changed, 182 insertions(+), 110 deletions(-) create mode 100644 custom_components/integration_blueprint/data.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4cb2ee0..4433fa9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,5 +25,8 @@ jobs: - name: "Install requirements" run: python3 -m pip install -r requirements.txt - - name: "Run" + - name: "Lint" run: python3 -m ruff check . + + - name: "Format" + run: python3 -m ruff format . --check diff --git a/.ruff.toml b/.ruff.toml index 00cc87f..8ea6a71 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -4,39 +4,16 @@ target-version = "py312" [lint] select = [ - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "C", # complexity - "D", # docstrings - "E", # pycodestyle - "F", # pyflakes/autoflake - "ICN001", # import concentions; {name} should be imported as {asname} - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T20", # flake8-print - "TRY004", # Prefer TypeError exception for invalid type - "RUF006", # Store a reference to the return value of asyncio.create_task - "UP", # pyupgrade - "W", # pycodestyle + "ALL", ] ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D404", # First word of the docstring should not be This - "D406", # Section name should end with a newline - "D407", # Section name underlining - "D411", # Missing blank line before section - "E501", # line too long - "E731", # do not assign a lambda expression, use a def + "ANN101", # Missing type annotation for `self` in method + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "D203", # no-blank-line-before-class (incompatible with formatter) + "D212", # multi-line-summary-first-line (incompatible with formatter) + "COM812", # incompatible with formatter + "ISC001", # incompatible with formatter ] [lint.flake8-pytest-style] diff --git a/custom_components/integration_blueprint/__init__.py b/custom_components/integration_blueprint/__init__.py index a9adfdc..7e79eca 100644 --- a/custom_components/integration_blueprint/__init__.py +++ b/custom_components/integration_blueprint/__init__.py @@ -1,18 +1,26 @@ -"""Custom integration to integrate integration_blueprint with Home Assistant. +""" +Custom integration to integrate integration_blueprint with Home Assistant. For more details about this integration, please refer to https://github.com/ludeeus/integration_blueprint """ + from __future__ import annotations -from homeassistant.config_entries import ConfigEntry +from typing import TYPE_CHECKING + from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.loader import async_get_loaded_integration from .api import IntegrationBlueprintApiClient -from .const import DOMAIN from .coordinator import BlueprintDataUpdateCoordinator +from .data import IntegrationBlueprintData + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from .data import IntegrationBlueprintConfigEntry PLATFORMS: list[Platform] = [ Platform.SENSOR, @@ -22,17 +30,24 @@ # https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, + entry: IntegrationBlueprintConfigEntry, +) -> bool: """Set up this integration using UI.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator = BlueprintDataUpdateCoordinator( + coordinator = BlueprintDataUpdateCoordinator( hass=hass, + ) + entry.runtime_data = IntegrationBlueprintData( client=IntegrationBlueprintApiClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], session=async_get_clientsession(hass), ), + integration=async_get_loaded_integration(hass, entry.domain), + coordinator=coordinator, ) + # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities await coordinator.async_config_entry_first_refresh() @@ -42,14 +57,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, + entry: IntegrationBlueprintConfigEntry, +) -> bool: """Handle removal of an entry.""" - if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unloaded + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_reload_entry( + hass: HomeAssistant, + entry: IntegrationBlueprintConfigEntry, +) -> None: """Reload config entry.""" await async_unload_entry(hass, entry) await async_setup_entry(hass, entry) diff --git a/custom_components/integration_blueprint/api.py b/custom_components/integration_blueprint/api.py index f517d7f..441e745 100644 --- a/custom_components/integration_blueprint/api.py +++ b/custom_components/integration_blueprint/api.py @@ -1,7 +1,9 @@ """Sample API Client.""" + from __future__ import annotations import socket +from typing import Any import aiohttp import async_timeout @@ -12,17 +14,27 @@ class IntegrationBlueprintApiClientError(Exception): class IntegrationBlueprintApiClientCommunicationError( - IntegrationBlueprintApiClientError + IntegrationBlueprintApiClientError, ): """Exception to indicate a communication error.""" class IntegrationBlueprintApiClientAuthenticationError( - IntegrationBlueprintApiClientError + IntegrationBlueprintApiClientError, ): """Exception to indicate an authentication error.""" +def _verify_response_or_raise(response: aiohttp.ClientResponse) -> None: + """Verify that the response is valid.""" + if response.status in (401, 403): + msg = "Invalid credentials" + raise IntegrationBlueprintApiClientAuthenticationError( + msg, + ) + response.raise_for_status() + + class IntegrationBlueprintApiClient: """Sample API Client.""" @@ -37,13 +49,14 @@ def __init__( self._password = password self._session = session - async def async_get_data(self) -> any: + async def async_get_data(self) -> Any: """Get data from the API.""" return await self._api_wrapper( - method="get", url="https://jsonplaceholder.typicode.com/posts/1" + method="get", + url="https://jsonplaceholder.typicode.com/posts/1", ) - async def async_set_title(self, value: str) -> any: + async def async_set_title(self, value: str) -> Any: """Get data from the API.""" return await self._api_wrapper( method="patch", @@ -58,7 +71,7 @@ async def _api_wrapper( url: str, data: dict | None = None, headers: dict | None = None, - ) -> any: + ) -> Any: """Get information from the API.""" try: async with async_timeout.timeout(10): @@ -68,22 +81,21 @@ async def _api_wrapper( headers=headers, json=data, ) - if response.status in (401, 403): - raise IntegrationBlueprintApiClientAuthenticationError( - "Invalid credentials", - ) - response.raise_for_status() + _verify_response_or_raise(response) return await response.json() except TimeoutError as exception: + msg = f"Timeout error fetching information - {exception}" raise IntegrationBlueprintApiClientCommunicationError( - "Timeout error fetching information", + msg, ) from exception except (aiohttp.ClientError, socket.gaierror) as exception: + msg = f"Error fetching information - {exception}" raise IntegrationBlueprintApiClientCommunicationError( - "Error fetching information", + msg, ) from exception except Exception as exception: # pylint: disable=broad-except + msg = f"Something really wrong happened! - {exception}" raise IntegrationBlueprintApiClientError( - "Something really wrong happened!" + msg, ) from exception diff --git a/custom_components/integration_blueprint/binary_sensor.py b/custom_components/integration_blueprint/binary_sensor.py index fff5b21..2491e7e 100644 --- a/custom_components/integration_blueprint/binary_sensor.py +++ b/custom_components/integration_blueprint/binary_sensor.py @@ -1,16 +1,24 @@ """Binary sensor platform for integration_blueprint.""" + from __future__ import annotations +from typing import TYPE_CHECKING + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator from .entity import IntegrationBlueprintEntity +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .coordinator import BlueprintDataUpdateCoordinator + from .data import IntegrationBlueprintConfigEntry + ENTITY_DESCRIPTIONS = ( BinarySensorEntityDescription( key="integration_blueprint", @@ -20,12 +28,15 @@ ) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry( + hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + entry: IntegrationBlueprintConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the binary_sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( + async_add_entities( IntegrationBlueprintBinarySensor( - coordinator=coordinator, + coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) for entity_description in ENTITY_DESCRIPTIONS diff --git a/custom_components/integration_blueprint/config_flow.py b/custom_components/integration_blueprint/config_flow.py index 6b8c6ef..601089b 100644 --- a/custom_components/integration_blueprint/config_flow.py +++ b/custom_components/integration_blueprint/config_flow.py @@ -3,12 +3,11 @@ from __future__ import annotations import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_create_clientsession - from .api import ( IntegrationBlueprintApiClient, IntegrationBlueprintApiClientAuthenticationError, @@ -26,7 +25,7 @@ class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict | None = None, - ) -> config_entries.FlowResult: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by the user.""" _errors = {} if user_input is not None: @@ -56,18 +55,18 @@ async def async_step_user( { vol.Required( CONF_USERNAME, - default=(user_input or {}).get(CONF_USERNAME), + default=(user_input or {}).get(CONF_USERNAME, vol.UNDEFINED), ): selector.TextSelector( selector.TextSelectorConfig( - type=selector.TextSelectorType.TEXT + type=selector.TextSelectorType.TEXT, ), ), vol.Required(CONF_PASSWORD): selector.TextSelector( selector.TextSelectorConfig( - type=selector.TextSelectorType.PASSWORD + type=selector.TextSelectorType.PASSWORD, ), ), - } + }, ), errors=_errors, ) diff --git a/custom_components/integration_blueprint/const.py b/custom_components/integration_blueprint/const.py index 84753a6..ff45085 100644 --- a/custom_components/integration_blueprint/const.py +++ b/custom_components/integration_blueprint/const.py @@ -4,7 +4,5 @@ LOGGER: Logger = getLogger(__package__) -NAME = "Integration blueprint" DOMAIN = "integration_blueprint" -VERSION = "0.0.0" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/integration_blueprint/coordinator.py b/custom_components/integration_blueprint/coordinator.py index d427a1a..809cec5 100644 --- a/custom_components/integration_blueprint/coordinator.py +++ b/custom_components/integration_blueprint/coordinator.py @@ -1,48 +1,47 @@ """DataUpdateCoordinator for integration_blueprint.""" + from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING, Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import ( - IntegrationBlueprintApiClient, IntegrationBlueprintApiClientAuthenticationError, IntegrationBlueprintApiClientError, ) from .const import DOMAIN, LOGGER +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from .data import IntegrationBlueprintConfigEntry + # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities class BlueprintDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" - config_entry: ConfigEntry + config_entry: IntegrationBlueprintConfigEntry def __init__( self, hass: HomeAssistant, - client: IntegrationBlueprintApiClient, ) -> None: """Initialize.""" - self.client = client super().__init__( hass=hass, logger=LOGGER, name=DOMAIN, - update_interval=timedelta(minutes=5), + update_interval=timedelta(hours=1), ) - async def _async_update_data(self): + async def _async_update_data(self) -> Any: """Update data via library.""" try: - return await self.client.async_get_data() + return await self.config_entry.runtime_data.client.async_get_data() except IntegrationBlueprintApiClientAuthenticationError as exception: raise ConfigEntryAuthFailed(exception) from exception except IntegrationBlueprintApiClientError as exception: diff --git a/custom_components/integration_blueprint/data.py b/custom_components/integration_blueprint/data.py new file mode 100644 index 0000000..cdeb1ea --- /dev/null +++ b/custom_components/integration_blueprint/data.py @@ -0,0 +1,25 @@ +"""Custom types for integration_blueprint.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.loader import Integration + + from .api import IntegrationBlueprintApiClient + from .coordinator import BlueprintDataUpdateCoordinator + + +type IntegrationBlueprintConfigEntry = ConfigEntry[IntegrationBlueprintData] + + +@dataclass +class IntegrationBlueprintData: + """Data for the Blueprint integration.""" + + client: IntegrationBlueprintApiClient + coordinator: BlueprintDataUpdateCoordinator + integration: Integration diff --git a/custom_components/integration_blueprint/entity.py b/custom_components/integration_blueprint/entity.py index 4325227..583bf40 100644 --- a/custom_components/integration_blueprint/entity.py +++ b/custom_components/integration_blueprint/entity.py @@ -1,14 +1,15 @@ """BlueprintEntity class.""" + from __future__ import annotations -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTION, DOMAIN, NAME, VERSION +from .const import ATTRIBUTION from .coordinator import BlueprintDataUpdateCoordinator -class IntegrationBlueprintEntity(CoordinatorEntity): +class IntegrationBlueprintEntity(CoordinatorEntity[BlueprintDataUpdateCoordinator]): """BlueprintEntity class.""" _attr_attribution = ATTRIBUTION @@ -18,8 +19,13 @@ def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None: super().__init__(coordinator) self._attr_unique_id = coordinator.config_entry.entry_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - name=NAME, - model=VERSION, - manufacturer=NAME, + identifiers={ + ( + coordinator.config_entry.domain, + coordinator.config_entry.entry_id, + ), + }, + name=coordinator.config_entry.runtime_data.integration.name, + model=coordinator.config_entry.runtime_data.integration.version, + manufacturer=coordinator.config_entry.runtime_data.integration.name, ) diff --git a/custom_components/integration_blueprint/sensor.py b/custom_components/integration_blueprint/sensor.py index 06201fe..bd5f89b 100644 --- a/custom_components/integration_blueprint/sensor.py +++ b/custom_components/integration_blueprint/sensor.py @@ -1,12 +1,20 @@ """Sensor platform for integration_blueprint.""" + from __future__ import annotations +from typing import TYPE_CHECKING + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator from .entity import IntegrationBlueprintEntity +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .coordinator import BlueprintDataUpdateCoordinator + from .data import IntegrationBlueprintConfigEntry + ENTITY_DESCRIPTIONS = ( SensorEntityDescription( key="integration_blueprint", @@ -16,12 +24,15 @@ ) -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry( + hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + entry: IntegrationBlueprintConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( + async_add_entities( IntegrationBlueprintSensor( - coordinator=coordinator, + coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) for entity_description in ENTITY_DESCRIPTIONS @@ -41,6 +52,6 @@ def __init__( self.entity_description = entity_description @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return the native value of the sensor.""" return self.coordinator.data.get("body") diff --git a/custom_components/integration_blueprint/switch.py b/custom_components/integration_blueprint/switch.py index 33340a2..7629220 100644 --- a/custom_components/integration_blueprint/switch.py +++ b/custom_components/integration_blueprint/switch.py @@ -1,12 +1,20 @@ """Switch platform for integration_blueprint.""" + from __future__ import annotations +from typing import TYPE_CHECKING, Any + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator from .entity import IntegrationBlueprintEntity +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .coordinator import BlueprintDataUpdateCoordinator + from .data import IntegrationBlueprintConfigEntry + ENTITY_DESCRIPTIONS = ( SwitchEntityDescription( key="integration_blueprint", @@ -16,12 +24,15 @@ ) -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( +async def async_setup_entry( + hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + entry: IntegrationBlueprintConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switch platform.""" + async_add_entities( IntegrationBlueprintSwitch( - coordinator=coordinator, + coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) for entity_description in ENTITY_DESCRIPTIONS @@ -45,12 +56,12 @@ def is_on(self) -> bool: """Return true if the switch is on.""" return self.coordinator.data.get("title", "") == "foo" - async def async_turn_on(self, **_: any) -> None: + async def async_turn_on(self, **_: Any) -> None: """Turn on the switch.""" - await self.coordinator.api.async_set_title("bar") + await self.coordinator.config_entry.runtime_data.client.async_set_title("bar") await self.coordinator.async_request_refresh() - async def async_turn_off(self, **_: any) -> None: + async def async_turn_off(self, **_: Any) -> None: """Turn off the switch.""" - await self.coordinator.api.async_set_title("foo") + await self.coordinator.config_entry.runtime_data.client.async_set_title("foo") await self.coordinator.async_request_refresh() diff --git a/scripts/lint b/scripts/lint index 9b5b1df..5d68d15 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,4 +4,5 @@ set -e cd "$(dirname "$0")/.." +ruff format . ruff check . --fix