1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +00:00

Add internal util.snakecase, use instead of stringcase (#156775)

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ville Skyttä
2025-11-22 16:19:15 +02:00
committed by GitHub
parent ca2e8bfb56
commit 8a2e8d2c61
12 changed files with 53 additions and 68 deletions

View File

@@ -3,11 +3,8 @@
from __future__ import annotations
import logging
import re
from typing import Any, cast
from stringcase import snakecase
from homeassistant.components.device_tracker import (
DOMAIN as DEVICE_TRACKER_DOMAIN,
ScannerEntity,
@@ -18,6 +15,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import snakecase
from . import Router
from .const import (
@@ -156,22 +154,6 @@ def async_add_new_entities(
async_add_entities(new_entities, True)
def _better_snakecase(text: str) -> str:
# Awaiting https://github.com/okunishinishi/python-stringcase/pull/18
if text == text.upper():
# All uppercase to all lowercase to get http for HTTP, not h_t_t_p
text = text.lower()
else:
# Three or more consecutive uppercase with middle part lowercased
# to get http_response for HTTPResponse, not h_t_t_p_response
text = re.sub(
r"([A-Z])([A-Z]+)([A-Z](?:[^A-Z]|$))",
lambda match: f"{match.group(1)}{match.group(2).lower()}{match.group(3)}",
text,
)
return cast(str, snakecase(text))
class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
"""Huawei LTE router scanner entity."""
@@ -235,7 +217,7 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
self._ip_address = (host.get("IpAddress") or "").split(";", 2)[0] or None
self._hostname = host.get("HostName")
self._extra_state_attributes = {
_better_snakecase(k): v
snakecase(k): v
for k, v in host.items()
if k
in {

View File

@@ -6,11 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"iot_class": "local_polling",
"loggers": ["huawei_lte_api.Session"],
"requirements": [
"huawei-lte-api==1.11.0",
"stringcase==1.2.0",
"url-normalize==2.2.1"
],
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==2.2.1"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",

View File

@@ -13,8 +13,8 @@ rules:
status: todo
comment: See if we can catch more specific exceptions in get_device_info.
dependency-transparency:
status: todo
comment: stringcase is not built and published to PyPI from a public CI pipeline. huawei-lte-api is from https://gitlab.salamek.cz/Mirrors/huawei-lte-api, see https://github.com/Salamek/huawei-lte-api/issues/253
status: done
comment: huawei-lte-api is from https://gitlab.salamek.cz/Mirrors/huawei-lte-api, see https://github.com/Salamek/huawei-lte-api/issues/253
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
@@ -82,5 +82,4 @@ rules:
status: exempt
comment: Underlying huawei-lte-api does not use aiohttp or httpx, so this does not apply.
strict-typing:
status: todo
comment: Integration is strictly typechecked already, and huawei-lte-api and url-normalize are in order. stringcase is not typed.
status: done

View File

@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any
from aiosolaredge import SolarEdge
from solaredge_web import EnergyData, SolarEdgeWeb, TimeUnit
from stringcase import snakecase
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
@@ -26,7 +25,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from homeassistant.util import dt as dt_util, snakecase
from homeassistant.util.unit_conversion import EnergyConverter
from .const import (

View File

@@ -14,9 +14,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aiosolaredge", "solaredge_web"],
"requirements": [
"aiosolaredge==0.2.0",
"stringcase==1.2.0",
"solaredge-web==0.0.1"
]
"requirements": ["aiosolaredge==0.2.0", "solaredge-web==0.0.1"]
}

View File

@@ -9,7 +9,7 @@ import logging
from requests import RequestException
from requests.exceptions import HTTPError
from stringcase import camelcase, snakecase
from stringcase import camelcase
import thermoworks_smoke
import voluptuous as vol
@@ -30,6 +30,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import snakecase
_LOGGER = logging.getLogger(__name__)

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/traccar",
"iot_class": "cloud_push",
"loggers": ["pytraccar"],
"requirements": ["pytraccar==3.0.0", "stringcase==1.2.0"]
"requirements": ["pytraccar==3.0.0"]
}

View File

@@ -97,6 +97,20 @@ def get_random_string(length: int = 10) -> str:
return "".join(generator.choice(source_chars) for _ in range(length))
# Adapted from https://github.com/okunishinishi/python-stringcase, with improvements
def snakecase(text: str) -> str:
"""Convert a string to snake_case."""
text = re.sub(r"[\s.-]", "_", text)
if not text.isupper():
# Underscore before last uppercase of groups of 2+ uppercase ("HTTPResponse", "IPAddress")
text = re.sub(
r"[A-Z]{2,}(?=[A-Z][^A-Z])", lambda match: match.group(0) + "_", text
)
# Underscore between non-uppercase followed by uppercase
text = re.sub(r"(?<=[^A-Z_])[A-Z]", lambda match: "_" + match.group(0), text)
return text.lower()
class Throttle:
"""A class for throttling the execution of tasks.

5
requirements_all.txt generated
View File

@@ -2899,11 +2899,6 @@ stookwijzer==1.6.1
# homeassistant.components.streamlabswater
streamlabswater==1.0.1
# homeassistant.components.huawei_lte
# homeassistant.components.solaredge
# homeassistant.components.traccar
stringcase==1.2.0
# homeassistant.components.subaru
subarulink==0.7.15

View File

@@ -2408,11 +2408,6 @@ stookwijzer==1.6.1
# homeassistant.components.streamlabswater
streamlabswater==1.0.1
# homeassistant.components.huawei_lte
# homeassistant.components.solaredge
# homeassistant.components.traccar
stringcase==1.2.0
# homeassistant.components.subaru
subarulink==0.7.15

View File

@@ -1,20 +0,0 @@
"""Huawei LTE device tracker tests."""
import pytest
from homeassistant.components.huawei_lte import device_tracker
@pytest.mark.parametrize(
("value", "expected"),
[
("HTTP", "http"),
("ID", "id"),
("IPAddress", "ip_address"),
("HTTPResponse", "http_response"),
("foo_bar", "foo_bar"),
],
)
def test_better_snakecase(value, expected) -> None:
"""Test that better snakecase works better."""
assert device_tracker._better_snakecase(value) == expected

View File

@@ -233,3 +233,31 @@ async def test_throttle_async() -> None:
assert (await test_method2()) is True
assert (await test_method2()) is None
@pytest.mark.parametrize(
("input", "expected"),
[
("fooBar", "foo_bar"),
("FooBar", "foo_bar"),
("Foobar", "foobar"),
("Foo.bar", "foo_bar"),
("_foo-Bar", "_foo_bar"),
("HTTP", "http"),
("HTTPResponse", "http_response"),
("iPhone", "i_phone"),
("IPAddress", "ip_address"),
("IP_Address", "ip_address"),
("My IP Address", "my_ip_address"),
("LocalIP", "local_ip"),
("Python3Thing", "python3_thing"),
("mTLS", "m_tls"),
("DTrace", "dtrace"),
("IPv4", "ipv4"),
("ID", "id"),
("", ""),
],
)
def test_snakecase(input, expected) -> None:
"""Test snake casing a string."""
assert util.snakecase(input) == expected