Skip to content

Commit ec2107d

Browse files
authored
Merge pull request #2280 from AppDaemon/ast-parsing
Failing app fixes
2 parents f1b56f7 + 2a31cc9 commit ec2107d

6 files changed

Lines changed: 58 additions & 18 deletions

File tree

appdaemon/app_management.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ async def check_app_config_files(self, update_actions: UpdateActions):
615615
self._compare_sequences(update_actions, cfg, files_to_read)
616616
continue
617617

618-
if name in self.non_apps:
618+
if name in self.non_apps or cfg.disable:
619619
continue
620620

621621
# New config found
@@ -685,8 +685,14 @@ def import_module(self, module_name: str) -> int:
685685
if not module_name.startswith("appdaemon"):
686686
self.logger.debug("Importing '%s'", module_name)
687687
importlib.import_module(module_name)
688-
except SyntaxError as exc:
689-
path = Path(exc.filename)
688+
except Exception as exc:
689+
match exc:
690+
case SyntaxError():
691+
path = Path(exc.filename)
692+
case NameError():
693+
path = Path(traceback.extract_tb(exc.__traceback__)[-1].filename)
694+
case _:
695+
raise exc
690696
mtime = self.dependency_manager.python_deps.files.mtimes.get(path)
691697
self.dependency_manager.python_deps.bad_files.add((path, mtime))
692698
raise exc
@@ -989,13 +995,16 @@ async def _stop_apps(self, update_actions: UpdateActions):
989995
update_actions.apps.reload -= failed_to_stop
990996

991997
async def _start_apps(self, update_actions: UpdateActions):
998+
if failed := update_actions.apps.failed:
999+
self.logger.warning('Failed to start apps: %s', failed)
1000+
9921001
start_order = update_actions.apps.start_sort(self.dependency_manager)
9931002
if start_order:
9941003
self.logger.info("Starting apps: %s", update_actions.apps.init_set)
9951004
self.logger.debug("App start order: %s", start_order)
9961005

9971006
for app_name in start_order:
998-
if isinstance((cfg := self.app_config.root[app_name]), AppConfig):
1007+
if isinstance((cfg := self.app_config.root[app_name]), AppConfig) and not cfg.disable:
9991008
@ade.wrap_async(self.error, self.AD.app_dir, f"'{app_name}' instantiation")
10001009
async def safe_create(self: "AppManagement"):
10011010
try:

appdaemon/dependency.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def gen_modules() -> Generator[str, None, None]:
9292
mod: ast.Module = ast.parse(file_content, filename=file_path)
9393
except Exception as e:
9494
logger.warning(f"Error parsing python module with AST: {e}")
95+
raise e
9596
else:
9697
for node in get_imports(mod):
9798
match node:
@@ -107,18 +108,32 @@ def gen_modules() -> Generator[str, None, None]:
107108
return set(gen_modules())
108109

109110

110-
def get_dependency_graph(files: Iterable[Path], exclude: set[Path] | None = None):
111-
graph = {
112-
get_full_module_name(f): get_file_deps(f)
113-
for f in files
114-
if exclude is None or f not in exclude
115-
}
111+
def get_dependency_graph(
112+
files: Iterable[Path],
113+
exclude: set[Path] | None = None
114+
) -> tuple[dict[str, set[str]], set[Path]]:
115+
"""Gets the dependency graph for some Python files.
116+
117+
Returns:
118+
A tuple containing:
119+
- A dictionary where keys are module names and values are sets of module names that the key module depends on.
120+
- A set of paths that failed to parse or resolve dependencies.
121+
"""
122+
graph = {}
123+
failed = set()
124+
for f in files:
125+
if exclude is None or f not in exclude:
126+
try:
127+
graph[get_full_module_name(f)] = get_file_deps(f)
128+
except Exception:
129+
failed.add(f)
130+
continue
116131

117132
for mod, deps in graph.items():
118133
if mod in deps:
119134
deps.remove(mod)
120135

121-
return graph
136+
return graph, failed
122137

123138

124139
def get_all_nodes(deps: dict[str, set[str]]) -> set[str]:

appdaemon/dependency_manager.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC
2+
from copy import deepcopy
23
from dataclasses import InitVar, dataclass, field
34
from pathlib import Path
45
from typing import Iterable
@@ -16,14 +17,14 @@ class Dependencies(ABC):
1617
ext: str = field(init=False) # this has to be defined by the children classes
1718
dep_graph: dict[str, set[str]] = field(init=False)
1819
rev_graph: dict[str, set[str]] = field(init=False)
19-
bad_files: set[Path] = field(default_factory=set, init=False)
20+
bad_files: set[tuple[Path, float]] = field(default_factory=set, init=False)
2021

2122
def __post_init__(self):
2223
self.refresh_dep_graph()
2324

2425
def update(self, new_files: Iterable[Path]):
2526
self.files.update(new_files)
26-
for bf, mtime in self.bad_files:
27+
for bf, mtime in deepcopy(self.bad_files):
2728
new_mtime = self.files.mtimes.get(bf)
2829
if new_mtime != mtime:
2930
assert new_mtime > mtime, f"File {bf} was modified in the future"
@@ -65,7 +66,12 @@ def refresh_dep_graph(self):
6566
if self.bad_files:
6667
bad_files, _ = zip(*self.bad_files)
6768
bad_files = set(bad_files)
68-
self.dep_graph = get_dependency_graph(self.files, exclude=bad_files)
69+
70+
self.dep_graph, failed = get_dependency_graph(self.files, exclude=bad_files)
71+
72+
for file in failed:
73+
self.bad_files.add((file, self.files.mtimes.get(file, 0)))
74+
6975
self.rev_graph = reverse_graph(self.dep_graph)
7076

7177
def modules_to_import(self) -> set[str]:

appdaemon/exceptions.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ def user_exception_block(logger: Logger, exception: AppDaemonException, app_dir:
5454
"""Function to generate a user-friendly block of text for an exception. Gets the whole chain of exception causes to decide what to do.
5555
"""
5656
width = 75
57+
spacing = 4
5758
inset = 5
5859
if header is not None:
59-
header = f'{"=" * inset} {header} {"=" * (width - inset - len(header))}'
60+
header = f'{"=" * inset} {header} {"=" * (width - spacing - inset - len(header))}'
6061
else:
6162
header = '=' * width
6263
logger.error(header)
@@ -86,10 +87,12 @@ def user_exception_block(logger: Logger, exception: AppDaemonException, app_dir:
8687
logger.error(f'{indent}{filename} line {line} in {func_name}')
8788
case OSError() if str(exc).endswith('address already in use'):
8889
logger.error(f'{indent}{exc.__class__.__name__}: {exc}')
89-
case NameError():
90+
case NameError() | ImportError():
9091
logger.error(f'{indent}{exc.__class__.__name__}: {exc}')
9192
if tb := traceback.extract_tb(exc.__traceback__):
9293
frame = tb[-1]
94+
file = Path(frame.filename).relative_to(app_dir.parent)
95+
logger.error(f'{indent} line {frame.lineno} in {file.name}')
9396
logger.error(f'{indent} {frame._line.rstrip()}')
9497
error_len = frame.end_colno - frame.colno
9598
logger.error(f'{indent} {" " * (frame.colno - 1)}{"^" * error_len}')
@@ -112,7 +115,7 @@ def user_exception_block(logger: Logger, exception: AppDaemonException, app_dir:
112115
for line in lines:
113116
logger.error(f'{indent}{line}')
114117

115-
logger.error('=' * 75)
118+
logger.error('=' * width)
116119

117120

118121
def unexpected_block(logger: Logger, exception: Exception):

appdaemon/models/internal/app_management.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def start_sort(self, dm: DependencyManager, logger: Logger = None) -> list[str]:
7171
Uses a dependency graph to sort the internal ``init`` and ``reload`` sets together
7272
"""
7373
items = copy(self.init_set)
74-
items |= find_all_dependents(items, dm.app_deps.rev_graph)
74+
items |= find_all_dependents(items, dm.app_deps.dep_graph)
7575
priorities = {
7676
app_name: dm.app_deps.app_config.root[app_name].priority
7777
for app_name in items

docs/HISTORY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ None
99
**Fixes**
1010

1111
- Revert unintentional sync vs async changes
12+
- Respecting the `disable` key in app configurations when
13+
- processing module import order
14+
- processing app start order
15+
- Fixed a bug where a failed app could get re-introduced to the start order only to fail again
16+
- Fixed a bug where changing files at exactly right time would throw an error
17+
- Files with syntax errors get added to `bad_files` list, even if they aren't used for apps
18+
- Improved error text for `ImportErrors` and `SyntaxErrors` in user apps
1219
- Add a fix to the http component to handle URLs with no port, and URLs with port 80
1320

1421
**Breaking Changes**

0 commit comments

Comments
 (0)