Add CI for python lightning app Python unit tests (#13491)

* Update lightning_app src

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update lightning app tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add CI

* update tests

* requirements

* fix version tests

* todo

* fix tests

* fix tests

* fix tests

* fix tests

* fix formatting

Co-authored-by: mansy <mansy@lightning.ai>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: awaelchli <aedu.waelchli@gmail.com>
This commit is contained in:
Mansy 2022-07-01 22:28:44 +02:00 committed by GitHub
parent 3daa244458
commit dc70b6511c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1599 additions and 151 deletions

View File

@ -19,18 +19,10 @@ jobs:
echo "$file"
done
- name: Block edits in src/lightning_app
if: contains(steps.changed-files.outputs.all_changed_and_modified_files, 'src/lightning_app')
run: exit 1
- name: Block edits in docs/source-app
if: contains(steps.changed-files.outputs.all_changed_and_modified_files, 'docs/source-app')
run: exit 1
- name: Block edits in tests/tests_app
if: contains(steps.changed-files.outputs.all_changed_and_modified_files, 'tests/tests_app')
run: exit 1
- name: Block edits in examples/app
if: contains(steps.changed-files.outputs.all_changed_and_modified_files, 'examples/app_')
run: exit 1

140
.github/workflows/ci-app_tests.yml vendored Normal file
View File

@ -0,0 +1,140 @@
name: CI App Tests
# see: https://help.github.com/en/actions/reference/events-that-trigger-workflows
on: # Trigger the workflow on push or pull request, but only for the master branch
push:
branches:
- "master"
pull_request:
paths:
- "src/lightning_app/**"
- "tests/tests_app/**"
- "requirements/app/**"
- "setup.py"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
pytest:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macOS-10.15, windows-2019]
python-version: [3.8]
requires: ["oldest", "latest"]
# Timeout: https://stackoverflow.com/a/59076067/4521646
timeout-minutes: 20
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
# TODO: use replace_oldest_ver() instead
- name: Set min. dependencies
if: matrix.requires == 'oldest'
run: |
for fpath in ('requirements/app/base.txt', 'requirements/app/test.txt'):
req = open(fpath).read().replace('>=', '==')
open(fpath, 'w').write(req)
shell: python
- run: echo "::set-output name=period::$(python -c 'import time ; days = time.time() / 60 / 60 / 24 ; print(int(days / 7))' 2>&1)"
if: matrix.requires == 'latest'
id: times
# Note: This uses an internal pip API and may not always work
# https://github.com/actions/cache/blob/master/examples.md#multiple-oss-in-a-workflow
- name: Get pip cache
id: pip-cache
run: |
python -c "from pip._internal.locations import USER_CACHE_DIR; print('::set-output name=dir::' + USER_CACHE_DIR)"
- name: Cache pip
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ matrix.requires }}-td${{ steps.times.outputs.period }}-${{ hashFiles('requirements/app/base.txt') }}
restore-keys: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ matrix.requires }}-td${{ steps.times.outputs.period }}-
- name: Install dependencies
run: |
pip --version
pip install -r requirements/app/devel.txt --quiet --find-links https://download.pytorch.org/whl/cpu/torch_stable.html
pip list
shell: bash
# - name: Start Redis
# if: runner.os == 'Linux'
# uses: supercharge/redis-github-action@1.4.0
# with:
# redis-version: 6
# redis-port: 6379
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install Yarn
run: npm install -g yarn
- name: Install Lightning as top-level
run: pip install -e . -r requirements/app/base.txt
shell: bash
- name: Tests
working-directory: ./tests
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
PYTEST_ARTIFACT: results-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.requires }}.xml
run: |
coverage run --source lightning_app -m pytest -m "not cloud" tests_app --timeout=300 -vvvv --junitxml=$PYTEST_ARTIFACT --durations=0
- name: Upload pytest test results
uses: actions/upload-artifact@v2
with:
name: unittest-results-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.requires }}
path: tests/results-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.requires }}.xml
if: failure()
- name: Statistics
if: success()
working-directory: ./tests
run: |
coverage xml -i
coverage report -i
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: tests/coverage.xml
flags: unittests
env_vars: OS,PYTHON
name: codecov-umbrella
fail_ci_if_error: false
# TODO: figure out why we clone and install quick-start
# - name: Clone Quick Start Example Repo
# uses: actions/checkout@v3
# # TODO: this needs to be git submodule
# if: matrix.os == 'windows-2019' # because the install doesn't work on windows
# with:
# repository: Lightning-AI/lightning-quick-start
# ref: 'main'
# path: lightning-quick-start
#
# - name: Lightning Install quick-start
# shell: bash
# if: matrix.os != 'windows-2019' # because the install doesn't work on windows
# run: |
# python -m lightning install app lightning/quick-start -y

View File

@ -37,6 +37,6 @@ if _module_available("lightning_app.components.demo"):
from lightning_app.components import demo # noqa: F401
_PACKAGE_ROOT = os.path.dirname(__file__)
_PROJECT_ROOT = os.path.dirname(_PACKAGE_ROOT)
_PROJECT_ROOT = os.path.dirname(os.path.dirname(_PACKAGE_ROOT))
__all__ = ["LightningApp", "LightningFlow", "LightningWork", "BuildConfig", "CloudCompute"]

View File

@ -58,8 +58,8 @@ coverage.xml
# Sphinx documentation
docs/_build/
docs/source/api/
docs/source/*.md
docs/source-app/api/
docs/source-app/*.md
# PyBuilder
target/
@ -132,9 +132,9 @@ coverage.*
# Frontend build artifacts
*lightning_app/ui*
gradio_cached_examples
/docs/source/api_reference/generated/*
/docs/source-app/api_reference/generated/*
examples/my_own_leaderboard/submissions/*
docs/source/api_reference/generated/*
docs/source-app/api_reference/generated/*
*.ckpt
redis-stable
node_modules

View File

@ -1,9 +1,9 @@
from placeholdername import ComponentA, ComponentB
import lightning_app as la
import lightning as L
class LitApp(la.LightningFlow):
class LitApp(L.LightningFlow):
def __init__(self) -> None:
super().__init__()
self.component_a = ComponentA()
@ -14,4 +14,4 @@ class LitApp(la.LightningFlow):
self.component_b.run()
app = la.LightningApp(LitApp())
app = L.LightningApp(LitApp())

View File

@ -1,6 +1,6 @@
import lightning_app as la
import lightning as L
class ComponentA(la.LightningFlow):
class ComponentA(L.LightningFlow):
def run(self):
print("hello from component A")

View File

@ -1,6 +1,6 @@
import lightning_app as la
import lightning as L
class ComponentB(la.LightningFlow):
class ComponentB(L.LightningFlow):
def run(self):
print("hello from component B")

View File

@ -10,7 +10,7 @@ import io
import os
from contextlib import redirect_stdout
from lightning_app.testing.testing import application_testing, LightningTestApp
from lightning.app.testing.testing import application_testing, LightningTestApp
class LightningAppTestInt(LightningTestApp):

View File

@ -124,7 +124,7 @@ def component(component_name):
Use the component inside an app:
from {name_for_files} import TemplateComponent
import lightning_app as la
import lightning.app as la
class LitApp(la.LightningFlow):
def __init__(self) -> None:

View File

@ -49,10 +49,12 @@ def gallery_app(name, yes_arg, version_arg, cwd=None, overwrite=False):
app_entry = _resolve_resource(registry_url, name=name, version_arg=version_arg, resource_type="app")
# give the user the chance to do a manual install
source_url, git_url, folder_name = _show_install_app_prompt(app_entry, app, org, yes_arg, resource_type="app")
source_url, git_url, folder_name, git_sha = _show_install_app_prompt(
app_entry, app, org, yes_arg, resource_type="app"
)
# run installation if requested
_install_app(source_url, git_url, folder_name, cwd=cwd, overwrite=overwrite)
_install_app(source_url, git_url, folder_name, cwd=cwd, overwrite=overwrite, git_sha=git_sha)
def non_gallery_app(gh_url, yes_arg, cwd=None, overwrite=False):
@ -161,14 +163,16 @@ def _show_non_gallery_install_component_prompt(gh_url, yes_arg):
def _show_install_app_prompt(entry, app, org, yes_arg, resource_type):
source_url = entry["sourceUrl"] # This URL is used only to display the repo and extract folder name
full_git_url = entry["gitUrl"] # Used to clone the repo (can include tokens for private repos)
git_url = full_git_url.split("#ref=")[0]
git_url_parts = full_git_url.split("#ref=")
git_url = git_url_parts[0]
git_sha = git_url_parts[1] if len(git_url_parts) == 2 else None
folder_name = source_url.split("/")[-1]
# yes arg does not prompt the user for permission to install anything
# automatically creates env and sets up the project
if yes_arg:
return source_url, git_url, folder_name
return source_url, git_url, folder_name, git_sha
prompt = f"""
Installing Lightning {resource_type}
@ -192,7 +196,7 @@ def _show_install_app_prompt(entry, app, org, yes_arg, resource_type):
if not should_install:
raise KeyboardInterrupt()
return source_url, git_url, folder_name
return source_url, git_url, folder_name, git_sha
except KeyboardInterrupt:
repo = entry["sourceUrl"]
m = f"""
@ -367,7 +371,9 @@ def _install_with_env(repo_url, folder_name, cwd=None):
logger.info(m)
def _install_app(source_url: str, git_url: str, folder_name: str, cwd=None, overwrite: bool = False):
def _install_app(
source_url: str, git_url: str, folder_name: str, cwd=None, overwrite: bool = False, git_sha: str = None
):
"""Installing lighting app from the `git_url`
Args:
@ -381,6 +387,8 @@ def _install_app(source_url: str, git_url: str, folder_name: str, cwd=None, over
Working director. If not specified, current working directory is used.
overwrite:
If true, overwrite the app directory without asking if it already exists
git_sha:
The git_sha for checking out the git repo of the app.
"""
if not cwd:
@ -412,6 +420,15 @@ def _install_app(source_url: str, git_url: str, folder_name: str, cwd=None, over
os.chdir(f"{folder_name}")
cwd = os.getcwd()
try:
if git_sha:
subprocess.check_output(["git", "checkout", git_sha], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
if "did not match any" in str(e.output):
raise SystemExit("Looks like the git SHA is not valid or doesn't exist in app repo.")
else:
raise Exception(e)
# activate and install reqs
# TODO: remove shell=True... but need to run command in venv
logger.info("⚡ RUN: install requirements (pip install -r requirements.txt)")

View File

@ -52,7 +52,7 @@ jobs:
- name: Clone Template React UI Repo
uses: actions/checkout@v3
with:
repository: PyTorchLightning/lightning
repository: Lightning-AI/lightning
token: ${{ secrets.PAT_GHOST }}
ref: 'master'
path: lightning

View File

@ -58,8 +58,8 @@ coverage.xml
# Sphinx documentation
docs/_build/
docs/source/api/
docs/source/*.md
docs/source-app/api/
docs/source-app/*.md
# PyBuilder
target/
@ -132,9 +132,9 @@ coverage.*
# Frontend build artifacts
*lightning_app/ui*
gradio_cached_examples
/docs/source/api_reference/generated/*
/docs/source-app/api_reference/generated/*
examples/my_own_leaderboard/submissions/*
docs/source/api_reference/generated/*
docs/source-app/api_reference/generated/*
*.ckpt
redis-stable
node_modules

View File

@ -8,7 +8,7 @@ lightning init component placeholdername
## To run placeholdername
First, install placeholdername (warning: this app has not been officially approved on the lightning gallery):
First, install placeholdername (warning: this component has not been officially approved on the lightning gallery):
```bash
lightning install component https://github.com/theUser/placeholdername
@ -18,10 +18,10 @@ Once the app is installed, use it in an app:
```python
from placeholdername import TemplateComponent
import lightning_app as la
import lightning as L
class LitApp(lapp.LightningFlow):
class LitApp(L.LightningFlow):
def __init__(self) -> None:
super().__init__()
self.placeholdername = TemplateComponent()
@ -31,5 +31,5 @@ class LitApp(lapp.LightningFlow):
self.placeholdername.run()
app = lapp.LightningApp(LitApp())
app = L.LightningApp(LitApp())
```

View File

@ -1,9 +1,9 @@
from placeholdername import TemplateComponent
import lightning_app as la
import lightning as L
class LitApp(la.LightningFlow):
class LitApp(L.LightningFlow):
def __init__(self) -> None:
super().__init__()
self.placeholdername = TemplateComponent()
@ -13,4 +13,4 @@ class LitApp(la.LightningFlow):
self.placeholdername.run()
app = la.LightningApp(LitApp())
app = L.LightningApp(LitApp())

View File

@ -1,7 +1,7 @@
import lightning_app as la
import lightning as L
class TemplateComponent(la.LightningWork):
class TemplateComponent(L.LightningWork):
def __init__(self) -> None:
super().__init__()
self.value = 0

View File

@ -68,6 +68,7 @@ def _run_app(
)
env_vars = _format_input_env_variables(env)
os.environ.update(env_vars)
def on_before_run(*args):
if open_ui and not without_server:

View File

@ -4,10 +4,10 @@ from typing import Dict, List, Optional, Union
from core.components import TensorBoard, WeightsAndBiases
from core.components.script_runner import ScriptRunner
from lightning_app import LightningApp, LightningFlow
from lightning_app.frontend import StaticWebFrontend
from lightning_app.storage import Path
from lightning_app.utilities.packaging.cloud_compute import CloudCompute
from lightning.app import LightningApp, LightningFlow
from lightning.app.frontend import StaticWebFrontend
from lightning.app.storage import Path
from lightning.app.utilities.packaging.cloud_compute import CloudCompute
class ReactUI(LightningFlow):

View File

@ -5,7 +5,7 @@ from typing import Any, Dict, Optional, TYPE_CHECKING, Union
from core.state import ProgressBarState, TrainerState
import pytorch_lightning as pl
from lightning_app.storage import Path
from lightning.app.storage import Path
from pytorch_lightning import Callback
from pytorch_lightning.callbacks.progress.base import get_standard_metrics
from pytorch_lightning.loggers import TensorBoardLogger, WandbLogger

View File

@ -2,8 +2,8 @@ import subprocess
import time
from typing import Dict, List
from lightning_app import BuildConfig, LightningFlow, LightningWork
from lightning_app.storage import Path
from lightning.app import BuildConfig, LightningFlow, LightningWork
from lightning.app.storage import Path
class TensorBoard(LightningFlow):

View File

@ -1,7 +1,7 @@
import os
from typing import Dict, List, Optional, TYPE_CHECKING
from lightning_app import LightningFlow
from lightning.app import LightningFlow
if TYPE_CHECKING:
import wandb

View File

@ -1,14 +1,12 @@
import os
import sys
import traceback
from typing import Any, Dict, List, Optional, Tuple
from pkg_resources import parse_requirements
from lightning_app.components.python import TracerPythonScript
from lightning_app.storage import Path
from lightning_app.utilities.packaging.build_config import BuildConfig
from lightning_app.utilities.tracer import Tracer
from lightning.app.components.python import TracerPythonScript
from lightning.app.storage import Path
from lightning.app.utilities.packaging.build_config import BuildConfig
from lightning.app.utilities.tracer import Tracer
from lightning_app.utilities.packaging.build_config import load_requirements
class ScriptRunner(TracerPythonScript):
@ -76,7 +74,6 @@ class ScriptRunner(TracerPythonScript):
]
if Path(root_path, "requirements.txt").exists():
# Requirements from the user's code folder
path_req = os.path.join(root_path, "requirements.txt")
requirements.extend(list(map(str, parse_requirements(open(path_req).readlines()))))
requirements.extend(load_requirements(root_path, file_name="requirements.txt"))
return BuildConfig(requirements=requirements)

View File

@ -5,7 +5,7 @@ import pytest
from core.callbacks import PLAppArtifactsTracker, PLAppProgressTracker, PLAppSummary
from core.components.script_runner import ScriptRunner
from lightning_app.storage import Path
from lightning.app.storage import Path
from pytorch_lightning import LightningModule, Trainer
from pytorch_lightning.loggers import TensorBoardLogger

View File

@ -4,7 +4,7 @@ This is a full react template ready to use in a component
This UI was automatically generated with:
```bash
```commandline
lightning init react-ui
```
@ -34,12 +34,12 @@ lightning run app react-ui/example_app.py
To connect the react UI to your component, simply point the `StaticWebFrontend` to the `dist/` folder generated by yarn after building your react website.
```python
import lightning_app as la
import lightning as L
class YourComponent(lapp.LightningFlow):
class YourComponent(L.LightningFlow):
def configure_layout(self):
return lapp.frontend.StaticWebFrontend(Path(__file__).parent / "react-ui/src/dist")
return Lapp.frontend.StaticWebFrontend(Path(__file__).parent / "react-ui/src/dist")
```
### Set up interactions between React and the component

View File

@ -2,20 +2,21 @@
from pathlib import Path
import lightning_app as la
import lightning as L
from lightning.app import frontend
class YourComponent(la.LightningFlow):
class YourComponent(L.LightningFlow):
def __init__(self):
super().__init__()
self.message_to_print = "Hello World!"
self.should_print = False
def configure_layout(self):
return la.frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
return frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
class HelloLitReact(la.LightningFlow):
class HelloLitReact(L.LightningFlow):
def __init__(self):
super().__init__()
self.counter = 0
@ -30,4 +31,4 @@ class HelloLitReact(la.LightningFlow):
return [{"name": "React UI", "content": self.react_ui}]
app = la.LightningApp(HelloLitReact())
app = L.LightningApp(HelloLitReact())

View File

@ -10,7 +10,7 @@
"dependencies": {
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.5.0",
"@mui/material": "5.8.5",
"axios": "^0.26.1",
"lodash": "^4.17.21",
"nanoid": "^3.3.1",

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ class PopenPythonScript(LightningWork):
env: Optional[Dict] = None,
**kwargs,
):
"""The PopenPythonScript Class enables to easily run a Python Script.
"""The PopenPythonScript component enables to easily run a python script within a subprocess.
Arguments:
script_path: Path of the python script to run.
@ -56,7 +56,7 @@ class PopenPythonScript(LightningWork):
In this example, the script will be launch with the :class:`~subprocess.Popen`.
.. literalinclude:: ../../../../examples/components/python/component_popen.py
.. literalinclude:: ../../../../examples/app_components/python/component_popen.py
:language: python
"""
super().__init__(**kwargs)

View File

@ -33,9 +33,12 @@ class TracerPythonScript(LightningWork):
env: Optional[Dict] = None,
**kwargs,
):
"""The TracerPythonScript Class enables to easily run a Python Script with Lightning
:class:`~lightning_app.utilities.tracer.Tracer`. Simply overrides the
:meth:`~lightning_app.components.python.tracer.TracerPythonScript.configure_tracer` method.
"""The TracerPythonScript class enables to easily run a python script.
When subclassing this class, you can configure your own :class:`~lightning_app.utilities.tracer.Tracer`
by :meth:`~lightning_app.components.python.tracer.TracerPythonScript.configure_tracer` method
The tracer is quite a magical class. It enables you to inject core into a script execution without changing it.
Arguments:
script_path: Path of the python script to run.
@ -47,6 +50,13 @@ class TracerPythonScript(LightningWork):
Raises:
FileNotFoundError: If the provided `script_path` doesn't exists.
**How does it works ?**
It works by executing the python script with python built-in `runpy
<https://docs.python.org/3/library/runpy.html>`_ run_path method.
This method takes any python globals before executing the script,
e.g you can modify classes or function from the script.
.. doctest::
>>> from lightning_app.components.python import TracerPythonScript
@ -59,17 +69,27 @@ class TracerPythonScript(LightningWork):
Hello World !
>>> os.remove("a.py")
In this example, you would be able to implement your own :class:`~lightning_app.utilities.tracer.Tracer`
and intercept / modify elements while the script is being executed.
In the example below, we subclass the :class:`~lightning_app.components.python.TracerPythonScript`
component and override its configure_tracer method.
.. literalinclude:: ../../../../examples/components/python/component_tracer.py
Using the Tracer, we are patching the ``__init__`` method of the PyTorch Lightning Trainer.
Once the script starts running and if a Trainer is instantiated, the provided ``pre_fn`` is
called and we inject a Lightning callback.
This callback has a reference to the work and on every batch end, we are capturing the
trainer ``global_step`` and ``best_model_path``.
Even more interesting, this component works for ANY Pytorch Lightning script and
its state can be used in real time in a UI.
.. literalinclude:: ../../../../examples/app_components/python/component_tracer.py
:language: python
Once implemented, this component can easily be integrated within a larger app
to execute a specific python script.
.. literalinclude:: ../../../../examples/components/python/app.py
.. literalinclude:: ../../../../examples/app_components/python/app.py
:language: python
"""
super().__init__(**kwargs)

View File

@ -18,7 +18,7 @@ class ServeGradio(LightningWork, abc.ABC):
In the example below, the ``ServeGradio`` is subclassed to deploy ``AnimeGANv2``.
.. literalinclude:: ../../../../examples/components/serve/gradio/app.py
.. literalinclude:: ../../../../examples/app_components/serve/gradio/app.py
:language: python
The result would be the following:

View File

@ -37,26 +37,33 @@ class LightningApp:
root: "lightning_app.LightningFlow",
debug: bool = False,
):
"""LightningApp, or App in short, alternatively run its root
:class:`~lightning_app.core.flow.LightningFlow` component and collects state changes from external
sources to maintain the application state up-to-date or performs checkpointing. All those operations
are executed within an infinite loop.
"""The Lightning App, or App in short runs a tree of one or more components that interact to create end-to-end
applications. There are two kinds of components: :class:`~lightning_app.core.flow.LightningFlow` and
:class:`~lightning_app.core.work.LightningWork`. This modular design enables you to reuse components
created by other users.
The Lightning App alternatively run an event loop triggered by delta changes sent from
either :class:`~lightning.app.core.work.LightningWork` or from the Lightning UI.
Once deltas are received, the Lightning App runs
the :class:`~lightning.app.core.flow.LightningFlow` provided.
Arguments:
root: The root LightningFlow component, that defined all the app's nested components, running infinitely.
debug: Whether to run the application in debug model.
root: The root LightningFlow component, that defined all
the app's nested components, running infinitely.
debug: Whether to activate the Lightning Logger debug mode.
This can be helpful when reporting bugs on Lightning repo.
.. doctest::
>>> from lightning_app import LightningFlow, LightningApp
>>> from lightning_app.runners import SingleProcessRuntime
>>> from lightning import LightningFlow, LightningApp
>>> from lightning.app.runners import MultiProcessRuntime
>>> class RootFlow(LightningFlow):
... def run(self):
... print("Hello World!")
... self._exit()
...
>>> app = LightningApp(RootFlow()) # application can be dispatched using the `runners`.
>>> SingleProcessRuntime(app).dispatch()
>>> MultiProcessRuntime(app).dispatch()
Hello World!
"""

View File

@ -25,10 +25,10 @@ class LightningFlow:
}
def __init__(self):
"""The LightningFlow is a building block to coordinate and manage long running-tasks contained within
:class:`~lightning_app.core.work.LightningWork` or nested LightningFlow.
"""The LightningFlow is used by the :class:`~lightning_app.core.app.LightningApp` to coordinate and manage
long- running jobs contained, the :class:`~lightning_app.core.work.LightningWork`.
At a minimum, a LightningFlow is characterized by:
A LightningFlow is characterized by:
* A set of state variables.
* Long-running jobs (:class:`~lightning_app.core.work.LightningWork`).
@ -41,11 +41,6 @@ class LightningFlow:
They also may not reach into global variables unless they are constant.
.. note ::
The limitation to primitive types will be lifted in time for
certain aggregate types, and will be made extensible so that component
developers will be able to add custom state-compatible types.
The attributes need to be all defined in `__init__` method,
and eventually assigned to different values throughout the lifetime of the object.
However, defining new attributes outside of `__init__` is not allowed.
@ -83,7 +78,7 @@ class LightningFlow:
.. doctest::
>>> from lightning_app import LightningFlow
>>> from lightning import LightningFlow
>>> class RootFlow(LightningFlow):
... def __init__(self):
... super().__init__()
@ -345,6 +340,7 @@ class LightningFlow:
return name in LightningFlow._INTERNAL_STATE_VARS or not name.startswith("_")
def run(self, *args, **kwargs) -> None:
"""Override with your own logic."""
pass
def schedule(
@ -352,15 +348,16 @@ class LightningFlow:
) -> bool:
"""The schedule method is used to run a part of the flow logic on timely manner.
.. code-block::
.. code-block:: python
from lightning_app import LightningFlow
class Flow(LightningFlow):
class Flow(LightningFlow):
def run(self):
if self.schedule("hourly"):
# run some code once every hour.
print("run this every hour")
Arguments:
cron_pattern: The cron pattern to provide. Learn more at https://crontab.guru/.
@ -370,7 +367,7 @@ class LightningFlow:
A best practice is to avoid running a dynamic flow or work under the self.schedule method.
Instead, instantiate them within the condition, but run them outside.
.. code-block:: python
.. code-block:: python
from lightning_app import LightningFlow
from lightning_app.structures import List
@ -382,11 +379,40 @@ class LightningFlow:
self.dags = List()
def run(self):
if self.schedule("@hourly"):
if self.schedule("hourly"):
self.dags.append(DAG(...))
for dag in self.dags:
payload = dag.run()
**Learn more about Scheduling**
.. raw:: html
<div class="display-card-container">
<div class="row">
.. displayitem::
:header: Schedule your components
:description: Learn more scheduling.
:col_css: col-md-4
:button_link: ../../../glossary/scheduling.html
:height: 180
:tag: Basic
.. displayitem::
:header: Build your own DAG
:description: Learn more DAG scheduling with examples.
:col_css: col-md-4
:button_link: ../../../examples/app_dag/dag.html
:height: 180
:tag: Basic
.. raw:: html
</div>
</div>
<br />
"""
if not user_key:
frame = cast(FrameType, inspect.currentframe()).f_back
@ -454,40 +480,48 @@ class LightningFlow:
**Example:** Serve a static directory (with at least a file index.html inside).
.. code-block::
.. code-block:: python
from lightning_app.frontend import StaticWebFrontend
class Flow(LightningFlow):
...
def configure_layout(self):
return StaticWebFrontend("path/to/folder/to/serve")
**Example:** Serve a streamlit UI (needs the streamlit package to be installed).
.. code-block::
.. code-block:: python
from lightning_app.frontend import StaticWebFrontend
class Flow(LightningFlow):
...
def configure_layout(self):
return StreamlitFrontend(render_fn=my_streamlit_ui)
def my_streamlit_ui(state):
# add your streamlit code here!
import streamlit as st
st.button("Hello!")
**Example:** Arrange the UI of my children in tabs (default UI by Lightning).
.. code-block::
.. code-block:: python
class Flow(LightningFlow):
...
def configure_layout(self):
return [
dict(name="First Tab", content=self.child0),
dict(name="Second Tab", content=self.child1),
...
# You can include direct URLs too
dict(name="Lightning", content="https://lightning.ai"),
]
@ -500,6 +534,27 @@ class LightningFlow:
returned layout configuration can depend on the state. The only exception are the flows that return a
:class:`~lightning_app.frontend.frontend.Frontend`. These need to be provided at the time of app creation
in order for the runtime to start the server.
**Learn more about adding UI**
.. raw:: html
<div class="display-card-container">
<div class="row">
.. displayitem::
:header: Add a web user interface (UI)
:description: Learn more how to integrate several UIs.
:col_css: col-md-4
:button_link: ../../../workflows/add_web_ui/index.html
:height: 180
:tag: Basic
.. raw:: html
</div>
</div>
<br />
"""
return [dict(name=name, content=component) for (name, component) in self.flows.items()]

View File

@ -63,7 +63,31 @@ class LightningWork(abc.ABC):
with the same arguments in subsequent calls.
raise_exception: Whether to re-raise an exception in the flow when raised from within the work run method.
host: Bind socket to this host
port: Bind socket to this port
port: Bind socket to this port. Be default, this is None and should be called within your run method.
local_build_config: The local BuildConfig isn't used until Lightning supports DockerRuntime.
cloud_build_config: The cloud BuildConfig enables user to easily configure machine before running this work.
run_once: Deprecated in favor of cache_calls. This will be removed soon.
**Learn More About Lightning Work Inner Workings**
.. raw:: html
<div class="display-card-container">
<div class="row">
.. displayitem::
:header: The Lightning Work inner workings.
:description: Learn more Lightning Work.
:col_css: col-md-4
:button_link: ../../../core_api/lightning_work/index.html
:height: 180
:tag: Basic
.. raw:: html
</div>
</div>
<br />
"""
from lightning_app.runners.backends.backend import Backend
@ -98,6 +122,7 @@ class LightningWork(abc.ABC):
@property
def url(self) -> str:
"""Returns the current url of the work."""
return self._url
@url.setter
@ -106,6 +131,7 @@ class LightningWork(abc.ABC):
@property
def host(self) -> str:
"""Returns the current host of the work."""
return self._host
@property
@ -166,6 +192,7 @@ class LightningWork(abc.ABC):
@property
def cloud_build_config(self) -> BuildConfig:
"""Returns the cloud build config used to prepare the selected cloud hardware."""
return self._cloud_build_config
@cloud_build_config.setter
@ -179,6 +206,7 @@ class LightningWork(abc.ABC):
@cloud_compute.setter
def cloud_compute(self, cloud_compute) -> None:
"""Returns the cloud compute used to select the cloud hardware."""
self._cloud_compute = cloud_compute
@property

View File

@ -23,7 +23,7 @@ def call_script(
if args is None:
args = []
args = [str(a) for a in args]
command = [sys.executable, "-m", "coverage", "run", filepath] + args
command = [sys.executable, filepath] + args # todo: add back coverage
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
stdout, stderr = p.communicate(timeout=timeout)

View File

@ -81,7 +81,7 @@ def application_testing(lightning_app_cls: Type[LightningTestApp], command_line:
from click.testing import CliRunner
with mock.patch("lightning_app.LightningApp", lightning_app_cls):
with mock.patch("lightning.LightningApp", lightning_app_cls):
runner = CliRunner()
return runner.invoke(run_app, command_line, catch_exceptions=False)
@ -208,22 +208,14 @@ def run_app_in_cloud(app_folder: str, app_name: str = "app.py") -> Generator:
""",
[LIGHTNING_CLOUD_PROJECT_ID],
)
admin_page.reload()
admin_page.goto(f"{Config.url}/{Config.username}/apps")
try:
# Closing the Create Project modal
button = admin_page.locator('button:has-text("Cancel")')
button.wait_for(timeout=1 * 1000)
button.wait_for(timeout=3 * 1000)
button.click()
except (playwright._impl._api_types.Error, playwright._impl._api_types.TimeoutError):
pass
try:
# Skipping the Hubspot form
button = admin_page.locator('button:has-text("Skip for now")')
button.wait_for(timeout=1 * 1000)
button.click()
except (playwright._impl._api_types.Error, playwright._impl._api_types.TimeoutError):
pass
admin_page.goto(f"{Config.url}/{Config.username}/apps")
admin_page.locator(f"text={name}").click()
admin_page.evaluate(
"""data => {

View File

@ -232,7 +232,7 @@ class StreamLitStatePlugin(BaseStatePlugin):
# Adapted from
# https://github.com/Lightning-AI/lightning/blob/master/pytorch_lightning/utilities/model_helpers.py#L21
# https://github.com/Lightning-AI/pytorch-lightning/blob/master/pytorch_lightning/utilities/model_helpers.py#L21
def is_overridden(method_name: str, instance: Optional[object] = None, parent: Optional[Type[object]] = None) -> bool:
if instance is None:
return False

View File

@ -48,7 +48,7 @@ def _configure_session() -> Session:
return http
def _check_service_url_is_ready(url: str, timeout: float = 0.1) -> bool:
def _check_service_url_is_ready(url: str, timeout: float = 0.5) -> bool:
try:
response = requests.get(url, timeout=timeout)
return response.status_code in (200, 404)

View File

@ -1,12 +1,11 @@
import inspect
import logging
import os
import re
from dataclasses import asdict, dataclass
from types import FrameType
from typing import cast, List, Optional, TYPE_CHECKING, Union
from pkg_resources import parse_requirements
if TYPE_CHECKING:
from lightning_app import LightningWork
from lightning_app.utilities.packaging.cloud_compute import CloudCompute
@ -15,6 +14,37 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def load_requirements(
path_dir: str, file_name: str = "base.txt", comment_char: str = "#", unfreeze: bool = True
) -> List[str]:
"""Load requirements from a file.
.. code-block:: python
path_req = os.path.join(_PROJECT_ROOT, "requirements")
requirements = load_requirements(path_req)
print(requirements) # ['numpy...', 'torch...', ...]
"""
with open(os.path.join(path_dir, file_name)) as file:
lines = [ln.strip() for ln in file.readlines()]
reqs = []
for ln in lines:
# filer all comments
comment = ""
if comment_char in ln:
comment = ln[ln.index(comment_char) :]
ln = ln[: ln.index(comment_char)]
req = ln.strip()
# skip directly installed dependencies
if not req or req.startswith("http") or "@http" in req:
continue
# remove version restrictions unless they are strict
if unfreeze and "<" in req and "strict" not in comment:
req = re.sub(r",? *<=? *[\d\.\*]+", "", req).strip()
reqs.append(req)
return reqs
@dataclass
class BuildConfig:
"""The Build Configuration describes how the environment a LightningWork runs in should be set up.
@ -61,7 +91,7 @@ class BuildConfig:
class MyOwnBuildConfig(BuildConfig):
def build_commands(self):
return ["sudo apt-get install libsparsehash-dev"]
return ["apt-get install libsparsehash-dev"]
BuildConfig(requirements=["git+https://github.com/mit-han-lab/torchsparse.git@v1.4.0"])
"""
@ -84,8 +114,9 @@ class BuildConfig:
requirement_files = [os.path.join(dirname, f) for f in os.listdir(dirname) if f == "requirements.txt"]
if not requirement_files:
return []
dirname, basename = os.path.dirname(requirement_files[0]), os.path.basename(requirement_files[0])
try:
requirements = list(map(str, parse_requirements(open(requirement_files[0]).readlines())))
requirements = load_requirements(dirname, basename)
except NotADirectoryError:
requirements = []
return [r for r in requirements if r != "lightning"]
@ -116,7 +147,9 @@ class BuildConfig:
path = os.path.join(self._call_dir, req)
if os.path.exists(path):
try:
requirements.extend(list(map(str, parse_requirements(open(path).readlines()))))
requirements.extend(
load_requirements(os.path.dirname(path), os.path.basename(path)),
)
except NotADirectoryError:
pass
else:

View File

@ -32,8 +32,8 @@ class CloudCompute:
This timeout starts whenever your run() method succeeds (or fails).
If the timeout is reached, the instance pauses until the next run() call happens.
shm_size: Shared memory size in MiB, backed by RAM. min 512, max 4096, it will auto update in steps of 512.
For example 1100 will become 1024. If set to zero (the default) will get the default 65MB inside docker.
shm_size: Shared memory size in MiB, backed by RAM. min 512, max 8192, it will auto update in steps of 512.
For example 1100 will become 1024. If set to zero (the default) will get the default 64MiB inside docker.
"""
name: str = "default"

View File

@ -14,7 +14,7 @@ from typing import Any, Callable, Optional
from packaging.version import Version
from lightning_app import _logger, _PROJECT_ROOT, _root_logger
from lightning_app.__about__ import __version__
from lightning_app.__version__ import version
from lightning_app.core.constants import PREPARE_LIGHTING
from lightning_app.utilities.git import check_github_repository, get_dir_name
@ -29,7 +29,7 @@ def download_frontend(root):
"""Downloads an archive file for a specific release of the Lightning frontend and extracts it to the correct
directory."""
build_dir = "build"
frontend_dir = pathlib.Path(root, "lightning_app", "ui")
frontend_dir = pathlib.Path(root, "src", "lightning_app", "ui")
download_dir = tempfile.mkdtemp()
shutil.rmtree(frontend_dir, ignore_errors=True)
@ -43,41 +43,51 @@ def download_frontend(root):
print("The Lightning UI has successfully been downloaded!")
def _cleanup(tar_file: str):
shutil.rmtree(os.path.join(_PROJECT_ROOT, "dist"), ignore_errors=True)
os.remove(tar_file)
def _cleanup(*tar_files: str):
for tar_file in tar_files:
shutil.rmtree(os.path.join(_PROJECT_ROOT, "dist"), ignore_errors=True)
os.remove(tar_file)
def _prepare_lightning_wheels():
def _prepare_wheel(path):
with open("log.txt", "w") as logfile:
with subprocess.Popen(
["rm", "-r", "dist"], stdout=logfile, stderr=logfile, bufsize=0, close_fds=True, cwd=_PROJECT_ROOT
["rm", "-r", "dist"], stdout=logfile, stderr=logfile, bufsize=0, close_fds=True, cwd=path
) as proc:
proc.wait()
with subprocess.Popen(
["python", "setup.py", "sdist"],
stdout=logfile,
stderr=logfile,
bufsize=0,
close_fds=True,
cwd=_PROJECT_ROOT,
cwd=path,
) as proc:
proc.wait()
os.remove("log.txt")
def _copy_lightning_tar(root: Path) -> str:
dist_dir = os.path.join(_PROJECT_ROOT, "dist")
def _copy_tar(project_root, dest: Path) -> str:
dist_dir = os.path.join(project_root, "dist")
tar_files = os.listdir(dist_dir)
assert len(tar_files) == 1
tar_name = tar_files[0]
tar_path = os.path.join(dist_dir, tar_name)
shutil.copy(tar_path, root)
shutil.copy(tar_path, dest)
return tar_name
def get_dist_path_if_editable_install(project_name) -> str:
"""Is distribution an editable install - modified version from pip that
fetches egg-info instead of egg-link"""
for path_item in sys.path:
egg_info = os.path.join(path_item, project_name + ".egg-info")
if os.path.isdir(egg_info):
return path_item
return ""
def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable]:
if "site-packages" in _PROJECT_ROOT:
@ -88,20 +98,37 @@ def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable]
if not PREPARE_LIGHTING and (not git_dir_name or (git_dir_name and not git_dir_name.startswith("lightning"))):
return
if not bool(int(os.getenv("SKIP_LIGHTING_WHEELS_BUILD", "0"))):
download_frontend(_PROJECT_ROOT)
_prepare_lightning_wheels()
_prepare_wheel(_PROJECT_ROOT)
logger.info("Packaged Lightning with your application.")
tar_name = _copy_lightning_tar(root)
tar_name = _copy_tar(_PROJECT_ROOT, root)
return functools.partial(_cleanup, tar_file=os.path.join(root, tar_name))
tar_files = [os.path.join(root, tar_name)]
# skipping this by default
if not bool(int(os.getenv("SKIP_LIGHTING_UTILITY_WHEELS_BUILD", "1"))):
# building and copying launcher wheel if installed in editable mode
launcher_project_path = get_dist_path_if_editable_install("lightning_launcher")
if launcher_project_path:
_prepare_wheel(launcher_project_path)
tar_name = _copy_tar(launcher_project_path, root)
tar_files.append(os.path.join(root, tar_name))
# building and copying lightning-cloud wheel if installed in editable mode
lightning_cloud_project_path = get_dist_path_if_editable_install("lightning_cloud")
if lightning_cloud_project_path:
_prepare_wheel(lightning_cloud_project_path)
tar_name = _copy_tar(lightning_cloud_project_path, root)
tar_files.append(os.path.join(root, tar_name))
return functools.partial(_cleanup, *tar_files)
def _enable_debugging():
tar_file = os.path.join(os.getcwd(), f"lightning-{__version__}.tar.gz")
tar_file = os.path.join(os.getcwd(), f"lightning-{version}.tar.gz")
if not os.path.exists(tar_file):
return
@ -138,7 +165,7 @@ def _fetch_latest_version(package_name: str) -> str:
if proc.stdout:
logs = " ".join([line.decode("utf-8") for line in iter(proc.stdout.readline, b"")])
return logs.split(")\n")[0].split(",")[-1].replace(" ", "")
return __version__
return version
def _verify_lightning_version():
@ -149,7 +176,7 @@ def _verify_lightning_version():
lightning_latest_version = _fetch_latest_version("lightning")
if Version(lightning_latest_version) > Version(__version__):
if Version(lightning_latest_version) > Version(version):
raise Exception(
f"You need to use the latest version of Lightning ({lightning_latest_version}) to run in the cloud. "
"Please, run `pip install -U lightning`"

View File

@ -56,4 +56,4 @@ def test_copy_and_setup_react_ui(tmpdir):
def test_correct_num_react_template_files():
template_dir = os.path.join(la.__path__[0], "cli/react-ui-template")
files = cmd_init._ls_recursively(template_dir)
assert len(files) == 15, "react-ui template files must be minimal... do not add nice to haves"
assert len(files) == 16, "react-ui template files must be minimal... do not add nice to haves"

View File

@ -2,7 +2,7 @@ import os
import pytest
from lightning import __about__
from lightning.__version__ import version
from lightning_app.testing.helpers import RunIf
from lightning_app.utilities.packaging import lightning_utils
from lightning_app.utilities.packaging.lightning_utils import (
@ -15,7 +15,7 @@ def test_prepare_lightning_wheels_and_requirement(tmpdir):
"""This test ensures the lightning source gets packaged inside the lightning repo."""
cleanup_handle = _prepare_lightning_wheels_and_requirements(tmpdir)
tar_name = f"lightning-{__about__.__version__}.tar.gz"
tar_name = f"lightning-{version}.tar.gz"
assert sorted(os.listdir(tmpdir)) == [tar_name]
cleanup_handle()
assert os.listdir(tmpdir) == []

View File

@ -15,7 +15,7 @@ def test_execute_git_command():
res = execute_git_command(["pull"])
assert res
assert get_dir_name() == "lightning-app"
assert get_dir_name() == "lightning"
assert check_github_repository()