Skip to content

Commit 51fb5cd

Browse files
NodeJSmithCopilot
andauthored
Fix/bug in app handler (#188)
* use reload instead of init to avoid losing apps * cache original dev_mode value, do not change * change to debug log level * Update tests/test_apps.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add validation aliases for autodetect_apps, remove changelog entry * add changelog entry for fix * Update src/hassette/config/helpers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove inner function * bump version and update changelog --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f0f2fef commit 51fb5cd

File tree

8 files changed

+62
-18
lines changed

8 files changed

+62
-18
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
## [0.15.5] - 2025-11-14
11+
1012
### Changed
11-
- **Breaking:** Rename all `auto_detect` values to `autodetect` (e.g. `auto_detect_apps` -> `autodetect_apps`)
1213
- Update `HassetteConfig` defaults to differ if in dev mode
1314
- Generally speaking, values are extended (e.g. timeouts) and more permissive (e.g. `allow_startup_if_app_precheck_fails = true` in dev mode)
1415
- Moved `AppManifest` and `HassetteTomlConfigSettingsSource` to `classes.py`
@@ -17,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1718
- Bumped version of `uv` in `mise.toml`, docker image, and build backend
1819
- Converted docs to mkdocs instead of sphinx
1920

21+
### Fixed
22+
- Fixed bug in AppHandler where all apps would be lost when `handle_changes` was called, due to improper reloading of configuration
23+
- Now uses `HassetteConfig.reload()` to reload config instead of re-initializing the class
24+
2025
## [0.15.4] - 2025-11-07
2126

2227
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "uv_build"
44

55
[project]
66
name = "hassette"
7-
version = "0.15.4"
7+
version = "0.15.5"
88
description = "Hassette is a simple, modern, async-first Python framework for building Home Assistant automations."
99
readme = "README.md"
1010
authors = [{ name = "Jessica", email = "12jessicasmith34@gmail.com" }]

src/hassette/config/classes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, settings_cls: type[BaseSettings], toml_file: PathType | None
3131
super().__init__(settings_cls, self.toml_file_path)
3232
return
3333

34-
LOGGER.info("Merging 'hassette' section from TOML config into top level")
34+
LOGGER.debug("Merging 'hassette' section from TOML config into top level")
3535
top_level_keys = set(self.toml_data.keys()) - {"hassette"}
3636
hassette_values = self.toml_data.pop("hassette")
3737
for key in top_level_keys.intersection(hassette_values.keys()):

src/hassette/config/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def settings_customise_sources(
109109
"""Access token for Home Assistant instance"""
110110

111111
# has to be before apps to allow auto-detection
112-
autodetect_apps: bool = Field(default=True)
112+
autodetect_apps: bool = Field(default=True, validation_alias=AliasChoices("autodetect_apps", "auto_detect_apps"))
113113
"""Whether to automatically detect apps in the app directory."""
114114

115115
extend_autodetect_exclude_dirs: tuple[str, ...] = Field(default_factory=tuple)

src/hassette/config/helpers.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import platformdirs
1111
from packaging.version import Version
1212

13+
from hassette import context
1314
from hassette.types.types import LOG_LEVELS
1415

1516
LOG_LEVEL_VALUES = get_args(LOG_LEVELS)
@@ -37,20 +38,32 @@ def get_dev_mode() -> bool:
3738
Returns:
3839
True if developer mode is enabled, False otherwise.
3940
"""
40-
logger = logging.getLogger(__name__)
41-
if "debugpy" in sys.modules:
42-
logger.warning("Developer mode enabled via 'debugpy'")
43-
return True
4441

45-
if sys.gettrace() is not None:
46-
logger.warning("Developer mode enabled via 'sys.gettrace()'")
47-
return True
42+
curr_config = context.HASSETTE_CONFIG.get(None)
43+
if curr_config:
44+
# not sure if we can even change this during runtime, but for now we are not
45+
# going to allow it
46+
return curr_config.dev_mode
4847

49-
if sys.flags.dev_mode:
50-
logger.warning("Developer mode enabled via 'python -X dev'")
51-
return True
48+
logger = logging.getLogger(__name__)
5249

53-
return False
50+
enabled = False
51+
reason = None
52+
53+
if "debugpy" in sys.modules:
54+
enabled = True
55+
reason = "debugpy"
56+
elif sys.gettrace() is not None:
57+
enabled = True
58+
reason = "sys.gettrace()"
59+
elif sys.flags.dev_mode:
60+
enabled = True
61+
reason = "python -X dev"
62+
63+
if enabled:
64+
logger.info("Developer mode enabled (%s)", reason)
65+
66+
return enabled
5467

5568

5669
def default_config_dir() -> Path:

src/hassette/services/app_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ async def refresh_config(self) -> tuple[dict[str, "AppManifest"], dict[str, "App
363363
# Reinitialize config to pick up changes.
364364
# https://docs.pydantic.dev/latest/concepts/pydantic_settings/#in-place-reloading
365365
try:
366-
self.hassette.config.__init__()
366+
self.hassette.config.reload()
367367
except Exception as e:
368368
self.logger.exception("Failed to reload configuration: %s", e)
369369

@@ -383,7 +383,7 @@ async def handle_changes(self, changed_file_path: Path | None = None) -> None:
383383
orphans, new_apps, reimport_apps, reload_apps = self._calculate_app_changes(
384384
original_apps_config, curr_apps_config, changed_file_path
385385
)
386-
self.logger.debug(
386+
self.logger.info(
387387
"App changes detected - orphans: %s, new: %s, reimport: %s, reload: %s",
388388
orphans,
389389
new_apps,

tests/test_apps.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,32 @@ def test_all_apps(self) -> None:
5454
assert "MyApp" in class_names, "MyApp should be in the list of running apps"
5555
assert "MyAppSync" in class_names, "MyAppSync should be in the list of running apps"
5656

57+
async def test_handle_changes_does_not_lose_apps(self) -> None:
58+
"""Verify that calling handle_changes() without config modifications preserves all running apps."""
59+
60+
orig_apps = set(self.app_handler.apps.keys())
61+
62+
event = asyncio.Event()
63+
64+
async def handler(*args, **kwargs): # noqa
65+
self.hassette.task_bucket.post_to_loop(event.set)
66+
67+
self.hassette._bus_service.add_listener(
68+
Listener.create(
69+
self.app_handler.task_bucket,
70+
owner="test",
71+
topic=HASSETTE_EVENT_APP_LOAD_COMPLETED,
72+
handler=handler,
73+
where=None,
74+
)
75+
)
76+
77+
await self.app_handler.handle_changes()
78+
await asyncio.wait_for(event.wait(), timeout=1)
79+
80+
new_apps = set(self.app_handler.apps.keys())
81+
assert orig_apps == new_apps, "No apps should be lost during handle_changes"
82+
5783
async def test_handle_changes_disables_app(self) -> None:
5884
"""Verify that editing hassette.toml to disable an app stops the running instance."""
5985

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)