Skip to content

Commit e5135b0

Browse files
committed
fix(components): correctly pass vis to component unload method
1 parent a65b25e commit e5135b0

File tree

2 files changed

+61
-58
lines changed

2 files changed

+61
-58
lines changed

tests/components/test__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import Literal
6+
from typing import TYPE_CHECKING, Literal, cast
77
from unittest.mock import ANY, Mock, call, patch
88

99
import pytest
@@ -23,7 +23,11 @@
2323
from viseron.exceptions import ComponentNotReady
2424

2525
from tests.common import MockComponent, MockComponentModule
26-
from tests.conftest import MockViseron
26+
27+
if TYPE_CHECKING:
28+
from types import ModuleType
29+
30+
from tests.conftest import MockViseron
2731

2832

2933
class TestSetupComponents:
@@ -163,6 +167,7 @@ def test_add_domain_to_setup(
163167
self, vis: MockViseron, caplog: pytest.LogCaptureFixture
164168
) -> None:
165169
"""Test add_domain_to_setup."""
170+
caplog.set_level(logging.DEBUG)
166171
registry = vis.domain_registry
167172

168173
component1 = Component(vis, "component1", "component1", {})
@@ -180,8 +185,8 @@ def test_add_domain_to_setup(
180185
assert entry is not None
181186
assert entry.component_name == "component1"
182187
assert (
183-
"Domain object_detector with identifier identifier1 already in setup "
184-
"queue. Skipping setup of domain object_detector with identifier "
188+
"Domain object_detector with identifier identifier1 already pending setup. "
189+
"Skipping setup of domain object_detector with identifier "
185190
"identifier1 for component component2" in caplog.text
186191
)
187192
caplog.clear()
@@ -196,7 +201,7 @@ def test_get_component_success(self, vis: MockViseron) -> None:
196201
result = component.get_component()
197202

198203
mock_import.assert_called_once_with("viseron.components.test")
199-
assert result == mock_module
204+
assert result == cast("ModuleType", mock_module)
200205

201206

202207
class TestComponentConfigValidation:
@@ -218,7 +223,7 @@ def test_validate_config_valid_schema(self, vis: MockViseron) -> None:
218223
"""Test validation with valid CONFIG_SCHEMA."""
219224
validated_config: dict[str, bool] = {"validated": True}
220225

221-
def schema(_config) -> dict[str, bool]:
226+
def schema(_config: dict[str, bool]) -> dict[str, bool]:
222227
return validated_config
223228

224229
mock_module = MockComponentModule(config_schema=schema)

viseron/components/__init__.py

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
import importlib
66
import logging
77
import time
8-
import traceback
98
from concurrent.futures import ThreadPoolExecutor, as_completed
109
from timeit import default_timer as timer
11-
from typing import TYPE_CHECKING, Any
10+
from typing import TYPE_CHECKING, Any, ClassVar
1211

1312
import voluptuous as vol
1413
from voluptuous.humanize import humanize_error
@@ -30,6 +29,8 @@
3029
from viseron.watchdog.thread_watchdog import RestartableThread
3130

3231
if TYPE_CHECKING:
32+
from types import ModuleType
33+
3334
from viseron import Viseron
3435
from viseron.domains import OptionalDomain, RequireDomain
3536
from viseron.viseron_types import SupportedDomains
@@ -51,7 +52,7 @@
5152
class Component:
5253
"""Represents a Viseron component."""
5354

54-
retry_timers: dict[str, NamedTimer] = {}
55+
retry_timers: ClassVar[dict[str, NamedTimer]] = {}
5556

5657
def __init__(
5758
self,
@@ -79,7 +80,7 @@ def path(self) -> str:
7980
"""Return component path."""
8081
return self._path
8182

82-
def get_component(self):
83+
def get_component(self) -> ModuleType:
8384
"""Return component module."""
8485
return importlib.import_module(self._path)
8586

@@ -92,20 +93,21 @@ def validate_component_config(self) -> dict | bool | None:
9293
except vol.Invalid as ex:
9394
LOGGER.exception(
9495
f"Error validating config for component {self.name}: "
95-
f"{humanize_error(self._config, ex)}"
96+
f"{humanize_error(self._config, ex)}" # noqa: TRY401
9697
)
9798
return None
9899
except Exception: # pylint: disable=broad-except
99100
LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", self.name)
100101
return None
101102
return True
102103

103-
def setup_component(self, tries: int = 1, domains_only=False) -> bool:
104+
def setup_component(self, tries: int = 1, *, domains_only: bool = False) -> bool:
104105
"""Set up component."""
105106
LOGGER.info(
106-
"Setting up component %s%s",
107+
"Setting up component %s%s%s",
107108
self.name,
108109
(f", attempt {tries}" if tries > 1 else ""),
110+
(" (domains only)" if domains_only else ""),
109111
)
110112
slow_setup_warning = NamedTimer(
111113
SLOW_SETUP_WARNING,
@@ -131,15 +133,13 @@ def setup_component(self, tries: int = 1, domains_only=False) -> bool:
131133
# setup() is optional for stateless components
132134
if hasattr(component_module, "setup") and not domains_only:
133135
result = component_module.setup(self._vis, config)
136+
# No setup function, assume success if setup_domains exists
137+
elif hasattr(component_module, "setup_domains"):
138+
result = True
134139
else:
135-
# No setup function, assume success if setup_domains exists
136-
if hasattr(component_module, "setup_domains"):
137-
result = True
138-
else:
139-
LOGGER.error(
140-
f"Component {self.name} has neither setup() "
141-
"nor setup_domains()"
142-
)
140+
LOGGER.error(
141+
f"Component {self.name} has neither setup() nor setup_domains()"
142+
)
143143
except ComponentNotReady as error:
144144
if self._vis.shutdown_event.is_set():
145145
LOGGER.warning(
@@ -153,7 +153,7 @@ def setup_component(self, tries: int = 1, domains_only=False) -> bool:
153153
LOGGER.error(
154154
f"Component {self.name} is not ready. "
155155
f"Retrying in {wait_time} seconds in the background. "
156-
f"Error: {str(error)}"
156+
f"Error: {error!s}"
157157
)
158158
retry_timer = NamedTimer(
159159
wait_time,
@@ -178,11 +178,8 @@ def cancel_retry_timer() -> None:
178178
VISERON_SIGNAL_SHUTDOWN, cancel_retry_timer
179179
)
180180
retry_timer.start()
181-
except Exception as ex: # pylint: disable=broad-except
182-
LOGGER.error(
183-
f"Uncaught exception setting up component {self.name}: {ex}\n"
184-
f"{traceback.format_exc()}"
185-
)
181+
except Exception: # pylint: disable=broad-except
182+
LOGGER.exception(f"Uncaught exception setting up component {self.name}")
186183
finally:
187184
slow_setup_warning.cancel()
188185

@@ -192,10 +189,9 @@ def cancel_retry_timer() -> None:
192189
if hasattr(component_module, "setup_domains"):
193190
try:
194191
component_module.setup_domains(self._vis, config)
195-
except Exception as ex: # pylint: disable=broad-except
196-
LOGGER.error(
197-
f"Uncaught exception in setup_domains for component "
198-
f"{self.name}: {ex}\n{traceback.format_exc()}"
192+
except Exception: # pylint: disable=broad-except
193+
LOGGER.exception(
194+
f"Uncaught exception in setup_domains for component {self.name}"
199195
)
200196
return False
201197
LOGGER.info(
@@ -238,11 +234,14 @@ def add_domain_to_setup(
238234
# Check if already registered (any state)
239235
existing = registry.get(domain, identifier)
240236
if existing:
241-
LOGGER.warning(
242-
f"Domain {domain} with identifier {identifier} already in setup queue. "
243-
f"Skipping setup of domain {domain} with identifier {identifier} for "
244-
f"component {self.name}",
245-
)
237+
if existing.state == DomainState.PENDING:
238+
LOGGER.warning(
239+
f"Domain {domain} with identifier {identifier} "
240+
"already pending setup. "
241+
f"Skipping setup of domain {domain} "
242+
f"with identifier {identifier} for "
243+
f"component {self.name}",
244+
)
246245
return None
247246

248247
return registry.register(
@@ -274,7 +273,7 @@ def get_component(
274273

275274

276275
def setup_component(
277-
vis: Viseron, component: Component, tries: int = 1, domains_only=False
276+
vis: Viseron, component: Component, tries: int = 1, *, domains_only: bool = False
278277
) -> None:
279278
"""Set up single component."""
280279
# When tries is larger than one, it means we are in a retry loop.
@@ -340,9 +339,9 @@ def unload_component(vis: Viseron, component: str) -> set[str] | None:
340339
component_module = component_instance.get_component()
341340
if hasattr(component_module, "unload"):
342341
try:
343-
component_module.unload()
344-
except Exception as ex: # pylint: disable=broad-except
345-
LOGGER.error(f"Error unloading component {component}: {ex}")
342+
component_module.unload(vis)
343+
except Exception: # pylint: disable=broad-except
344+
LOGGER.exception(f"Error unloading component {component}")
346345
else:
347346
LOGGER.debug(f"Component {component} has no unload method")
348347

@@ -359,7 +358,7 @@ class CriticalComponentsConfigStore:
359358
Used to store the last known good config for critical components.
360359
"""
361360

362-
def __init__(self, vis) -> None:
361+
def __init__(self, vis: Viseron) -> None:
363362
self._vis = vis
364363
self._store = Storage(vis, STORAGE_KEY)
365364

@@ -448,27 +447,26 @@ def setup_components(
448447
return
449448

450449
# Setup components in parallel
451-
setup_threads = []
452-
for component in (
453-
components_to_setup
454-
- set(LOGGING_COMPONENTS)
455-
- set(CORE_COMPONENTS)
456-
- set(DEFAULT_COMPONENTS)
457-
):
458-
setup_threads.append(
459-
RestartableThread(
460-
target=setup_component,
461-
args=(vis, get_component(vis, component, config)),
462-
kwargs={"domains_only": domains_only},
463-
name=f"{component}_setup",
464-
daemon=True,
465-
register=False,
466-
)
450+
setup_threads = [
451+
RestartableThread(
452+
target=setup_component,
453+
args=(vis, get_component(vis, component, config)),
454+
kwargs={"domains_only": domains_only},
455+
name=f"{component}_setup",
456+
daemon=True,
457+
register=False,
458+
)
459+
for component in (
460+
components_to_setup
461+
- set(LOGGING_COMPONENTS)
462+
- set(CORE_COMPONENTS)
463+
- set(DEFAULT_COMPONENTS)
467464
)
465+
]
468466
for thread in setup_threads:
469467
thread.start()
470468

471-
def join(thread) -> None:
469+
def join(thread: RestartableThread) -> None:
472470
thread.join(timeout=30)
473471
time.sleep(0.5) # Wait for thread to exit properly
474472
if thread.is_alive():

0 commit comments

Comments
 (0)