diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 83e82bf17ff..58e61c80bfe 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -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 { diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index c2e945e9c49..63e9674565f 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -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", diff --git a/homeassistant/components/huawei_lte/quality_scale.yaml b/homeassistant/components/huawei_lte/quality_scale.yaml index 5d8a87b2843..57fce90fdd6 100644 --- a/homeassistant/components/huawei_lte/quality_scale.yaml +++ b/homeassistant/components/huawei_lte/quality_scale.yaml @@ -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 diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index e69ed045024..37e871454d7 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -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 ( diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 15796f99ef3..295d562778f 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -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"] } diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 7ce0dfb9993..46d0301db83 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -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__) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 5d3b8361f1b..338b9d1ab8e 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -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"] } diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 17a4a86f106..f57966b2a30 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -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. diff --git a/requirements_all.txt b/requirements_all.txt index 65aa7a1ff4f..a573ed2bc12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7273d432b8e..e9784ae09c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/huawei_lte/test_device_tracker.py b/tests/components/huawei_lte/test_device_tracker.py deleted file mode 100644 index 56eb594fca0..00000000000 --- a/tests/components/huawei_lte/test_device_tracker.py +++ /dev/null @@ -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 diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 111b086b48b..47e9981902c 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -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