Skip to content

Commit 19df40d

Browse files
Allow force run app on cloud if loading locally errors (#15019)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ec156ad commit 19df40d

File tree

5 files changed

+73
-16
lines changed

5 files changed

+73
-16
lines changed

src/lightning_app/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
2626
- Add a `JustPyFrontend` to ease UI creation with `https://github.com/justpy-org/justpy` ([#15002](https://github.com/Lightning-AI/lightning/pull/15002))
2727
- Added a layout endpoint to the Rest API and enable to disable pulling or pushing to the state ([#15367](https://github.com/Lightning-AI/lightning/pull/15367)
2828
- Added support for functions for `configure_api` and `configure_commands` to be executed in the Rest API process ([#15098](https://github.com/Lightning-AI/lightning/pull/15098)
29-
29+
- Added support to start lightning app on cloud without needing to install dependencies locally ([#15019](https://github.com/Lightning-AI/lightning/pull/15019)
3030

3131
### Changed
3232

src/lightning_app/runners/cloud.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import string
55
import sys
66
import time
7+
import traceback
78
from dataclasses import dataclass
89
from pathlib import Path
910
from typing import Any, Callable, List, Optional, Union
@@ -60,6 +61,7 @@
6061
from lightning_app.utilities.app_helpers import Logger
6162
from lightning_app.utilities.cloud import _get_project
6263
from lightning_app.utilities.dependency_caching import get_hash
64+
from lightning_app.utilities.load_app import _prettifiy_exception, load_app_from_file
6365
from lightning_app.utilities.packaging.app_config import AppConfig, find_config_file
6466
from lightning_app.utilities.packaging.lightning_utils import _prepare_lightning_wheels_and_requirements
6567
from lightning_app.utilities.secrets import _names_to_ids
@@ -463,6 +465,30 @@ def _project_has_sufficient_credits(self, project: V1Membership, app: Optional[L
463465

464466
return balance >= 1
465467

468+
@classmethod
469+
def load_app_from_file(cls, filepath: str) -> "LightningApp":
470+
"""This is meant to use only locally for cloud runtime."""
471+
try:
472+
app = load_app_from_file(filepath, raise_exception=True)
473+
except ModuleNotFoundError:
474+
# this is very generic exception.
475+
logger.info("Could not load the app locally. Starting the app directly on the cloud.")
476+
# we want to format the exception as if no frame was on top.
477+
exp, val, tb = sys.exc_info()
478+
listing = traceback.format_exception(exp, val, tb)
479+
# remove the entry for the first frame
480+
del listing[1]
481+
from lightning_app.testing.helpers import EmptyFlow
482+
483+
# Create a mocking app.
484+
app = LightningApp(EmptyFlow())
485+
486+
except FileNotFoundError as e:
487+
raise e
488+
except Exception:
489+
_prettifiy_exception(filepath)
490+
return app
491+
466492

467493
def _create_mount_drive_spec(work_name: str, mount: Mount) -> V1LightningworkDrives:
468494
if mount.protocol == "s3://":

src/lightning_app/runners/runtime.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def dispatch(
5656

5757
runtime_type = RuntimeType(runtime_type)
5858
runtime_cls: Type[Runtime] = runtime_type.get_runtime()
59-
app = load_app_from_file(str(entrypoint_file))
59+
app = runtime_cls.load_app_from_file(str(entrypoint_file))
6060

6161
env_vars = {} if env_vars is None else env_vars
6262
secrets = {} if secrets is None else secrets
@@ -151,3 +151,8 @@ def _add_stopped_status_to_work(self, work: "lightning_app.LightningWork") -> No
151151
latest_call_hash = work._calls[CacheCallsKeys.LATEST_CALL_HASH]
152152
if latest_call_hash in work._calls:
153153
work._calls[latest_call_hash]["statuses"].append(make_status(WorkStageStatus.STOPPED))
154+
155+
@classmethod
156+
def load_app_from_file(cls, filepath: str) -> "LightningApp":
157+
158+
return load_app_from_file(filepath)

src/lightning_app/utilities/load_app.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,28 @@
1616
logger = Logger(__name__)
1717

1818

19-
def load_app_from_file(filepath: str) -> "LightningApp":
19+
def _prettifiy_exception(filepath: str):
20+
"""Pretty print the exception that occurred when loading the app."""
21+
# we want to format the exception as if no frame was on top.
22+
exp, val, tb = sys.exc_info()
23+
listing = traceback.format_exception(exp, val, tb)
24+
# remove the entry for the first frame
25+
del listing[1]
26+
listing = [
27+
f"Found an exception when loading your application from {filepath}. Please, resolve it to run your app.\n\n"
28+
] + listing
29+
logger.error("".join(listing))
30+
sys.exit(1)
31+
32+
33+
def load_app_from_file(filepath: str, raise_exception: bool = False) -> "LightningApp":
34+
"""Load a LightningApp from a file.
35+
36+
Arguments:
37+
filepath: The path to the file containing the LightningApp.
38+
raise_exception: If True, raise an exception if the app cannot be loaded.
39+
"""
40+
2041
# Taken from StreamLit: https://github.com/streamlit/streamlit/blob/develop/lib/streamlit/script_runner.py#L313
2142

2243
from lightning_app.core.app import LightningApp
@@ -30,17 +51,10 @@ def load_app_from_file(filepath: str) -> "LightningApp":
3051
try:
3152
with _patch_sys_argv():
3253
exec(code, module.__dict__)
33-
except Exception:
34-
# we want to format the exception as if no frame was on top.
35-
exp, val, tb = sys.exc_info()
36-
listing = traceback.format_exception(exp, val, tb)
37-
# remove the entry for the first frame
38-
del listing[1]
39-
listing = [
40-
f"Found an exception when loading your application from {filepath}. Please, resolve it to run your app.\n\n"
41-
] + listing
42-
logger.error("".join(listing))
43-
sys.exit(1)
54+
except Exception as e:
55+
if raise_exception:
56+
raise e
57+
_prettifiy_exception(filepath)
4458

4559
apps = [v for v in module.__dict__.values() if isinstance(v, LightningApp)]
4660
if len(apps) > 1:

tests/tests_app/runners/test_cloud.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
from copy import copy
34
from pathlib import Path
45
from unittest import mock
@@ -36,9 +37,10 @@
3637
V1Work,
3738
)
3839

39-
from lightning_app import LightningApp, LightningWork
40-
from lightning_app.runners import backends, cloud
40+
from lightning_app import _PROJECT_ROOT, LightningApp, LightningWork
41+
from lightning_app.runners import backends, cloud, CloudRuntime
4142
from lightning_app.storage import Drive, Mount
43+
from lightning_app.testing.helpers import EmptyFlow
4244
from lightning_app.utilities.cloud import _get_project
4345
from lightning_app.utilities.dependency_caching import get_hash
4446
from lightning_app.utilities.packaging.cloud_compute import CloudCompute
@@ -1082,3 +1084,13 @@ def test_project_has_sufficient_credits():
10821084
for balance, result in credits_and_test_value:
10831085
project = V1Membership(name="test-project1", project_id="test-project-id1", balance=balance)
10841086
assert cloud_runtime._project_has_sufficient_credits(project) is result
1087+
1088+
1089+
@mock.patch(
1090+
"lightning_app.runners.cloud.load_app_from_file",
1091+
MagicMock(side_effect=ModuleNotFoundError("Module X not found")),
1092+
)
1093+
def test_load_app_from_file_module_error():
1094+
empty_app = CloudRuntime.load_app_from_file(os.path.join(_PROJECT_ROOT, "examples", "app_v0", "app.py"))
1095+
assert isinstance(empty_app, LightningApp)
1096+
assert isinstance(empty_app.root, EmptyFlow)

0 commit comments

Comments
 (0)