Enable strict typing of date_time (#106868)

* Enable strict typing of date_time

* Fix parse_datetime

* Add test

* Add comments

* Update tests/util/test_dt.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Erik Montnemery
2024-01-02 13:57:25 +01:00
committed by GitHub
parent 15cdd42c99
commit 8f9bd75a36
5 changed files with 57 additions and 15 deletions

View File

@ -373,6 +373,7 @@ homeassistant.components.tibber.*
homeassistant.components.tile.*
homeassistant.components.tilt_ble.*
homeassistant.components.time.*
homeassistant.components.time_date.*
homeassistant.components.todo.*
homeassistant.components.tolo.*
homeassistant.components.tplink.*

View File

@ -1,14 +1,14 @@
"""Support for showing the date and the time."""
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_DISPLAY_OPTIONS
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
@ -47,7 +47,7 @@ async def async_setup_platform(
) -> None:
"""Set up the Time and Date sensor."""
if hass.config.time_zone is None:
_LOGGER.error("Timezone is not set in Home Assistant configuration")
_LOGGER.error("Timezone is not set in Home Assistant configuration") # type: ignore[unreachable]
return False
async_add_entities(
@ -58,28 +58,28 @@ async def async_setup_platform(
class TimeDateSensor(SensorEntity):
"""Implementation of a Time and Date sensor."""
def __init__(self, hass, option_type):
def __init__(self, hass: HomeAssistant, option_type: str) -> None:
"""Initialize the sensor."""
self._name = OPTION_TYPES[option_type]
self.type = option_type
self._state = None
self._state: str | None = None
self.hass = hass
self.unsub = None
self.unsub: CALLBACK_TYPE | None = None
self._update_internal_state(dt_util.utcnow())
@property
def name(self):
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self):
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return self._state
@property
def icon(self):
def icon(self) -> str:
"""Icon to use in the frontend, if any."""
if "date" in self.type and "time" in self.type:
return "mdi:calendar-clock"
@ -99,7 +99,7 @@ class TimeDateSensor(SensorEntity):
self.unsub()
self.unsub = None
def get_next_interval(self):
def get_next_interval(self) -> datetime:
"""Compute next time an update should occur."""
now = dt_util.utcnow()
@ -121,7 +121,7 @@ class TimeDateSensor(SensorEntity):
return next_interval
def _update_internal_state(self, time_date):
def _update_internal_state(self, time_date: datetime) -> None:
time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT)
time_utc = time_date.strftime(TIME_STR_FORMAT)
date = dt_util.as_local(time_date).date().isoformat()
@ -155,10 +155,12 @@ class TimeDateSensor(SensorEntity):
self._state = f"@{beat:03d}"
elif self.type == "date_time_iso":
self._state = dt_util.parse_datetime(f"{date} {time}").isoformat()
self._state = dt_util.parse_datetime(
f"{date} {time}", raise_on_error=True
).isoformat()
@callback
def point_in_time_listener(self, time_date):
def point_in_time_listener(self, time_date: datetime) -> None:
"""Get the latest data and update state."""
self._update_internal_state(time_date)
self.async_write_ha_state()

View File

@ -6,7 +6,7 @@ from contextlib import suppress
import datetime as dt
from functools import partial
import re
from typing import Any
from typing import Any, Literal, overload
import zoneinfo
import ciso8601
@ -177,18 +177,41 @@ def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datet
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/main/LICENSE
@overload
def parse_datetime(dt_str: str) -> dt.datetime | None:
...
@overload
def parse_datetime(dt_str: str, *, raise_on_error: Literal[True]) -> dt.datetime:
...
@overload
def parse_datetime(
dt_str: str, *, raise_on_error: Literal[False] | bool
) -> dt.datetime | None:
...
def parse_datetime(dt_str: str, *, raise_on_error: bool = False) -> dt.datetime | None:
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
Raises ValueError if the input is well formatted but not a valid datetime.
Returns None if the input isn't well formatted.
If the input isn't well formatted, returns None if raise_on_error is False
or raises ValueError if it's True.
"""
# First try if the string can be parsed by the fast ciso8601 library
with suppress(ValueError, IndexError):
return ciso8601.parse_datetime(dt_str)
# ciso8601 failed to parse the string, fall back to regex
if not (match := DATETIME_RE.match(dt_str)):
if raise_on_error:
raise ValueError
return None
kws: dict[str, Any] = match.groupdict()
if kws["microsecond"]:

View File

@ -3492,6 +3492,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.time_date.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.todo.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -147,6 +147,12 @@ def test_parse_datetime_returns_none_for_incorrect_format() -> None:
assert dt_util.parse_datetime("not a datetime string") is None
def test_parse_datetime_raises_for_incorrect_format() -> None:
"""Test parse_datetime raises ValueError if raise_on_error is set with an incorrect format."""
with pytest.raises(ValueError):
dt_util.parse_datetime("not a datetime string", raise_on_error=True)
@pytest.mark.parametrize(
("duration_string", "expected_result"),
[