From 8923fae082b72365bb36fb28c3b68140c5d6d2c6 Mon Sep 17 00:00:00 2001 From: Indu Prakash Date: Thu, 3 Oct 2024 06:59:35 -0500 Subject: [PATCH] Updated homeassistant to 2024.10.0 * Updated plugins --- .../pylint/plugins/hass_decorator.py | 119 +++++++++ .../plugins/hass_enforce_class_module.py | 168 +++++++++++++ .../hass_enforce_coordinator_module.py | 55 ----- .../pylint/plugins/hass_enforce_type_hints.py | 167 ++++++------- .../pylint/plugins/hass_imports.py | 227 +++++++++++++++--- container_content/pyproject.toml | 91 +++++-- requirements.txt | 93 +++---- 7 files changed, 683 insertions(+), 237 deletions(-) create mode 100644 container_content/pylint/plugins/hass_decorator.py create mode 100644 container_content/pylint/plugins/hass_enforce_class_module.py delete mode 100644 container_content/pylint/plugins/hass_enforce_coordinator_module.py diff --git a/container_content/pylint/plugins/hass_decorator.py b/container_content/pylint/plugins/hass_decorator.py new file mode 100644 index 0000000..7e50977 --- /dev/null +++ b/container_content/pylint/plugins/hass_decorator.py @@ -0,0 +1,119 @@ +"""Plugin to check decorators.""" + +from __future__ import annotations + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + + +class HassDecoratorChecker(BaseChecker): + """Checker for decorators.""" + + name = "hass_decorator" + priority = -1 + msgs = { + "W7471": ( + "A coroutine function should not be decorated with @callback", + "hass-async-callback-decorator", + "Used when a coroutine function has an invalid @callback decorator", + ), + "W7472": ( + "Fixture %s is invalid here, please %s", + "hass-pytest-fixture-decorator", + "Used when a pytest fixture is invalid", + ), + } + + def _get_pytest_fixture_node(self, node: nodes.FunctionDef) -> nodes.Call | None: + for decorator in node.decorators.nodes: + if ( + isinstance(decorator, nodes.Call) + and decorator.func.as_string() == "pytest.fixture" + ): + return decorator + + return None + + def _get_pytest_fixture_node_keyword( + self, decorator: nodes.Call, search_arg: str + ) -> nodes.Keyword | None: + for keyword in decorator.keywords: + if keyword.arg == search_arg: + return keyword + + return None + + def _check_pytest_fixture( + self, node: nodes.FunctionDef, decoratornames: set[str] + ) -> None: + if ( + "_pytest.fixtures.FixtureFunctionMarker" not in decoratornames + or not (root_name := node.root().name).startswith("tests.") + or (decorator := self._get_pytest_fixture_node(node)) is None + or not ( + scope_keyword := self._get_pytest_fixture_node_keyword( + decorator, "scope" + ) + ) + or not isinstance(scope_keyword.value, nodes.Const) + or not (scope := scope_keyword.value.value) + ): + return + + parts = root_name.split(".") + test_component: str | None = None + if root_name.startswith("tests.components.") and parts[2] != "conftest": + test_component = parts[2] + + if scope == "session": + if test_component: + self.add_message( + "hass-pytest-fixture-decorator", + node=decorator, + args=("scope `session`", "use `package` or lower"), + ) + return + if not ( + autouse_keyword := self._get_pytest_fixture_node_keyword( + decorator, "autouse" + ) + ) or ( + isinstance(autouse_keyword.value, nodes.Const) + and not autouse_keyword.value.value + ): + self.add_message( + "hass-pytest-fixture-decorator", + node=decorator, + args=( + "scope/autouse combination", + "set `autouse=True` or reduce scope", + ), + ) + return + + test_module = parts[3] if len(parts) > 3 else "" + + if test_component and scope == "package" and test_module != "conftest": + self.add_message( + "hass-pytest-fixture-decorator", + node=decorator, + args=("scope `package`", "use `module` or lower"), + ) + + def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: + """Apply checks on an AsyncFunctionDef node.""" + if decoratornames := node.decoratornames(): + if "homeassistant.core.callback" in decoratornames: + self.add_message("hass-async-callback-decorator", node=node) + self._check_pytest_fixture(node, decoratornames) + + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Apply checks on an AsyncFunctionDef node.""" + if decoratornames := node.decoratornames(): + self._check_pytest_fixture(node, decoratornames) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassDecoratorChecker(linter)) diff --git a/container_content/pylint/plugins/hass_enforce_class_module.py b/container_content/pylint/plugins/hass_enforce_class_module.py new file mode 100644 index 0000000..09fe61b --- /dev/null +++ b/container_content/pylint/plugins/hass_enforce_class_module.py @@ -0,0 +1,168 @@ +"""Plugin for checking if class is in correct module.""" + +from __future__ import annotations + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + +from homeassistant.const import Platform + +_BASE_ENTITY_MODULES: set[str] = { + "BaseCoordinatorEntity", + "CoordinatorEntity", + "Entity", + "EntityDescription", + "ManualTriggerEntity", + "RestoreEntity", + "ToggleEntity", + "ToggleEntityDescription", + "TriggerBaseEntity", +} +_MODULES: dict[str, set[str]] = { + "air_quality": {"AirQualityEntity"}, + "alarm_control_panel": { + "AlarmControlPanelEntity", + "AlarmControlPanelEntityDescription", + }, + "assist_satellite": {"AssistSatelliteEntity", "AssistSatelliteEntityDescription"}, + "binary_sensor": {"BinarySensorEntity", "BinarySensorEntityDescription"}, + "button": {"ButtonEntity", "ButtonEntityDescription"}, + "calendar": {"CalendarEntity", "CalendarEntityDescription"}, + "camera": {"Camera", "CameraEntityDescription"}, + "climate": {"ClimateEntity", "ClimateEntityDescription"}, + "coordinator": {"DataUpdateCoordinator"}, + "conversation": {"ConversationEntity"}, + "cover": {"CoverEntity", "CoverEntityDescription"}, + "date": {"DateEntity", "DateEntityDescription"}, + "datetime": {"DateTimeEntity", "DateTimeEntityDescription"}, + "device_tracker": { + "DeviceTrackerEntity", + "ScannerEntity", + "ScannerEntityDescription", + "TrackerEntity", + "TrackerEntityDescription", + }, + "event": {"EventEntity", "EventEntityDescription"}, + "fan": {"FanEntity", "FanEntityDescription"}, + "geo_location": {"GeolocationEvent"}, + "humidifier": {"HumidifierEntity", "HumidifierEntityDescription"}, + "image": {"ImageEntity", "ImageEntityDescription"}, + "image_processing": { + "ImageProcessingEntity", + "ImageProcessingFaceEntity", + "ImageProcessingEntityDescription", + }, + "lawn_mower": {"LawnMowerEntity", "LawnMowerEntityDescription"}, + "light": {"LightEntity", "LightEntityDescription"}, + "lock": {"LockEntity", "LockEntityDescription"}, + "media_player": {"MediaPlayerEntity", "MediaPlayerEntityDescription"}, + "notify": {"NotifyEntity", "NotifyEntityDescription"}, + "number": {"NumberEntity", "NumberEntityDescription", "RestoreNumber"}, + "remote": {"RemoteEntity", "RemoteEntityDescription"}, + "select": {"SelectEntity", "SelectEntityDescription"}, + "sensor": {"RestoreSensor", "SensorEntity", "SensorEntityDescription"}, + "siren": {"SirenEntity", "SirenEntityDescription"}, + "stt": {"SpeechToTextEntity"}, + "switch": {"SwitchEntity", "SwitchEntityDescription"}, + "text": {"TextEntity", "TextEntityDescription"}, + "time": {"TimeEntity", "TimeEntityDescription"}, + "todo": {"TodoListEntity"}, + "tts": {"TextToSpeechEntity"}, + "update": {"UpdateEntity", "UpdateEntityDescription"}, + "vacuum": {"StateVacuumEntity", "VacuumEntity", "VacuumEntityDescription"}, + "wake_word": {"WakeWordDetectionEntity"}, + "water_heater": {"WaterHeaterEntity"}, + "weather": { + "CoordinatorWeatherEntity", + "SingleCoordinatorWeatherEntity", + "WeatherEntity", + "WeatherEntityDescription", + }, +} +_ENTITY_COMPONENTS: set[str] = {platform.value for platform in Platform}.union( + { + "alert", + "automation", + "counter", + "dominos", + "input_boolean", + "input_button", + "input_datetime", + "input_number", + "input_select", + "input_text", + "microsoft_face", + "person", + "plant", + "remember_the_milk", + "schedule", + "script", + "tag", + "timer", + } +) + + +_MODULE_CLASSES = { + class_name for classes in _MODULES.values() for class_name in classes +} + + +class HassEnforceClassModule(BaseChecker): + """Checker for class in correct module.""" + + name = "hass_enforce_class_module" + priority = -1 + msgs = { + "C7461": ( + "Derived %s is recommended to be placed in the '%s' module", + "hass-enforce-class-module", + "Used when derived class should be placed in its own module.", + ), + } + + def visit_classdef(self, node: nodes.ClassDef) -> None: + """Check if derived class is placed in its own module.""" + root_name = node.root().name + + # we only want to check components + if not root_name.startswith("homeassistant.components."): + return + parts = root_name.split(".") + current_integration = parts[2] + current_module = parts[3] if len(parts) > 3 else "" + + ancestors = list(node.ancestors()) + + if current_module != "entity" and current_integration not in _ENTITY_COMPONENTS: + top_level_ancestors = list(node.ancestors(recurs=False)) + + for ancestor in top_level_ancestors: + if ancestor.name in _BASE_ENTITY_MODULES and not any( + anc.name in _MODULE_CLASSES for anc in ancestors + ): + self.add_message( + "hass-enforce-class-module", + node=node, + args=(ancestor.name, "entity"), + ) + return + + for expected_module, classes in _MODULES.items(): + if expected_module in (current_module, current_integration): + continue + + for ancestor in ancestors: + if ancestor.name in classes: + self.add_message( + "hass-enforce-class-module", + node=node, + args=(ancestor.name, expected_module), + ) + return + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassEnforceClassModule(linter)) diff --git a/container_content/pylint/plugins/hass_enforce_coordinator_module.py b/container_content/pylint/plugins/hass_enforce_coordinator_module.py deleted file mode 100644 index 924b69f..0000000 --- a/container_content/pylint/plugins/hass_enforce_coordinator_module.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Plugin for checking if coordinator is in its own module.""" - -from __future__ import annotations - -from astroid import nodes -from pylint.checkers import BaseChecker -from pylint.lint import PyLinter - - -class HassEnforceCoordinatorModule(BaseChecker): - """Checker for coordinators own module.""" - - name = "hass_enforce_coordinator_module" - priority = -1 - msgs = { - "C7461": ( - "Derived data update coordinator is recommended to be placed in the 'coordinator' module", - "hass-enforce-coordinator-module", - "Used when derived data update coordinator should be placed in its own module.", - ), - } - options = ( - ( - "ignore-wrong-coordinator-module", - { - "default": False, - "type": "yn", - "metavar": "", - "help": "Set to ``no`` if you wish to check if derived data update coordinator " - "is placed in its own module.", - }, - ), - ) - - def visit_classdef(self, node: nodes.ClassDef) -> None: - """Check if derived data update coordinator is placed in its own module.""" - if self.linter.config.ignore_wrong_coordinator_module: - return - - root_name = node.root().name - - # we only want to check component update coordinators - if not root_name.startswith("homeassistant.components"): - return - - is_coordinator_module = root_name.endswith(".coordinator") - for ancestor in node.ancestors(): - if ancestor.name == "DataUpdateCoordinator" and not is_coordinator_module: - self.add_message("hass-enforce-coordinator-module", node=node) - return - - -def register(linter: PyLinter) -> None: - """Register the checker.""" - linter.register_checker(HassEnforceCoordinatorModule(linter)) diff --git a/container_content/pylint/plugins/hass_enforce_type_hints.py b/container_content/pylint/plugins/hass_enforce_type_hints.py index 2f107fb..a837650 100644 --- a/container_content/pylint/plugins/hass_enforce_type_hints.py +++ b/container_content/pylint/plugins/hass_enforce_type_hints.py @@ -28,6 +28,8 @@ } _KNOWN_GENERIC_TYPES_TUPLE = tuple(_KNOWN_GENERIC_TYPES) +_FORCE_ANNOTATION_PLATFORMS = ["config_flow"] + class _Special(Enum): """Sentinel values.""" @@ -69,7 +71,7 @@ class ClassTypeHintMatch: matches: list[TypeHintMatch] -_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\])|(?:\[\]))" _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" # or "dict | list | None" @@ -79,7 +81,7 @@ class ClassTypeHintMatch: _TYPE_HINT_MATCHERS.update( { f"x_of_y_{i}": re.compile( - rf"^(\w+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" + rf"^([\w\.]+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" ) for i in _INNER_MATCH_POSSIBILITIES } @@ -98,19 +100,24 @@ class ClassTypeHintMatch: _TEST_FIXTURES: dict[str, list[str] | str] = { "aioclient_mock": "AiohttpClientMocker", "aiohttp_client": "ClientSessionGenerator", + "aiohttp_server": "Callable[[], TestServer]", "area_registry": "AreaRegistry", - "async_setup_recorder_instance": "RecorderInstanceGenerator", + "async_test_recorder": "RecorderInstanceGenerator", "caplog": "pytest.LogCaptureFixture", + "capsys": "pytest.CaptureFixture[str]", "current_request_with_host": "None", "device_registry": "DeviceRegistry", "enable_bluetooth": "None", "enable_custom_integrations": "None", + "enable_missing_statistics": "bool", "enable_nightly_purge": "bool", "enable_statistics": "bool", "enable_schema_validation": "bool", "entity_registry": "EntityRegistry", "entity_registry_enabled_by_default": "None", + "event_loop": "AbstractEventLoop", "freezer": "FrozenDateTimeFactory", + "hass": "HomeAssistant", "hass_access_token": "str", "hass_admin_credential": "Credentials", "hass_admin_user": "MockUser", @@ -122,38 +129,44 @@ class ClassTypeHintMatch: "hass_owner_user": "MockUser", "hass_read_only_access_token": "str", "hass_read_only_user": "MockUser", - "hass_recorder": "Callable[..., HomeAssistant]", "hass_storage": "dict[str, Any]", "hass_supervisor_access_token": "str", "hass_supervisor_user": "MockUser", "hass_ws_client": "WebSocketGenerator", + "init_tts_cache_dir_side_effect": "Any", "issue_registry": "IssueRegistry", - "legacy_auth": "LegacyApiPasswordAuthProvider", "local_auth": "HassAuthProvider", - "mock_async_zeroconf": "None", + "mock_async_zeroconf": "MagicMock", "mock_bleak_scanner_start": "MagicMock", "mock_bluetooth": "None", "mock_bluetooth_adapters": "None", + "mock_conversation_agent": "MockAgent", "mock_device_tracker_conf": "list[Device]", - "mock_get_source_ip": "None", + "mock_get_source_ip": "_patch", "mock_hass_config": "None", "mock_hass_config_yaml": "None", - "mock_zeroconf": "None", + "mock_tts_cache_dir": "Path", + "mock_tts_get_cache_files": "MagicMock", + "mock_tts_init_cache_dir": "MagicMock", + "mock_zeroconf": "MagicMock", + "monkeypatch": "pytest.MonkeyPatch", "mqtt_client_mock": "MqttMockPahoClient", "mqtt_mock": "MqttMockHAClient", "mqtt_mock_entry": "MqttMockHAClientGenerator", "recorder_db_url": "str", "recorder_mock": "Recorder", - "requests_mock": "requests_mock.Mocker", + "request": "pytest.FixtureRequest", + "requests_mock": "Mocker", + "service_calls": "list[ServiceCall]", "snapshot": "SnapshotAssertion", + "socket_enabled": "None", "stub_blueprint_populate": "None", "tmp_path": "Path", "tmpdir": "py.path.local", + "tts_mutagen_mock": "MagicMock", + "unused_tcp_port_factory": "Callable[[], int]", + "unused_udp_port_factory": "Callable[[], int]", } -_TEST_FUNCTION_MATCH = TypeHintMatch( - function_name="test_*", - return_type=None, -) _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { @@ -1305,7 +1318,7 @@ class ClassTypeHintMatch: ), TypeHintMatch( function_name="source_type", - return_type=["SourceType", "str"], + return_type="SourceType", ), ], ), @@ -1750,39 +1763,6 @@ class ClassTypeHintMatch: ], ), ], - "mailbox": [ - ClassTypeHintMatch( - base_class="Mailbox", - matches=[ - TypeHintMatch( - function_name="media_type", - return_type="str", - ), - TypeHintMatch( - function_name="can_delete", - return_type="bool", - ), - TypeHintMatch( - function_name="has_media", - return_type="bool", - ), - TypeHintMatch( - function_name="async_get_media", - arg_types={1: "str"}, - return_type="bytes", - ), - TypeHintMatch( - function_name="async_get_messages", - return_type="list[dict[str, Any]]", - ), - TypeHintMatch( - function_name="async_delete", - arg_types={1: "str"}, - return_type="bool", - ), - ], - ), - ], "media_player": [ ClassTypeHintMatch( base_class="Entity", @@ -2909,6 +2889,10 @@ def _is_valid_type( if expected_type == "...": return isinstance(node, nodes.Const) and node.value == Ellipsis + # Special case for an empty list, such as Callable[[], TestServer] + if expected_type == "[]": + return isinstance(node, nodes.List) and not node.elts + # Special case for `xxx | yyy` if match := _TYPE_HINT_MATCHERS["a_or_b"].match(expected_type): return ( @@ -3087,11 +3071,6 @@ def _get_module_platform(module_name: str) -> str | None: return platform.lstrip(".") if platform else "__init__" -def _is_test_function(module_name: str, node: nodes.FunctionDef) -> bool: - """Return True if function is a pytest function.""" - return module_name.startswith("tests.") and node.name.startswith("test_") - - class HassTypeHintChecker(BaseChecker): """Checker for setup type hints.""" @@ -3108,6 +3087,12 @@ class HassTypeHintChecker(BaseChecker): "hass-return-type", "Used when method return type is incorrect", ), + "W7433": ( + "Argument %s is of type %s and could be moved to " + "`@pytest.mark.usefixtures` decorator in %s", + "hass-consider-usefixtures-decorator", + "Used when an argument type is None and could be a fixture", + ), } options = ( ( @@ -3124,27 +3109,31 @@ class HassTypeHintChecker(BaseChecker): _class_matchers: list[ClassTypeHintMatch] _function_matchers: list[TypeHintMatch] - _module_name: str + _module_node: nodes.Module + _module_platform: str | None + _in_test_module: bool def visit_module(self, node: nodes.Module) -> None: """Populate matchers for a Module node.""" self._class_matchers = [] self._function_matchers = [] - self._module_name = node.name + self._module_node = node + self._module_platform = _get_module_platform(node.name) + self._in_test_module = node.name.startswith("tests.") - if (module_platform := _get_module_platform(node.name)) is None: + if self._in_test_module or self._module_platform is None: return - if module_platform in _PLATFORMS: + if self._module_platform in _PLATFORMS: self._function_matchers.extend(_FUNCTION_MATCH["__any_platform__"]) - if function_matches := _FUNCTION_MATCH.get(module_platform): + if function_matches := _FUNCTION_MATCH.get(self._module_platform): self._function_matchers.extend(function_matches) - if class_matches := _CLASS_MATCH.get(module_platform): + if class_matches := _CLASS_MATCH.get(self._module_platform): self._class_matchers.extend(class_matches) - if property_matches := _INHERITANCE_MATCH.get(module_platform): + if property_matches := _INHERITANCE_MATCH.get(self._module_platform): self._class_matchers.extend(property_matches) self._class_matchers.reverse() @@ -3154,7 +3143,12 @@ def _ignore_function( ) -> bool: """Check if we can skip the function validation.""" return ( - self.linter.config.ignore_missing_annotations + # test modules are excluded from ignore_missing_annotations + not self._in_test_module + # some modules have checks forced + and self._module_platform not in _FORCE_ANNOTATION_PLATFORMS + # other modules are only checked ignore_missing_annotations + and self.linter.config.ignore_missing_annotations and node.returns is None and not _has_valid_annotations(annotations) ) @@ -3207,6 +3201,24 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: if self._ignore_function(node, annotations): return + # Check method or function matchers. + if node.is_method(): + matchers = _METHOD_MATCH + else: + if self._in_test_module and node.parent is self._module_node: + if node.name.startswith("test_"): + self._check_test_function(node, False) + return + if (decoratornames := node.decoratornames()) and ( + # `@pytest.fixture` + "_pytest.fixtures.fixture" in decoratornames + # `@pytest.fixture(...)` + or "_pytest.fixtures.FixtureFunctionMarker" in decoratornames + ): + self._check_test_function(node, True) + return + matchers = self._function_matchers + # Check that common arguments are correctly typed. for arg_name, expected_type in _COMMON_ARGUMENTS.items(): arg_node, annotation = _get_named_annotation(node, arg_name) @@ -3217,13 +3229,6 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: args=(arg_name, expected_type, node.name), ) - # Check method or function matchers. - if node.is_method(): - matchers = _METHOD_MATCH - else: - matchers = self._function_matchers - if _is_test_function(self._module_name, node): - self._check_test_function(node, annotations) for match in matchers: if not match.need_to_check_function(node): continue @@ -3240,11 +3245,7 @@ def _check_function( # Check that all positional arguments are correctly annotated. if match.arg_types: for key, expected_type in match.arg_types.items(): - if ( - node.args.args[key].name in _COMMON_ARGUMENTS - or _is_test_function(self._module_name, node) - and node.args.args[key].name in _TEST_FIXTURES - ): + if node.args.args[key].name in _COMMON_ARGUMENTS: # It has already been checked, avoid double-message continue if not _is_valid_type(expected_type, annotations[key]): @@ -3257,11 +3258,7 @@ def _check_function( # Check that all keyword arguments are correctly annotated. if match.named_arg_types is not None: for arg_name, expected_type in match.named_arg_types.items(): - if ( - arg_name in _COMMON_ARGUMENTS - or _is_test_function(self._module_name, node) - and arg_name in _TEST_FIXTURES - ): + if arg_name in _COMMON_ARGUMENTS: # It has already been checked, avoid double-message continue arg_node, annotation = _get_named_annotation(node, arg_name) @@ -3290,19 +3287,23 @@ def _check_function( args=(match.return_type or "None", node.name), ) - def _check_test_function( - self, node: nodes.FunctionDef, annotations: list[nodes.NodeNG | None] - ) -> None: - # Check the return type. - if not _is_valid_return_type(_TEST_FUNCTION_MATCH, node.returns): + def _check_test_function(self, node: nodes.FunctionDef, is_fixture: bool) -> None: + # Check the return type, should always be `None` for test_*** functions. + if not is_fixture and not _is_valid_type(None, node.returns, True): self.add_message( "hass-return-type", node=node, - args=(_TEST_FUNCTION_MATCH.return_type or "None", node.name), + args=("None", node.name), ) # Check that all positional arguments are correctly annotated. for arg_name, expected_type in _TEST_FIXTURES.items(): arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and expected_type == "None" and not is_fixture: + self.add_message( + "hass-consider-usefixtures-decorator", + node=arg_node, + args=(arg_name, expected_type, node.name), + ) if arg_node and not _is_valid_type(expected_type, annotation): self.add_message( "hass-argument-type", diff --git a/container_content/pylint/plugins/hass_imports.py b/container_content/pylint/plugins/hass_imports.py index d8f85df..eacabc5 100644 --- a/container_content/pylint/plugins/hass_imports.py +++ b/container_content/pylint/plugins/hass_imports.py @@ -360,6 +360,12 @@ class ObsoleteImportMatch: constant=re.compile(r"^RESULT_TYPE_(\w*)$"), ), ], + "homeassistant.helpers.config_validation": [ + ObsoleteImportMatch( + reason="should be imported from homeassistant/components/", + constant=re.compile(r"^PLATFORM_SCHEMA(_BASE)?$"), + ), + ], "homeassistant.helpers.device_registry": [ ObsoleteImportMatch( reason="replaced by DeviceEntryDisabler enum", @@ -386,12 +392,63 @@ class ObsoleteImportMatch: constant=re.compile(r"^IMPERIAL_SYSTEM$"), ), ], - "homeassistant.util.json": [ - ObsoleteImportMatch( - reason="moved to homeassistant.helpers.json", - constant=re.compile(r"^save_json|find_paths_unserializable_data$"), - ), - ], +} + +_IGNORE_ROOT_IMPORT = ( + "assist_pipeline", + "automation", + "bluetooth", + "camera", + "cast", + "device_automation", + "device_tracker", + "ffmpeg", + "ffmpeg_motion", + "google_assistant", + "hardware", + "homeassistant", + "homeassistant_hardware", + "http", + "manual", + "plex", + "recorder", + "rest", + "script", + "sensor", + "stream", + "zha", +) + + +# Blacklist of imports that should be using the namespace +@dataclass +class NamespaceAlias: + """Class for namespace imports.""" + + alias: str + names: set[str] # function names + + +_FORCE_NAMESPACE_IMPORT: dict[str, NamespaceAlias] = { + "homeassistant.helpers.area_registry": NamespaceAlias("ar", {"async_get"}), + "homeassistant.helpers.category_registry": NamespaceAlias("cr", {"async_get"}), + "homeassistant.helpers.device_registry": NamespaceAlias( + "dr", + { + "async_get", + "async_entries_for_config_entry", + }, + ), + "homeassistant.helpers.entity_registry": NamespaceAlias( + "er", + { + "async_get", + "async_entries_for_config_entry", + }, + ), + "homeassistant.helpers.floor_registry": NamespaceAlias("fr", {"async_get"}), + "homeassistant.helpers.issue_registry": NamespaceAlias("ir", {"async_get"}), + "homeassistant.helpers.label_registry": NamespaceAlias("lr", {"async_get"}), } @@ -422,6 +479,17 @@ class HassImportsFormatChecker(BaseChecker): "Used when an import from another component should be " "from the component root", ), + "W7425": ( + "`%s` should not be imported directly. Please import `%s` as `%s` " + "and use `%s.%s`", + "hass-helper-namespace-import", + "Used when a helper should be used via the namespace", + ), + "W7426": ( + "`%s` should be imported using an alias, such as `%s as %s`", + "hass-import-constant-alias", + "Used when a constant should be imported as an alias", + ), } options = () @@ -446,8 +514,9 @@ def visit_import(self, node: nodes.Import) -> None: if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) continue - if module.startswith("homeassistant.components.") and module.endswith( - "const" + if ( + module.startswith("homeassistant.components.") + and len(module.split(".")) > 3 ): if ( self.current_package.startswith("tests.components.") @@ -479,6 +548,85 @@ def _visit_importfrom_relative( if len(split_package) < node.level + 2: self.add_message("hass-absolute-import", node=node) + def _check_for_constant_alias( + self, + node: nodes.ImportFrom, + current_component: str | None, + imported_component: str, + ) -> bool: + """Check for hass-import-constant-alias.""" + if current_component == imported_component: + return True + + # Check for `from homeassistant.components.other import DOMAIN` + for name, alias in node.names: + if name == "DOMAIN" and (alias is None or alias == "DOMAIN"): + self.add_message( + "hass-import-constant-alias", + node=node, + args=( + "DOMAIN", + "DOMAIN", + f"{imported_component.upper()}_DOMAIN", + ), + ) + return False + + return True + + def _check_for_component_root_import( + self, + node: nodes.ImportFrom, + current_component: str | None, + imported_parts: list[str], + imported_component: str, + ) -> bool: + """Check for hass-component-root-import.""" + if ( + current_component == imported_component + or imported_component in _IGNORE_ROOT_IMPORT + ): + return True + + # Check for `from homeassistant.components.other.module import something` + if len(imported_parts) > 3: + self.add_message("hass-component-root-import", node=node) + return False + + # Check for `from homeassistant.components.other import const` + for name, _ in node.names: + if name == "const": + self.add_message("hass-component-root-import", node=node) + return False + + return True + + def _check_for_relative_import( + self, + current_package: str, + node: nodes.ImportFrom, + current_component: str | None, + ) -> bool: + """Check for hass-relative-import.""" + if node.modname == current_package or node.modname.startswith( + f"{current_package}." + ): + self.add_message("hass-relative-import", node=node) + return False + + for root in ("homeassistant", "tests"): + if current_package.startswith(f"{root}.components."): + if node.modname == f"{root}.components": + for name in node.names: + if name[0] == current_component: + self.add_message("hass-relative-import", node=node) + return False + elif node.modname.startswith(f"{root}.components.{current_component}."): + self.add_message("hass-relative-import", node=node) + return False + + return True + def visit_importfrom(self, node: nodes.ImportFrom) -> None: """Check for improper 'from _ import _' invocations.""" if not self.current_package: @@ -486,35 +634,36 @@ def visit_importfrom(self, node: nodes.ImportFrom) -> None: if node.level is not None: self._visit_importfrom_relative(self.current_package, node) return - if node.modname == self.current_package or node.modname.startswith( - f"{self.current_package}." - ): - self.add_message("hass-relative-import", node=node) - return + + # Cache current component + current_component: str | None = None for root in ("homeassistant", "tests"): if self.current_package.startswith(f"{root}.components."): current_component = self.current_package.split(".")[2] - if node.modname == f"{root}.components": - for name in node.names: - if name[0] == current_component: - self.add_message("hass-relative-import", node=node) - return - if node.modname.startswith(f"{root}.components.{current_component}."): - self.add_message("hass-relative-import", node=node) - return - if node.modname.startswith("homeassistant.components.") and ( - node.modname.endswith(".const") - or "const" in {names[0] for names in node.names} + + # Checks for hass-relative-import + if not self._check_for_relative_import( + self.current_package, node, current_component ): - if ( - self.current_package.startswith("tests.components.") - and self.current_package.split(".")[2] == node.modname.split(".")[2] + return + + if node.modname.startswith("homeassistant.components."): + imported_parts = node.modname.split(".") + imported_component = imported_parts[2] + + # Checks for hass-component-root-import + if not self._check_for_component_root_import( + node, current_component, imported_parts, imported_component ): - # Ignore check if the component being tested matches - # the component being imported from return - self.add_message("hass-component-root-import", node=node) - return + + # Checks for hass-import-constant-alias + if not self._check_for_constant_alias( + node, current_component, imported_component + ): + return + + # Checks for hass-deprecated-import if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): for name_tuple in node.names: for obsolete_import in obsolete_imports: @@ -525,6 +674,22 @@ def visit_importfrom(self, node: nodes.ImportFrom) -> None: args=(import_match.string, obsolete_import.reason), ) + # Checks for hass-helper-namespace-import + if namespace_alias := _FORCE_NAMESPACE_IMPORT.get(node.modname): + for name in node.names: + if name[0] in namespace_alias.names: + self.add_message( + "hass-helper-namespace-import", + node=node, + args=( + name[0], + node.modname, + namespace_alias.alias, + namespace_alias.alias, + name[0], + ), + ) + def register(linter: PyLinter) -> None: """Register the checker.""" diff --git a/container_content/pyproject.toml b/container_content/pyproject.toml index b2ddfa8..9fc0a0b 100644 --- a/container_content/pyproject.toml +++ b/container_content/pyproject.toml @@ -2,7 +2,10 @@ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.5", + # Integrations may depend on hassio integration without listing it to + # change behavior based on presence of supervisor + "aiohasupervisor==0.1.0", + "aiohttp==3.10.8", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", @@ -11,7 +14,7 @@ dependencies = [ "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==24.6.0", - "bcrypt==4.1.3", + "bcrypt==4.2.0", "certifi>=2021.5.30", "ciso8601==2.3.1", "fnv-hash-fast==1.0.2", @@ -20,36 +23,56 @@ dependencies = [ "hass-nabucasa==0.81.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.27.0", + "httpx==0.27.2", "home-assistant-bluetooth==1.12.2", "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", + "mashumaro==3.13.1", "PyJWT==2.9.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==43.0.0", + "cryptography==43.0.1", "Pillow==10.4.0", "pyOpenSSL==24.2.1", "orjson==3.10.7", "packaging>=23.1", - "pip>=21.3.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", "PyYAML==6.0.2", "requests==2.32.3", "SQLAlchemy==2.0.31", "typing-extensions>=4.12.2,<5.0", - "ulid-transform==0.13.1", + "ulid-transform==1.0.2", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 # https://github.com/home-assistant/core/issues/97248 "urllib3>=1.26.5,<2", + "uv==0.4.17", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.5", - "yarl==1.9.4", + "yarl==1.13.1", ] +[project.urls] +"Homepage" = "https://www.home-assistant.io/" +"Source Code" = "https://github.com/home-assistant/core" +"Bug Reports" = "https://github.com/home-assistant/core/issues" +"Docs: Dev" = "https://developers.home-assistant.io/" +"Discord" = "https://www.home-assistant.io/join-chat/" +"Forum" = "https://community.home-assistant.io/" + +[project.scripts] +hass = "homeassistant.__main__:main" + +[tool.setuptools] +platforms = ["any"] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages.find] +include = ["homeassistant*"] + [tool.pylint.MAIN] py-version = "3.12" # Use a conservative default here; 2 should speed up most setups and not hurt @@ -68,7 +91,8 @@ init-hook = """\ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", - "hass_enforce_coordinator_module", + "hass_decorator", + "hass_enforce_class_module", "hass_enforce_sorted_platforms", "hass_enforce_super_call", "hass_enforce_type_hints", @@ -107,7 +131,6 @@ class-const-naming-style = "any" # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this -# consider-using-f-string - str.format sometimes more readable # possibly-used-before-assignment - too many errors / not necessarily issues # --- # Pylint CodeStyle plugin @@ -129,8 +152,8 @@ disable = [ "too-many-locals", "too-many-public-methods", "too-many-boolean-expressions", + "too-many-positional-arguments", "wrong-import-order", - "consider-using-f-string", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", "possibly-used-before-assignment", @@ -272,6 +295,7 @@ disable = [ "broad-except", # BLE001 "protected-access", # SLF001 "broad-exception-raised", # TRY002 + "consider-using-f-string", # PLC0209 # "no-self-use", # PLR6301 # Optional plugin, not enabled # Handled by mypy @@ -402,6 +426,7 @@ norecursedirs = [ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "error::sqlalchemy.exc.SAWarning", @@ -436,10 +461,7 @@ filterwarnings = [ "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", # -- tracked upstream / open PRs - # https://github.com/ronf/asyncssh/issues/674 - v2.15.0 - "ignore:ARC4 has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.ARC4 and will be removed from this module in 48.0.0:UserWarning:asyncssh.crypto.cipher", - "ignore:TripleDES has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and will be removed from this module in 48.0.0:UserWarning:asyncssh.crypto.cipher", - # https://github.com/certbot/certbot/issues/9828 - v2.10.0 + # https://github.com/certbot/certbot/issues/9828 - v2.11.0 "ignore:X509Extension support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util", # https://github.com/beetbox/mediafile/issues/67 - v0.12.0 "ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning:mediafile", @@ -450,6 +472,8 @@ filterwarnings = [ # -- fixed, waiting for release / update # https://github.com/bachya/aiopurpleair/pull/200 - >=2023.10.0 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiopurpleair.helpers.validators", + # https://bugs.launchpad.net/beautifulsoup/+bug/2076897 - >4.12.3 + "ignore:The 'strip_cdata' option of HTMLParser\\(\\) has never done anything and will eventually be removed:DeprecationWarning:bs4.builder._lxml", # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 "ignore:invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", # https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0 @@ -458,7 +482,7 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:devialet.devialet_api", # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", - # https://github.com/influxdata/influxdb-client-python/issues/603 >1.45.0 + # https://github.com/influxdata/influxdb-client-python/issues/603 >=1.45.0 # https://github.com/influxdata/influxdb-client-python/pull/652 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb_client.client.write.point", # https://github.com/majuss/lupupy/pull/15 - >0.3.2 @@ -475,8 +499,6 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", # https://github.com/hunterjm/python-onvif-zeep-async/pull/51 - >3.1.12 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", - # https://github.com/googleapis/python-pubsub/commit/060f00bcea5cd129be3a2d37078535cc97b4f5e8 - >=2.13.12 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:google.pubsub_v1.services.publisher.client", # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 @@ -489,8 +511,6 @@ filterwarnings = [ # -- other # Locale changes might take some time to resolve upstream "ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud", - # https://github.com/protocolbuffers/protobuf - v4.25.1 - "ignore:Type google._upb._message.(Message|Scalar)MapContainer uses PyType_Spec with a metaclass that has custom tp_new. .* Python 3.14:DeprecationWarning", # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway", # https://github.com/lidatong/dataclasses-json/issues/328 @@ -541,8 +561,8 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pybotvac.version", # https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pymystrom", - # https://pypi.org/project/velbus-aio/ - v2024.7.5 - 2024-07-05 - # https://github.com/Cereal2nd/velbus-aio/blob/2024.7.5/velbusaio/handler.py#L22 + # https://pypi.org/project/velbus-aio/ - v2024.7.6 - 2024-07-31 + # https://github.com/Cereal2nd/velbus-aio/blob/2024.7.6/velbusaio/handler.py#L22 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler", # - pyOpenSSL v24.2.1 # https://pypi.org/project/acme/ - v2.11.0 - 2024-06-06 @@ -561,8 +581,8 @@ filterwarnings = [ # https://pypi.org/project/SpeechRecognition/ - v3.10.4 - 2024-05-05 # https://github.com/Uberi/speech_recognition/blob/3.10.4/speech_recognition/__init__.py#L7 "ignore:'aifc' is deprecated and slated for removal in Python 3.13:DeprecationWarning:speech_recognition", - # https://pypi.org/project/voip-utils/ - v0.1.0 - 2023-06-28 - # https://github.com/home-assistant-libs/voip-utils/blob/v0.1.0/voip_utils/rtp_audio.py#L2 + # https://pypi.org/project/voip-utils/ - v0.2.0 - 2024-09-06 + # https://github.com/home-assistant-libs/voip-utils/blob/v0.2.0/voip_utils/rtp_audio.py#L3 "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:voip_utils.rtp_audio", # -- Python 3.13 - unmaintained projects, last release about 2+ years @@ -646,7 +666,7 @@ exclude_lines = [ ] [tool.ruff] -required-version = ">=0.5.3" +required-version = ">=0.6.8" [tool.ruff.lint] select = [ @@ -677,6 +697,7 @@ select = [ "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake + "F541", # f-string without any placeholders "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format @@ -693,6 +714,7 @@ select = [ "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style + "PTH", # flake8-pathlib "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise @@ -727,9 +749,12 @@ select = [ "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print - "TID251", # Banned imports + "TCH", # flake8-type-checking + "TID", # Tidy imports "TRY", # tryceratops "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call "W", # pycodestyle ] @@ -759,6 +784,12 @@ ignore = [ "SIM103", # Return the condition {condition} directly "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TCH001", # Move application import {} into a type-checking block + "TCH002", # Move third-party import {} into a type-checking block + "TCH003", # Move standard library import {} into a type-checking block + "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 @@ -803,7 +834,6 @@ voluptuous = "vol" "homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" "homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" "homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" -"homeassistant.components.mailbox.PLATFORM_SCHEMA" = "MAILBOX_PLATFORM_SCHEMA" "homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" "homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" "homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" @@ -858,5 +888,14 @@ split-on-trailing-comma = false "homeassistant/scripts/*" = ["T201"] "script/*" = ["T20"] +# Allow relative imports within auth and within components +"homeassistant/auth/*/*" = ["TID252"] +"homeassistant/components/*/*/*" = ["TID252"] +"tests/components/*/*/*" = ["TID252"] + +# Temporary +"homeassistant/**" = ["PTH"] +"tests/**" = ["PTH"] + [tool.ruff.lint.mccabe] max-complexity = 25 diff --git a/requirements.txt b/requirements.txt index 1228334..0d5c25a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,28 @@ # Pre-commit requirements (copied from requirements_test_pre_commit.txt) codespell==2.3.0 -ruff==0.6.4 +ruff==0.6.8 yamllint==1.35.1 # from requirements_test.txt -astroid==3.2.4 -coverage==7.6.0 +# linters such as pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version + +# types-* that have versions roughly corresponding to the packages they +# contain hints for available should be kept in sync with them + +astroid==3.3.4 +coverage==7.6.1 freezegun==1.5.1 mock-open==1.4.0 -mypy-dev==1.12.0a3 -pre-commit==3.7.1 -pydantic==1.10.17 -pylint==3.2.6 +mypy-dev==1.12.0a5 +pre-commit==3.8.0 +pydantic==1.10.18 +pylint==3.3.1 pylint-per-file-ignores==1.3.2 -pipdeptree==2.23.1 -pip-licenses==4.5.1 -pytest-asyncio==0.23.8 +pipdeptree==2.23.4 +pip-licenses==5.0.0 +pytest-asyncio==0.24.0 pytest-aiohttp==1.0.5 pytest-cov==5.0.0 pytest-freezer==0.4.8 @@ -26,34 +33,34 @@ pytest-timeout==2.3.1 pytest-unordered==0.6.1 pytest-picked==0.5.0 pytest-xdist==3.6.1 -pytest==8.3.1 +pytest==8.3.3 requests-mock==1.12.1 respx==0.21.1 -syrupy==4.6.1 -tqdm==4.66.4 -types-aiofiles==23.2.0.20240623 +syrupy==4.7.1 +tqdm==4.66.5 +types-aiofiles==24.1.0.20240626 types-atomicwrites==1.4.5.1 types-croniter==2.0.0.20240423 -types-beautifulsoup4==4.12.0.20240511 -types-caldav==1.3.0.20240331 +types-beautifulsoup4==4.12.0.20240907 +types-caldav==1.3.0.20240824 types-chardet==0.1.5 types-decorator==5.1.8.20240310 types-paho-mqtt==1.6.0.20240321 -types-pillow==10.2.0.20240520 -types-protobuf==4.24.0.20240106 -types-psutil==6.0.0.20240621 -types-python-dateutil==2.9.0.20240316 +types-pillow==10.2.0.20240822 +types-protobuf==4.25.0.20240417 +types-psutil==6.0.0.20240901 +types-python-dateutil==2.9.0.20240906 types-python-slugify==8.0.2.20240310 -types-pytz==2024.1.0.20240417 -types-PyYAML==6.0.12.20240311 +types-pytz==2024.2.0.20240913 +types-PyYAML==6.0.12.20240917 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 -uv==0.2.27 # from requirements.txt aiodns==3.2.0 -aiohttp==3.10.5 +aiohasupervisor==0.1.0 +aiohttp==3.10.8 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 @@ -62,23 +69,23 @@ async-interrupt==1.2.0 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==24.6.0 -bcrypt==4.1.3 +bcrypt==4.2.0 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==1.0.2 hass-nabucasa==0.81.1 -httpx==0.27.0 +httpx==0.27.2 home-assistant-bluetooth==1.12.2 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 +mashumaro==3.13.1 PyJWT==2.9.0 -cryptography==43.0.0 +cryptography==43.0.1 Pillow==10.4.0 pyOpenSSL==24.2.1 orjson==3.10.7 packaging>=23.1 -pip>=21.3.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.2 @@ -87,18 +94,19 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==1.0.2 urllib3>=1.26.5,<2 +uv==0.4.17 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 -yarl==1.9.11 - +yarl==1.13.1 # Packages from package_constraints.txt which are otherwise installed on run aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 +aiohasupervisor==0.1.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.5 +aiohttp==3.10.8 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 @@ -107,16 +115,16 @@ async-upnp-client==0.40.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.6.0 -bcrypt==4.1.3 +bcrypt==4.2.0 bleak-retry-connector==3.5.0 bleak==0.22.2 bluetooth-adapters==0.19.4 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.20.0 -cached-ipaddress==0.5.0 +cached-ipaddress==0.6.0 certifi>=2021.5.30 ciso8601==2.3.1 -cryptography==43.0.0 +cryptography==43.0.1 dbus-fast==2.24.0 fnv-hash-fast==1.0.2 ha-av==10.1.1 @@ -125,18 +133,18 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240906.0 -home-assistant-intents==2024.9.4 -httpx==0.27.0 +home-assistant-frontend==20241002.2 +home-assistant-intents==2024.10.2 +httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 +mashumaro==3.13.1 mutagen==1.47.0 orjson==3.10.7 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.4.0 -pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.9.0 pymicro-vad==1.0.1 @@ -153,13 +161,14 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==1.0.2 urllib3>=1.26.5,<2 +uv==0.4.17 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 -yarl==1.9.11 -zeroconf==0.133.0 +yarl==1.13.1 +zeroconf==0.135.0 # HomeAssistant -pytest-homeassistant-custom-component==0.13.161 -homeassistant==2024.9.0 +pytest-homeassistant-custom-component==0.13.171 +homeassistant==2024.10.0