mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02: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:
@ -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.*
|
||||
|
@ -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()
|
||||
|
@ -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"]:
|
||||
|
10
mypy.ini
10
mypy.ini
@ -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
|
||||
|
@ -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"),
|
||||
[
|
||||
|
Reference in New Issue
Block a user