Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ff21b70f2 | |||
| d01903cd59 | |||
| 0d17f3062c | |||
| c706c83337 | |||
| 20f30f76d1 | |||
| 966b89cc14 |
@@ -735,6 +735,7 @@ class ThinQSensorEntity(ThinQEntity, SensorEntity):
|
||||
value = self.data.value
|
||||
|
||||
if isinstance(value, time):
|
||||
# pylint: disable-next=home-assistant-enforce-now
|
||||
local_now = datetime.now(
|
||||
tz=dt_util.get_time_zone(self.coordinator.hass.config.time_zone)
|
||||
)
|
||||
@@ -847,6 +848,7 @@ class ThinQEnergySensorEntity(ThinQEntity, SensorEntity):
|
||||
|
||||
async def _async_update_and_schedule(self) -> None:
|
||||
"""Update the state of the sensor."""
|
||||
# pylint: disable-next=home-assistant-enforce-now
|
||||
local_now = datetime.now(
|
||||
dt_util.get_time_zone(self.coordinator.hass.config.time_zone)
|
||||
)
|
||||
|
||||
@@ -264,9 +264,9 @@ class MetOfficeWeather(
|
||||
self.forecast_coordinators["daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
start_datetime = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=timesteps[0]["time"].tzinfo
|
||||
).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return [
|
||||
_build_daily_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
@@ -282,9 +282,9 @@ class MetOfficeWeather(
|
||||
)
|
||||
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
)
|
||||
start_datetime = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=timesteps[0]["time"].tzinfo
|
||||
).replace(minute=0, second=0, microsecond=0)
|
||||
return [
|
||||
_build_hourly_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
@@ -299,9 +299,9 @@ class MetOfficeWeather(
|
||||
self.forecast_coordinators["twice_daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
start_datetime = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=timesteps[0]["time"].tzinfo
|
||||
).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return [
|
||||
_build_twice_daily_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
|
||||
@@ -95,7 +95,9 @@ class PowerfoxReportDataUpdateCoordinator(PowerfoxBaseCoordinator[DeviceReport])
|
||||
|
||||
async def _async_fetch_data(self) -> DeviceReport:
|
||||
"""Fetch report data from the Powerfox API."""
|
||||
local_now = datetime.now(tz=dt_util.get_time_zone(self.hass.config.time_zone))
|
||||
local_now = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=dt_util.get_time_zone(self.hass.config.time_zone)
|
||||
)
|
||||
return await self.client.report(
|
||||
device_id=self.device.id,
|
||||
year=local_now.year,
|
||||
|
||||
@@ -74,9 +74,9 @@ def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
|
||||
|
||||
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
|
||||
try:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
zoned_time = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
timezone
|
||||
).replace(hour=t.hour, minute=t.minute, second=0, microsecond=0)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
utc_time = zoned_time.astimezone(UTC).time()
|
||||
|
||||
@@ -155,7 +155,9 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
# Tado resets somewhere between 12:00 and 13:00, Berlin time
|
||||
# So let's pretend we're in Berlin...
|
||||
reset_time = datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
reset_time = datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
ZoneInfo("Europe/Berlin")
|
||||
)
|
||||
|
||||
today_reset = datetime.combine(
|
||||
reset_time.date(),
|
||||
|
||||
@@ -105,6 +105,7 @@ Every check has a code following the
|
||||
| `W7421` | [`home-assistant-tests-direct-async-migrate-entry`](#w7421-home-assistant-tests-direct-async-migrate-entry) | Tests should not call an integration's `async_migrate_entry` directly |
|
||||
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
|
||||
| `C7414` | [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow) | Use `homeassistant.util.dt.utcnow` instead of `datetime.now(UTC)` |
|
||||
| `C7425` | [`home-assistant-enforce-now`](#c7425-home-assistant-enforce-now) | Use `homeassistant.util.dt.now` instead of `datetime.now(<tz>)` |
|
||||
| `W7423` | [`home-assistant-missing-entity-unique-id`](#w7423-home-assistant-missing-entity-unique-id) | Entity class does not statically guarantee a non-None unique id |
|
||||
| `W7424` | [`home-assistant-entity-unique-id-static`](#w7424-home-assistant-entity-unique-id-static) | Entity class sets `_attr_unique_id` to a static string at class level |
|
||||
| `C7412` | [`home-assistant-entity-description-redundant-default`](#c7412-home-assistant-entity-description-redundant-default) | Setting an EntityDescription field to its default value is redundant |
|
||||
@@ -458,6 +459,22 @@ lookup of `UTC` on every call, while keeping the codebase consistent in
|
||||
how the current UTC time is obtained.
|
||||
|
||||
|
||||
## `home_assistant_enforce_now` checker
|
||||
|
||||
Ensures the Home Assistant helper is used to get the current local time.
|
||||
|
||||
### `C7425`: `home-assistant-enforce-now`
|
||||
|
||||
Use `homeassistant.util.dt.now()` instead of `datetime.datetime.now(<tz>)`
|
||||
when called with a non-UTC time zone to create an aware `datetime`. The
|
||||
helper returns an aware `datetime` in the given time zone (defaulting to
|
||||
`DEFAULT_TIME_ZONE`), keeping the codebase consistent in how the current
|
||||
local time is obtained. The UTC case (`datetime.now(UTC)`) is handled by
|
||||
the [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow)
|
||||
checker, and `datetime.now()` with no argument is not flagged since it
|
||||
returns a naive local `datetime`.
|
||||
|
||||
|
||||
## `home_assistant_entity_unique_id` checker
|
||||
|
||||
Quality-scale-gated checker for the [`entity-unique-id`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/entity-unique-id)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Checker that enforces ``homeassistant.util.dt.now`` over ``datetime.now(tz)``.
|
||||
|
||||
Home Assistant exposes ``homeassistant.util.dt.now`` -- a helper that returns an
|
||||
aware ``datetime`` in the given time zone (defaulting to ``DEFAULT_TIME_ZONE``).
|
||||
Calling ``datetime.datetime.now(tz)`` directly with a time zone argument does the
|
||||
same thing but bypasses the helper. Using ``dt_util.now`` keeps the codebase
|
||||
consistent in how the current local time is obtained.
|
||||
|
||||
The UTC special case (``datetime.now(UTC)``) is intentionally left to the
|
||||
``home-assistant-enforce-utcnow`` checker, which steers it to the faster
|
||||
``dt_util.utcnow`` partial. ``datetime.now()`` with no argument returns a naive
|
||||
local ``datetime`` and is therefore not equivalent to ``dt_util.now()``; it is not
|
||||
flagged.
|
||||
"""
|
||||
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.helpers.datetime_now import HassEnforceDatetimeNowChecker
|
||||
|
||||
|
||||
class HassEnforceNowChecker(HassEnforceDatetimeNowChecker):
|
||||
"""Checker that flags ``datetime.now(tz)`` calls with a non-UTC time zone."""
|
||||
|
||||
name = "home_assistant_enforce_now"
|
||||
msgs = {
|
||||
"C7425": (
|
||||
"Use `homeassistant.util.dt.now()` instead of `datetime.now(<tz>)`",
|
||||
"home-assistant-enforce-now",
|
||||
"Used when ``datetime.datetime.now(<tz>)`` is called with a non-UTC "
|
||||
"time zone to create an aware ``datetime``. Use the "
|
||||
"``homeassistant.util.dt.now`` helper instead. The UTC case is "
|
||||
"handled by the ``home-assistant-enforce-utcnow`` checker.",
|
||||
),
|
||||
}
|
||||
|
||||
message = "home-assistant-enforce-now"
|
||||
flags_utc = False
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(HassEnforceNowChecker(linter))
|
||||
@@ -6,44 +6,15 @@ helper avoids the per-call global lookup of ``UTC`` and keeps the codebase
|
||||
consistent in how the current UTC time is obtained.
|
||||
"""
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
# ``homeassistant.util.dt`` defines ``utcnow`` itself, so it must call
|
||||
# ``datetime.datetime.now(UTC)`` directly.
|
||||
_SKIP_MODULES = frozenset({"homeassistant.util.dt"})
|
||||
from pylint_home_assistant.helpers.datetime_now import HassEnforceDatetimeNowChecker
|
||||
|
||||
|
||||
def _attribute_path(node: nodes.NodeNG) -> tuple[str, ...] | None:
|
||||
"""Return the dotted-name path of an Attribute/Name chain, or ``None``."""
|
||||
parts: list[str] = []
|
||||
while isinstance(node, nodes.Attribute):
|
||||
parts.append(node.attrname)
|
||||
node = node.expr
|
||||
if not isinstance(node, nodes.Name):
|
||||
return None
|
||||
parts.append(node.name)
|
||||
return tuple(reversed(parts))
|
||||
|
||||
|
||||
def _is_zoneinfo_utc(node: nodes.NodeNG) -> bool:
|
||||
"""Return True if *node* is ``ZoneInfo("UTC")`` or ``*.ZoneInfo("UTC")``."""
|
||||
match node:
|
||||
case nodes.Call(
|
||||
func=nodes.Name(name="ZoneInfo") | nodes.Attribute(attrname="ZoneInfo"),
|
||||
args=[nodes.Const(value="UTC")],
|
||||
keywords=[],
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HassEnforceUtcnowChecker(BaseChecker):
|
||||
class HassEnforceUtcnowChecker(HassEnforceDatetimeNowChecker):
|
||||
"""Checker that flags ``datetime.now(UTC)`` calls."""
|
||||
|
||||
name = "home_assistant_enforce_utcnow"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"C7414": (
|
||||
"Use `homeassistant.util.dt.utcnow()` instead of `datetime.now(UTC)`",
|
||||
@@ -54,81 +25,9 @@ class HassEnforceUtcnowChecker(BaseChecker):
|
||||
"and avoids the global lookup of ``UTC`` on every call.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_enabled: bool
|
||||
_datetime_class_paths: set[tuple[str, ...]]
|
||||
_utc_paths: set[tuple[str, ...]]
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Collect ``datetime`` bindings introduced by module-level imports."""
|
||||
self._datetime_class_paths = set()
|
||||
self._utc_paths = set()
|
||||
self._enabled = node.name not in _SKIP_MODULES
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
for stmt in node.body:
|
||||
match stmt:
|
||||
case nodes.ImportFrom(modname="datetime", names=names):
|
||||
for name, alias in names:
|
||||
local = alias or name
|
||||
match name:
|
||||
case "datetime":
|
||||
self._datetime_class_paths.add((local,))
|
||||
case "UTC":
|
||||
self._utc_paths.add((local,))
|
||||
case "timezone":
|
||||
self._utc_paths.add((local, "utc"))
|
||||
case nodes.ImportFrom(modname="homeassistant.util", names=names):
|
||||
# ``homeassistant.util.dt`` re-exports ``UTC`` from
|
||||
# ``datetime``, so ``dt_util.UTC`` must be flagged too.
|
||||
for name, alias in names:
|
||||
if name == "dt":
|
||||
local = alias or name
|
||||
self._utc_paths.add((local, "UTC"))
|
||||
case nodes.ImportFrom(modname="homeassistant.util.dt", names=names):
|
||||
for name, alias in names:
|
||||
if name == "UTC":
|
||||
self._utc_paths.add((alias or name,))
|
||||
case nodes.Import(names=names):
|
||||
for name, alias in names:
|
||||
match name:
|
||||
case "datetime":
|
||||
local = alias or name
|
||||
self._datetime_class_paths.add((local, "datetime"))
|
||||
self._utc_paths.add((local, "UTC"))
|
||||
self._utc_paths.add((local, "timezone", "utc"))
|
||||
case "homeassistant.util.dt" if alias:
|
||||
self._utc_paths.add((alias, "UTC"))
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Check for ``datetime.now(UTC)`` calls."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
match node:
|
||||
case nodes.Call(
|
||||
func=nodes.Attribute(attrname="now", expr=expr),
|
||||
args=[arg],
|
||||
keywords=[],
|
||||
):
|
||||
pass
|
||||
case nodes.Call(
|
||||
func=nodes.Attribute(attrname="now", expr=expr),
|
||||
args=[],
|
||||
keywords=[nodes.Keyword(arg="tz", value=arg)],
|
||||
):
|
||||
pass
|
||||
case _:
|
||||
return
|
||||
|
||||
if _attribute_path(expr) not in self._datetime_class_paths:
|
||||
return
|
||||
if _attribute_path(arg) not in self._utc_paths and not _is_zoneinfo_utc(arg):
|
||||
return
|
||||
|
||||
self.add_message("home-assistant-enforce-utcnow", node=node)
|
||||
message = "home-assistant-enforce-utcnow"
|
||||
flags_utc = True
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Shared logic for the ``datetime.now`` enforcement checkers.
|
||||
|
||||
Both the ``home-assistant-enforce-now`` and ``home-assistant-enforce-utcnow``
|
||||
checkers look for ``datetime.datetime.now(<tz>)`` calls and differ only in which
|
||||
time zone argument they care about. The common detection lives here; each checker
|
||||
module just declares its message and whether it fires on the UTC case.
|
||||
"""
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
|
||||
# ``homeassistant.util.dt`` defines ``now``/``utcnow`` itself, so it must call
|
||||
# ``datetime.datetime.now(...)`` directly.
|
||||
SKIP_MODULES = frozenset({"homeassistant.util.dt"})
|
||||
|
||||
|
||||
def attribute_path(node: nodes.NodeNG) -> tuple[str, ...] | None:
|
||||
"""Return the dotted-name path of an Attribute/Name chain, or ``None``."""
|
||||
parts: list[str] = []
|
||||
while isinstance(node, nodes.Attribute):
|
||||
parts.append(node.attrname)
|
||||
node = node.expr
|
||||
if not isinstance(node, nodes.Name):
|
||||
return None
|
||||
parts.append(node.name)
|
||||
return tuple(reversed(parts))
|
||||
|
||||
|
||||
def is_zoneinfo_utc(node: nodes.NodeNG) -> bool:
|
||||
"""Return True if *node* is ``ZoneInfo("UTC")`` or ``*.ZoneInfo("UTC")``."""
|
||||
match node:
|
||||
case nodes.Call(
|
||||
func=nodes.Name(name="ZoneInfo") | nodes.Attribute(attrname="ZoneInfo"),
|
||||
args=[nodes.Const(value="UTC")],
|
||||
keywords=[],
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HassEnforceDatetimeNowChecker(BaseChecker):
|
||||
"""Base checker for ``datetime.datetime.now(<tz>)`` calls.
|
||||
|
||||
Subclasses must define ``name`` and ``msgs`` and set:
|
||||
|
||||
- ``message``: the message symbol to emit.
|
||||
- ``flags_utc``: ``True`` to fire on the UTC case, ``False`` to fire on every
|
||||
other (non-UTC) time zone.
|
||||
"""
|
||||
|
||||
priority = -1
|
||||
options = ()
|
||||
|
||||
message: str
|
||||
flags_utc: bool
|
||||
|
||||
_enabled: bool
|
||||
_datetime_class_paths: set[tuple[str, ...]]
|
||||
_utc_paths: set[tuple[str, ...]]
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Collect ``datetime`` bindings introduced by module-level imports."""
|
||||
self._datetime_class_paths = set()
|
||||
self._utc_paths = set()
|
||||
self._enabled = node.name not in SKIP_MODULES
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
for stmt in node.body:
|
||||
match stmt:
|
||||
case nodes.ImportFrom(modname="datetime", names=names):
|
||||
for name, alias in names:
|
||||
local = alias or name
|
||||
match name:
|
||||
case "datetime":
|
||||
self._datetime_class_paths.add((local,))
|
||||
case "UTC":
|
||||
self._utc_paths.add((local,))
|
||||
case "timezone":
|
||||
self._utc_paths.add((local, "utc"))
|
||||
case nodes.ImportFrom(modname="homeassistant.util", names=names):
|
||||
# ``homeassistant.util.dt`` re-exports ``UTC`` from
|
||||
# ``datetime``, so ``dt_util.UTC`` must be flagged too.
|
||||
for name, alias in names:
|
||||
if name == "dt":
|
||||
local = alias or name
|
||||
self._utc_paths.add((local, "UTC"))
|
||||
case nodes.ImportFrom(modname="homeassistant.util.dt", names=names):
|
||||
for name, alias in names:
|
||||
if name == "UTC":
|
||||
self._utc_paths.add((alias or name,))
|
||||
case nodes.Import(names=names):
|
||||
for name, alias in names:
|
||||
match name:
|
||||
case "datetime":
|
||||
local = alias or name
|
||||
self._datetime_class_paths.add((local, "datetime"))
|
||||
self._utc_paths.add((local, "UTC"))
|
||||
self._utc_paths.add((local, "timezone", "utc"))
|
||||
case "homeassistant.util.dt" if alias:
|
||||
self._utc_paths.add((alias, "UTC"))
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Check for ``datetime.now(<tz>)`` calls matching the configured case."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
match node:
|
||||
case nodes.Call(
|
||||
func=nodes.Attribute(attrname="now", expr=expr),
|
||||
args=[arg],
|
||||
keywords=[],
|
||||
):
|
||||
pass
|
||||
case nodes.Call(
|
||||
func=nodes.Attribute(attrname="now", expr=expr),
|
||||
args=[],
|
||||
keywords=[nodes.Keyword(arg="tz", value=arg)],
|
||||
):
|
||||
pass
|
||||
case _:
|
||||
return
|
||||
|
||||
if attribute_path(expr) not in self._datetime_class_paths:
|
||||
return
|
||||
is_utc = attribute_path(arg) in self._utc_paths or is_zoneinfo_utc(arg)
|
||||
if is_utc is not self.flags_utc:
|
||||
return
|
||||
|
||||
self.add_message(self.message, node=node)
|
||||
@@ -537,7 +537,9 @@ async def test_add_event_date_time(
|
||||
mock_events_list({})
|
||||
assert await component_setup()
|
||||
|
||||
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
|
||||
start_datetime = datetime.datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=zoneinfo.ZoneInfo("America/Regina")
|
||||
)
|
||||
delta = datetime.timedelta(days=3, hours=3)
|
||||
end_datetime = start_datetime + delta
|
||||
|
||||
@@ -600,7 +602,9 @@ async def test_unsupported_create_event(
|
||||
mock_events_list({})
|
||||
assert await component_setup()
|
||||
|
||||
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
|
||||
start_datetime = datetime.datetime.now( # pylint: disable=home-assistant-enforce-now
|
||||
tz=zoneinfo.ZoneInfo("America/Regina")
|
||||
)
|
||||
delta = datetime.timedelta(days=3, hours=3)
|
||||
end_datetime = start_datetime + delta
|
||||
entity_id = "calendar.backyard_light"
|
||||
|
||||
@@ -17,6 +17,7 @@ from pylint_home_assistant.checkers.greek_micro_char import (
|
||||
HassEnforceGreekMicroCharChecker,
|
||||
)
|
||||
from pylint_home_assistant.checkers.imports import HassImportsFormatChecker
|
||||
from pylint_home_assistant.checkers.now import HassEnforceNowChecker
|
||||
from pylint_home_assistant.checkers.runtime_data import HassEnforceRuntimeDataChecker
|
||||
from pylint_home_assistant.checkers.sorted_platforms import (
|
||||
HassEnforceSortedPlatformsChecker,
|
||||
@@ -131,6 +132,14 @@ def enforce_greek_micro_char_checker_fixture(linter: UnittestLinter) -> BaseChec
|
||||
return enforce_greek_micro_char_checker
|
||||
|
||||
|
||||
@pytest.fixture(name="enforce_now_checker")
|
||||
def enforce_now_checker_fixture(linter: UnittestLinter) -> BaseChecker:
|
||||
"""Fixture to provide a now checker."""
|
||||
enforce_now_checker = HassEnforceNowChecker(linter)
|
||||
enforce_now_checker.module = "homeassistant.components.pylint_test"
|
||||
return enforce_now_checker
|
||||
|
||||
|
||||
@pytest.fixture(name="enforce_utcnow_checker")
|
||||
def enforce_utcnow_checker_fixture(linter: UnittestLinter) -> BaseChecker:
|
||||
"""Fixture to provide a utcnow checker."""
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Tests for the home-assistant-enforce-now checker."""
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
now = dt_util.now()
|
||||
""",
|
||||
id="now_helper",
|
||||
),
|
||||
pytest.param(
|
||||
# Calling ``datetime.now()`` with no argument returns naive local
|
||||
# time, which is not equivalent to ``dt_util.now()``.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now()
|
||||
""",
|
||||
id="now_no_args",
|
||||
),
|
||||
pytest.param(
|
||||
# The UTC case is handled by the ``enforce-utcnow`` checker.
|
||||
"""
|
||||
from datetime import datetime, UTC
|
||||
|
||||
now = datetime.now(UTC)
|
||||
""",
|
||||
id="now_with_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
""",
|
||||
id="now_with_timezone_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import datetime
|
||||
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
""",
|
||||
id="qualified_now_with_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now = datetime.now(ZoneInfo("UTC"))
|
||||
""",
|
||||
id="now_with_zoneinfo_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime, UTC
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
""",
|
||||
id="kwarg_now_with_utc",
|
||||
),
|
||||
pytest.param(
|
||||
# ``UTC`` re-exported from ``homeassistant.util.dt`` is still the
|
||||
# UTC case, handled by the ``enforce-utcnow`` checker.
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
now = datetime.datetime.now(dt_util.UTC)
|
||||
""",
|
||||
id="dt_util_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
now = datetime.datetime.now(tz=dt_util.UTC)
|
||||
""",
|
||||
id="kwarg_dt_util_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime
|
||||
from homeassistant.util.dt import UTC
|
||||
|
||||
now = datetime.now(UTC)
|
||||
""",
|
||||
id="from_util_dt_import_utc",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
now = datetime.datetime.now(dt_util.UTC)
|
||||
""",
|
||||
id="import_util_dt_as_dt_util_utc",
|
||||
),
|
||||
pytest.param(
|
||||
# Calling ``.now`` on something that is not ``datetime.datetime``
|
||||
# must not be flagged.
|
||||
"""
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
class Counter:
|
||||
def now(self, tz):
|
||||
return 0
|
||||
|
||||
Counter().now(ZoneInfo("Europe/Stockholm"))
|
||||
""",
|
||||
id="other_now_method",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_enforce_now_good(
|
||||
linter: UnittestLinter,
|
||||
enforce_now_checker: BaseChecker,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""Good test cases -- no message expected."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now = datetime.now(ZoneInfo("Europe/Stockholm"))
|
||||
""",
|
||||
id="from_import_zoneinfo",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now = datetime.now(tz=ZoneInfo("Europe/Stockholm"))
|
||||
""",
|
||||
id="kwarg_zoneinfo",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import datetime
|
||||
import zoneinfo
|
||||
|
||||
now = datetime.datetime.now(zoneinfo.ZoneInfo("Europe/Stockholm"))
|
||||
""",
|
||||
id="qualified_datetime",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
import datetime as dt
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now = dt.datetime.now(ZoneInfo("Europe/Stockholm"))
|
||||
""",
|
||||
id="aliased_datetime",
|
||||
),
|
||||
pytest.param(
|
||||
# A time zone passed in as a variable is still flagged.
|
||||
"""
|
||||
from datetime import datetime, tzinfo
|
||||
|
||||
def get_now(time_zone: tzinfo) -> datetime:
|
||||
return datetime.now(time_zone)
|
||||
""",
|
||||
id="variable_tz",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from datetime import datetime, tzinfo
|
||||
|
||||
def get_now(time_zone: tzinfo) -> datetime:
|
||||
return datetime.now(tz=time_zone)
|
||||
""",
|
||||
id="kwarg_variable_tz",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_enforce_now_bad(
|
||||
linter: UnittestLinter,
|
||||
enforce_now_checker: BaseChecker,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""Bad test cases -- one message expected per call."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-enforce-now"
|
||||
|
||||
|
||||
def test_enforce_now_skips_util_dt(
|
||||
linter: UnittestLinter,
|
||||
enforce_now_checker: BaseChecker,
|
||||
) -> None:
|
||||
"""``homeassistant.util.dt`` defines ``now`` itself, so it is skipped."""
|
||||
code = """
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
now = datetime.now(ZoneInfo("Europe/Stockholm"))
|
||||
"""
|
||||
root_node = astroid.parse(code, "homeassistant.util.dt")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
Reference in New Issue
Block a user