mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
15cdd42c99
commit
8f9bd75a36
@ -373,6 +373,7 @@ homeassistant.components.tibber.*
|
|||||||
homeassistant.components.tile.*
|
homeassistant.components.tile.*
|
||||||
homeassistant.components.tilt_ble.*
|
homeassistant.components.tilt_ble.*
|
||||||
homeassistant.components.time.*
|
homeassistant.components.time.*
|
||||||
|
homeassistant.components.time_date.*
|
||||||
homeassistant.components.todo.*
|
homeassistant.components.todo.*
|
||||||
homeassistant.components.tolo.*
|
homeassistant.components.tolo.*
|
||||||
homeassistant.components.tplink.*
|
homeassistant.components.tplink.*
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"""Support for showing the date and the time."""
|
"""Support for showing the date and the time."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||||
from homeassistant.const import CONF_DISPLAY_OPTIONS
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
@ -47,7 +47,7 @@ async def async_setup_platform(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Time and Date sensor."""
|
"""Set up the Time and Date sensor."""
|
||||||
if hass.config.time_zone is None:
|
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
|
return False
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
@ -58,28 +58,28 @@ async def async_setup_platform(
|
|||||||
class TimeDateSensor(SensorEntity):
|
class TimeDateSensor(SensorEntity):
|
||||||
"""Implementation of a Time and Date sensor."""
|
"""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."""
|
"""Initialize the sensor."""
|
||||||
self._name = OPTION_TYPES[option_type]
|
self._name = OPTION_TYPES[option_type]
|
||||||
self.type = option_type
|
self.type = option_type
|
||||||
self._state = None
|
self._state: str | None = None
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.unsub = None
|
self.unsub: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
self._update_internal_state(dt_util.utcnow())
|
self._update_internal_state(dt_util.utcnow())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self) -> str | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self) -> str:
|
||||||
"""Icon to use in the frontend, if any."""
|
"""Icon to use in the frontend, if any."""
|
||||||
if "date" in self.type and "time" in self.type:
|
if "date" in self.type and "time" in self.type:
|
||||||
return "mdi:calendar-clock"
|
return "mdi:calendar-clock"
|
||||||
@ -99,7 +99,7 @@ class TimeDateSensor(SensorEntity):
|
|||||||
self.unsub()
|
self.unsub()
|
||||||
self.unsub = None
|
self.unsub = None
|
||||||
|
|
||||||
def get_next_interval(self):
|
def get_next_interval(self) -> datetime:
|
||||||
"""Compute next time an update should occur."""
|
"""Compute next time an update should occur."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ class TimeDateSensor(SensorEntity):
|
|||||||
|
|
||||||
return next_interval
|
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 = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT)
|
||||||
time_utc = time_date.strftime(TIME_STR_FORMAT)
|
time_utc = time_date.strftime(TIME_STR_FORMAT)
|
||||||
date = dt_util.as_local(time_date).date().isoformat()
|
date = dt_util.as_local(time_date).date().isoformat()
|
||||||
@ -155,10 +155,12 @@ class TimeDateSensor(SensorEntity):
|
|||||||
|
|
||||||
self._state = f"@{beat:03d}"
|
self._state = f"@{beat:03d}"
|
||||||
elif self.type == "date_time_iso":
|
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
|
@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."""
|
"""Get the latest data and update state."""
|
||||||
self._update_internal_state(time_date)
|
self._update_internal_state(time_date)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -6,7 +6,7 @@ from contextlib import suppress
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any, Literal, overload
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
|
||||||
import ciso8601
|
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.
|
# Copyright (c) Django Software Foundation and individual contributors.
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
# https://github.com/django/django/blob/main/LICENSE
|
# https://github.com/django/django/blob/main/LICENSE
|
||||||
|
@overload
|
||||||
def parse_datetime(dt_str: str) -> dt.datetime | None:
|
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.
|
"""Parse a string and return a datetime.datetime.
|
||||||
|
|
||||||
This function supports time zone offsets. When the input contains one,
|
This function supports time zone offsets. When the input contains one,
|
||||||
the output uses a timezone with a fixed offset from UTC.
|
the output uses a timezone with a fixed offset from UTC.
|
||||||
Raises ValueError if the input is well formatted but not a valid datetime.
|
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):
|
with suppress(ValueError, IndexError):
|
||||||
return ciso8601.parse_datetime(dt_str)
|
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 not (match := DATETIME_RE.match(dt_str)):
|
||||||
|
if raise_on_error:
|
||||||
|
raise ValueError
|
||||||
return None
|
return None
|
||||||
kws: dict[str, Any] = match.groupdict()
|
kws: dict[str, Any] = match.groupdict()
|
||||||
if kws["microsecond"]:
|
if kws["microsecond"]:
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -3492,6 +3492,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.todo.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -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
|
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(
|
@pytest.mark.parametrize(
|
||||||
("duration_string", "expected_result"),
|
("duration_string", "expected_result"),
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user