From 46ce6fa552d36f5e603d0b5acbfaaa2735c3abcc Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 12:26:26 -0500 Subject: [PATCH 01/16] update --- src/lightning_app/core/queues.py | 18 +++++++++++++++--- src/lightning_app/core/work.py | 1 + src/lightning_app/runners/backends/backend.py | 17 +++++++++-------- .../runners/backends/mp_process.py | 4 +++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index a7fee9a3b6e12..9bccf6aebc3d6 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -6,8 +6,8 @@ from abc import ABC, abstractmethod from enum import Enum from pathlib import Path -from typing import Any, Optional - +from typing import Any, Optional, Generator +from contextlib import contextmanager from lightning_app.core.constants import ( HTTP_QUEUE_REFRESH_INTERVAL, HTTP_QUEUE_TOKEN, @@ -30,6 +30,17 @@ logger = Logger(__name__) +_START_METHOD = "fork" + +@contextmanager +def start_method_context(work: str) -> Generator: + global _START_METHOD + v = _START_METHOD + _START_METHOD = getattr(work, "_start_method", "fork") + yield + _START_METHOD = v + + READINESS_QUEUE_CONSTANT = "READINESS_QUEUE" ERROR_QUEUE_CONSTANT = "ERROR_QUEUE" @@ -198,7 +209,8 @@ class MultiProcessQueue(BaseQueue): def __init__(self, name: str, default_timeout: float): self.name = name self.default_timeout = default_timeout - self.queue = multiprocessing.Queue() + context = multiprocessing.get_context(_START_METHOD) + self.queue = context.Queue() def put(self, item): self.queue.put(item) diff --git a/src/lightning_app/core/work.py b/src/lightning_app/core/work.py index ab0dc8426ac91..d478846f730c7 100644 --- a/src/lightning_app/core/work.py +++ b/src/lightning_app/core/work.py @@ -46,6 +46,7 @@ class LightningWork: ) _run_executor_cls: Type[WorkRunExecutor] = WorkRunExecutor + _start_method = "fork" def __init__( self, diff --git a/src/lightning_app/runners/backends/backend.py b/src/lightning_app/runners/backends/backend.py index 54c1f9092bf0f..aa6635dd20e8c 100644 --- a/src/lightning_app/runners/backends/backend.py +++ b/src/lightning_app/runners/backends/backend.py @@ -2,7 +2,7 @@ from functools import partial from typing import Any, Callable, List, Optional, TYPE_CHECKING -from lightning_app.core.queues import QueuingSystem +from lightning_app.core.queues import QueuingSystem, start_method_context from lightning_app.utilities.proxies import ProxyWorkRun, unwrap if TYPE_CHECKING: @@ -97,13 +97,14 @@ def _prepare_queues(self, app: "lightning_app.LightningApp"): app.flow_to_work_delta_queues = {} def _register_queues(self, app, work): - kw = dict(queue_id=self.queue_id, work_name=work.name) - app.request_queues.update({work.name: self.queues.get_orchestrator_request_queue(**kw)}) - app.response_queues.update({work.name: self.queues.get_orchestrator_response_queue(**kw)}) - app.copy_request_queues.update({work.name: self.queues.get_orchestrator_copy_request_queue(**kw)}) - app.copy_response_queues.update({work.name: self.queues.get_orchestrator_copy_response_queue(**kw)}) - app.caller_queues.update({work.name: self.queues.get_caller_queue(**kw)}) - app.flow_to_work_delta_queues.update({work.name: self.queues.get_flow_to_work_delta_queue(**kw)}) + with start_method_context(work): + kw = dict(queue_id=self.queue_id, work_name=work.name) + app.request_queues.update({work.name: self.queues.get_orchestrator_request_queue(**kw)}) + app.response_queues.update({work.name: self.queues.get_orchestrator_response_queue(**kw)}) + app.copy_request_queues.update({work.name: self.queues.get_orchestrator_copy_request_queue(**kw)}) + app.copy_response_queues.update({work.name: self.queues.get_orchestrator_copy_response_queue(**kw)}) + app.caller_queues.update({work.name: self.queues.get_caller_queue(**kw)}) + app.flow_to_work_delta_queues.update({work.name: self.queues.get_flow_to_work_delta_queue(**kw)}) class WorkManager(ABC): diff --git a/src/lightning_app/runners/backends/mp_process.py b/src/lightning_app/runners/backends/mp_process.py index 36a067d0bfd80..0ecf86bc09a02 100644 --- a/src/lightning_app/runners/backends/mp_process.py +++ b/src/lightning_app/runners/backends/mp_process.py @@ -31,7 +31,9 @@ def start(self): flow_to_work_delta_queue=self.app.flow_to_work_delta_queues[self.work.name], run_executor_cls=self.work._run_executor_cls, ) - self._process = multiprocessing.Process(target=self._work_runner) + + context = multiprocessing.get_context(getattr(self.work, "_start_method", "fork")) + self._process = context.Process(target=self._work_runner) self._process.start() def kill(self): From ec43b45d00bdeaf54d1a7d7a57481ec06624ec61 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 12:29:50 -0500 Subject: [PATCH 02/16] update --- src/lightning_app/core/queues.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index 9bccf6aebc3d6..e3aa8b964e1dd 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -33,7 +33,8 @@ _START_METHOD = "fork" @contextmanager -def start_method_context(work: str) -> Generator: +def start_method_context(work) -> Generator: + """Context to switch the start method.""" global _START_METHOD v = _START_METHOD _START_METHOD = getattr(work, "_start_method", "fork") From 9d017edb6d9586d7a199d135d9acab5822466102 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 17:30:44 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lightning_app/core/queues.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index e3aa8b964e1dd..9e85e985a82c1 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -4,10 +4,11 @@ import time import warnings from abc import ABC, abstractmethod +from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import Any, Optional, Generator -from contextlib import contextmanager +from typing import Any, Generator, Optional + from lightning_app.core.constants import ( HTTP_QUEUE_REFRESH_INTERVAL, HTTP_QUEUE_TOKEN, @@ -32,6 +33,7 @@ _START_METHOD = "fork" + @contextmanager def start_method_context(work) -> Generator: """Context to switch the start method.""" @@ -42,7 +44,6 @@ def start_method_context(work) -> Generator: _START_METHOD = v - READINESS_QUEUE_CONSTANT = "READINESS_QUEUE" ERROR_QUEUE_CONSTANT = "ERROR_QUEUE" DELTA_QUEUE_CONSTANT = "DELTA_QUEUE" From f27d34971ba611af80b50c6ed7c8542a6b4b8664 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:00:02 -0500 Subject: [PATCH 04/16] update --- setup.py | 4 ++-- src/lightning_app/core/queues.py | 15 +-------------- src/lightning_app/runners/backends/backend.py | 15 +++++++-------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index 7e16a4496d35b..026081fa99967 100755 --- a/setup.py +++ b/setup.py @@ -123,8 +123,8 @@ def _set_manifest_path(manifest_dir: str, aggregate: bool = False) -> Generator: print(f"Installing the {package_to_install} package") # requires `-v` to appear is_wheel_install = "PEP517_BUILD_BACKEND" in os.environ print("is_wheel_install:", is_wheel_install) - if package_to_install not in _PACKAGE_MAPPING or (not is_wheel_install and _PACKAGE_NAME is None): - raise ValueError(f"Unexpected package name: {_PACKAGE_NAME}. Possible choices are: {list(_PACKAGE_MAPPING)}") + # if package_to_install not in _PACKAGE_MAPPING or (not is_wheel_install and _PACKAGE_NAME is None): + # raise ValueError(f"Unexpected package name: {_PACKAGE_NAME}. Possible choices are: {list(_PACKAGE_MAPPING)}") is_wheel_install &= _PACKAGE_NAME is None if package_to_install == "lightning": # install everything diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index e3aa8b964e1dd..82090b3508546 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -30,19 +30,6 @@ logger = Logger(__name__) -_START_METHOD = "fork" - -@contextmanager -def start_method_context(work) -> Generator: - """Context to switch the start method.""" - global _START_METHOD - v = _START_METHOD - _START_METHOD = getattr(work, "_start_method", "fork") - yield - _START_METHOD = v - - - READINESS_QUEUE_CONSTANT = "READINESS_QUEUE" ERROR_QUEUE_CONSTANT = "ERROR_QUEUE" DELTA_QUEUE_CONSTANT = "DELTA_QUEUE" @@ -210,7 +197,7 @@ class MultiProcessQueue(BaseQueue): def __init__(self, name: str, default_timeout: float): self.name = name self.default_timeout = default_timeout - context = multiprocessing.get_context(_START_METHOD) + context = multiprocessing.get_context("spawn") self.queue = context.Queue() def put(self, item): diff --git a/src/lightning_app/runners/backends/backend.py b/src/lightning_app/runners/backends/backend.py index aa6635dd20e8c..e75958324f139 100644 --- a/src/lightning_app/runners/backends/backend.py +++ b/src/lightning_app/runners/backends/backend.py @@ -97,14 +97,13 @@ def _prepare_queues(self, app: "lightning_app.LightningApp"): app.flow_to_work_delta_queues = {} def _register_queues(self, app, work): - with start_method_context(work): - kw = dict(queue_id=self.queue_id, work_name=work.name) - app.request_queues.update({work.name: self.queues.get_orchestrator_request_queue(**kw)}) - app.response_queues.update({work.name: self.queues.get_orchestrator_response_queue(**kw)}) - app.copy_request_queues.update({work.name: self.queues.get_orchestrator_copy_request_queue(**kw)}) - app.copy_response_queues.update({work.name: self.queues.get_orchestrator_copy_response_queue(**kw)}) - app.caller_queues.update({work.name: self.queues.get_caller_queue(**kw)}) - app.flow_to_work_delta_queues.update({work.name: self.queues.get_flow_to_work_delta_queue(**kw)}) + kw = dict(queue_id=self.queue_id, work_name=work.name) + app.request_queues.update({work.name: self.queues.get_orchestrator_request_queue(**kw)}) + app.response_queues.update({work.name: self.queues.get_orchestrator_response_queue(**kw)}) + app.copy_request_queues.update({work.name: self.queues.get_orchestrator_copy_request_queue(**kw)}) + app.copy_response_queues.update({work.name: self.queues.get_orchestrator_copy_response_queue(**kw)}) + app.caller_queues.update({work.name: self.queues.get_caller_queue(**kw)}) + app.flow_to_work_delta_queues.update({work.name: self.queues.get_flow_to_work_delta_queue(**kw)}) class WorkManager(ABC): From e09211bb025861997ea860efaa97aad10cafe935 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:03:43 -0500 Subject: [PATCH 05/16] update --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 026081fa99967..7e16a4496d35b 100755 --- a/setup.py +++ b/setup.py @@ -123,8 +123,8 @@ def _set_manifest_path(manifest_dir: str, aggregate: bool = False) -> Generator: print(f"Installing the {package_to_install} package") # requires `-v` to appear is_wheel_install = "PEP517_BUILD_BACKEND" in os.environ print("is_wheel_install:", is_wheel_install) - # if package_to_install not in _PACKAGE_MAPPING or (not is_wheel_install and _PACKAGE_NAME is None): - # raise ValueError(f"Unexpected package name: {_PACKAGE_NAME}. Possible choices are: {list(_PACKAGE_MAPPING)}") + if package_to_install not in _PACKAGE_MAPPING or (not is_wheel_install and _PACKAGE_NAME is None): + raise ValueError(f"Unexpected package name: {_PACKAGE_NAME}. Possible choices are: {list(_PACKAGE_MAPPING)}") is_wheel_install &= _PACKAGE_NAME is None if package_to_install == "lightning": # install everything From 54b8f002eea7cdc4a85ac1ea1af0dd901d6c0e40 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:17:54 -0500 Subject: [PATCH 06/16] update --- src/lightning_app/runners/backends/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/runners/backends/backend.py b/src/lightning_app/runners/backends/backend.py index e75958324f139..54c1f9092bf0f 100644 --- a/src/lightning_app/runners/backends/backend.py +++ b/src/lightning_app/runners/backends/backend.py @@ -2,7 +2,7 @@ from functools import partial from typing import Any, Callable, List, Optional, TYPE_CHECKING -from lightning_app.core.queues import QueuingSystem, start_method_context +from lightning_app.core.queues import QueuingSystem from lightning_app.utilities.proxies import ProxyWorkRun, unwrap if TYPE_CHECKING: From 6ae3ece1e471ced4a673cd6d6f907b6ceff982d9 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:19:14 -0500 Subject: [PATCH 07/16] update --- src/lightning_app/core/queues.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index 5ba0c98d01d0d..a7fee9a3b6e12 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -4,10 +4,9 @@ import time import warnings from abc import ABC, abstractmethod -from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import Any, Generator, Optional +from typing import Any, Optional from lightning_app.core.constants import ( HTTP_QUEUE_REFRESH_INTERVAL, @@ -31,6 +30,7 @@ logger = Logger(__name__) + READINESS_QUEUE_CONSTANT = "READINESS_QUEUE" ERROR_QUEUE_CONSTANT = "ERROR_QUEUE" DELTA_QUEUE_CONSTANT = "DELTA_QUEUE" @@ -198,8 +198,7 @@ class MultiProcessQueue(BaseQueue): def __init__(self, name: str, default_timeout: float): self.name = name self.default_timeout = default_timeout - context = multiprocessing.get_context("spawn") - self.queue = context.Queue() + self.queue = multiprocessing.Queue() def put(self, item): self.queue.put(item) From b3581e48332128897bada9ee422009d4e3b6ecde Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:20:45 -0500 Subject: [PATCH 08/16] update --- src/lightning_app/core/queues.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index a7fee9a3b6e12..f38942915abc3 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -198,7 +198,8 @@ class MultiProcessQueue(BaseQueue): def __init__(self, name: str, default_timeout: float): self.name = name self.default_timeout = default_timeout - self.queue = multiprocessing.Queue() + context = multiprocessing.get_context("spawn") + self.queue = context.Queue() def put(self, item): self.queue.put(item) From 792459b7b9c5a9773a77a6e8c6825c6a11994f55 Mon Sep 17 00:00:00 2001 From: Thomas Chaton Date: Tue, 6 Dec 2022 13:21:20 -0500 Subject: [PATCH 09/16] update --- src/lightning_app/runners/backends/mp_process.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/runners/backends/mp_process.py b/src/lightning_app/runners/backends/mp_process.py index 0ecf86bc09a02..d8f6d36c3a400 100644 --- a/src/lightning_app/runners/backends/mp_process.py +++ b/src/lightning_app/runners/backends/mp_process.py @@ -32,7 +32,8 @@ def start(self): run_executor_cls=self.work._run_executor_cls, ) - context = multiprocessing.get_context(getattr(self.work, "_start_method", "fork")) + start_method = getattr(self.work, "_start_method", "fork") + context = multiprocessing.get_context(start_method) self._process = context.Process(target=self._work_runner) self._process.start() From 821a37e93f3bf31f92c3e000f984a8ca29c72edc Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 18:25:00 +0000 Subject: [PATCH 10/16] update --- src/lightning_app/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md index ec7812e6e7417..f7cd8be3db982 100644 --- a/src/lightning_app/CHANGELOG.md +++ b/src/lightning_app/CHANGELOG.md @@ -19,6 +19,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added the property `ready` of the LightningFlow to inform when the `Open App` should be visible ([#15921](https://github.com/Lightning-AI/lightning/pull/15921)) +- Added private work attributed `_start_method` to customize how to start the works ([#15923](https://github.com/Lightning-AI/lightning/pull/15923)) + ### Changed From 6cc9fc9e9354b8d853182b68bddc9df0dc839a72 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 19:08:02 +0000 Subject: [PATCH 11/16] Update src/lightning_app/runners/backends/mp_process.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Wälchli --- src/lightning_app/runners/backends/mp_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/runners/backends/mp_process.py b/src/lightning_app/runners/backends/mp_process.py index d8f6d36c3a400..dc0681390046e 100644 --- a/src/lightning_app/runners/backends/mp_process.py +++ b/src/lightning_app/runners/backends/mp_process.py @@ -32,7 +32,7 @@ def start(self): run_executor_cls=self.work._run_executor_cls, ) - start_method = getattr(self.work, "_start_method", "fork") + start_method = self.work._start_method context = multiprocessing.get_context(start_method) self._process = context.Process(target=self._work_runner) self._process.start() From 4424d02f3d843377e300eb58602decaa8e27da59 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 19:27:22 +0000 Subject: [PATCH 12/16] update --- tests/tests_app/core/test_queues.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/tests_app/core/test_queues.py b/tests/tests_app/core/test_queues.py index 899ad9f606e85..0f930bcacabc7 100644 --- a/tests/tests_app/core/test_queues.py +++ b/tests/tests_app/core/test_queues.py @@ -5,7 +5,6 @@ from unittest import mock import pytest -import redis import requests_mock from lightning_app import LightningFlow @@ -23,6 +22,7 @@ def test_queue_api(queue_type, monkeypatch): This test run all the Queue implementation but we monkeypatch the Redis Queues to avoid external interaction """ + import redis blpop_out = (b"entry-id", pickle.dumps("test_entry")) @@ -104,12 +104,14 @@ def test_redis_queue_read_timeout(redis_mock): @pytest.mark.parametrize( "queue_type, queue_process_mock", - [(QueuingSystem.SINGLEPROCESS, queue), (QueuingSystem.MULTIPROCESS, multiprocessing)], + [(QueuingSystem.MULTIPROCESS, multiprocessing)], ) def test_process_queue_read_timeout(queue_type, queue_process_mock, monkeypatch): + context = mock.MagicMock() queue_mocked = mock.MagicMock() - monkeypatch.setattr(queue_process_mock, "Queue", queue_mocked) + context.Queue = queue_mocked + monkeypatch.setattr(queue_process_mock, "get_context", mock.MagicMock(return_value=context)) my_queue = queue_type.get_readiness_queue() # default timeout From f33166c626fd8058e9ad381925274daf118214a5 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 19:36:49 +0000 Subject: [PATCH 13/16] update --- src/lightning_app/core/flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index 72527bf7aee6f..f30cc8f579920 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -763,6 +763,10 @@ def __init__(self, work): super().__init__() self.work = work + @property + def ready(self) -> bool: + return self.work.url != "" + def run(self): if self.work.has_succeeded: self.work.stop() From c29a647af81059eaeacad41963dafcce73895107 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 19:49:29 +0000 Subject: [PATCH 14/16] update --- src/lightning_app/core/work.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/core/work.py b/src/lightning_app/core/work.py index d478846f730c7..857cbc9447ff1 100644 --- a/src/lightning_app/core/work.py +++ b/src/lightning_app/core/work.py @@ -1,3 +1,4 @@ +import sys import time import warnings from copy import deepcopy @@ -46,7 +47,8 @@ class LightningWork: ) _run_executor_cls: Type[WorkRunExecutor] = WorkRunExecutor - _start_method = "fork" + # TODO: Move to spawn for all Operating System. + _start_method = "spawn" if sys.platform == "win32" else "fork" def __init__( self, From e683fe3f7a647970e0915c0142ad5243379f4b38 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 20:34:45 +0000 Subject: [PATCH 15/16] update --- src/lightning_app/core/flow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index f30cc8f579920..56947b0d2cbef 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -765,6 +765,9 @@ def __init__(self, work): @property def ready(self) -> bool: + ready = getattr(self.work, "ready", None) + if ready: + return ready return self.work.url != "" def run(self): From 1dca92928d2eac40c9504aa953631bca66b65405 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 6 Dec 2022 20:40:09 +0000 Subject: [PATCH 16/16] update --- examples/app_installation_commands/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/app_installation_commands/app.py b/examples/app_installation_commands/app.py index 9eb1c2944ee2e..087d84b1335b2 100644 --- a/examples/app_installation_commands/app.py +++ b/examples/app_installation_commands/app.py @@ -13,6 +13,10 @@ def run(self): print("lmdb successfully installed") print("accessing a module in a Work or Flow body works!") + @property + def ready(self) -> bool: + return True + print(f"accessing an object in main code body works!: version={lmdb.version()}")