44
55import lightning_app
66from lightning_app .frontend .frontend import Frontend
7- from lightning_app .utilities .app_helpers import _MagicMockJsonSerializable
7+ from lightning_app .utilities .app_helpers import _MagicMockJsonSerializable , is_overridden
88from lightning_app .utilities .cloud import is_running_in_cloud
99
1010
@@ -45,9 +45,9 @@ def _collect_layout(app: "lightning_app.LightningApp", flow: "lightning_app.Ligh
4545 app .frontends .setdefault (flow .name , "mock" )
4646 return flow ._layout
4747 elif isinstance (layout , dict ):
48- layout = _collect_content_layout ([layout ], flow )
48+ layout = _collect_content_layout ([layout ], app , flow )
4949 elif isinstance (layout , (list , tuple )) and all (isinstance (item , dict ) for item in layout ):
50- layout = _collect_content_layout (layout , flow )
50+ layout = _collect_content_layout (layout , app , flow )
5151 else :
5252 lines = _add_comment_to_literal_code (flow .configure_layout , contains = "return" , comment = " <------- this guy" )
5353 m = f"""
@@ -76,7 +76,9 @@ def configure_layout(self):
7676 return layout
7777
7878
79- def _collect_content_layout (layout : List [Dict ], flow : "lightning_app.LightningFlow" ) -> List [Dict ]:
79+ def _collect_content_layout (
80+ layout : List [Dict ], app : "lightning_app.LightningApp" , flow : "lightning_app.LightningFlow"
81+ ) -> Union [List [Dict ], Dict ]:
8082 """Process the layout returned by the ``configure_layout()`` method if the returned format represents an
8183 aggregation of child layouts."""
8284 for entry in layout :
@@ -102,12 +104,43 @@ def _collect_content_layout(layout: List[Dict], flow: "lightning_app.LightningFl
102104 entry ["content" ] = entry ["content" ].name
103105
104106 elif isinstance (entry ["content" ], lightning_app .LightningWork ):
105- if entry [ "content" ]. url and not entry ["content" ]. url . startswith ( "/" ):
106- entry [ "content" ] = entry [ "content" ]. url
107- entry [ "target" ] = entry [ "content" ]
108- else :
107+ work = entry ["content" ]
108+ work_layout = _collect_work_layout ( work )
109+
110+ if work_layout is None :
109111 entry ["content" ] = ""
110- entry ["target" ] = ""
112+ elif isinstance (work_layout , str ):
113+ entry ["content" ] = work_layout
114+ entry ["target" ] = work_layout
115+ elif isinstance (work_layout , (Frontend , _MagicMockJsonSerializable )):
116+ if len (layout ) > 1 :
117+ lines = _add_comment_to_literal_code (
118+ flow .configure_layout , contains = "return" , comment = " <------- this guy"
119+ )
120+ m = f"""
121+ The return value of configure_layout() in `{ flow .__class__ .__name__ } ` is an
122+ unsupported format:
123+ \n { lines }
124+
125+ The tab containing a `{ work .__class__ .__name__ } ` must be the only tab in the
126+ layout of this flow.
127+
128+ (see the docs for `LightningWork.configure_layout`).
129+ """
130+ raise TypeError (m )
131+
132+ if isinstance (work_layout , Frontend ):
133+ # If the work returned a frontend, treat it as belonging to the flow.
134+ # NOTE: This could evolve in the future to run the Frontend directly in the work machine.
135+ frontend = work_layout
136+ frontend .flow = flow
137+ elif isinstance (work_layout , _MagicMockJsonSerializable ):
138+ # The import was mocked, we set a dummy `Frontend` so that `is_headless` knows there is a UI.
139+ frontend = "mock"
140+
141+ app .frontends .setdefault (flow .name , frontend )
142+ return flow ._layout
143+
111144 elif isinstance (entry ["content" ], _MagicMockJsonSerializable ):
112145 # The import was mocked, we just record dummy content so that `is_headless` knows there is a UI
113146 entry ["content" ] = "mock"
@@ -126,3 +159,43 @@ def configure_layout(self):
126159 """
127160 raise ValueError (m )
128161 return layout
162+
163+
164+ def _collect_work_layout (work : "lightning_app.LightningWork" ) -> Union [None , str , Frontend , _MagicMockJsonSerializable ]:
165+ """Check if ``configure_layout`` is overridden on the given work and return the work layout (either a string, a
166+ ``Frontend`` object, or an instance of a mocked import).
167+
168+ Args:
169+ work: The work to collect the layout for.
170+
171+ Raises:
172+ TypeError: If the value returned by ``configure_layout`` is not of a supported format.
173+ """
174+ if is_overridden ("configure_layout" , work ):
175+ work_layout = work .configure_layout ()
176+ else :
177+ work_layout = work .url
178+
179+ if work_layout is None :
180+ return None
181+ elif isinstance (work_layout , str ):
182+ url = work_layout
183+ # The URL isn't fully defined yet. Looks something like ``self.work.url + /something``.
184+ if url and not url .startswith ("/" ):
185+ return url
186+ return ""
187+ elif isinstance (work_layout , (Frontend , _MagicMockJsonSerializable )):
188+ return work_layout
189+ else :
190+ m = f"""
191+ The value returned by `{ work .__class__ .__name__ } .configure_layout()` is of an unsupported type.
192+
193+ { repr (work_layout )}
194+
195+ Return a `Frontend` or a URL string, for example:
196+
197+ class { work .__class__ .__name__ } (LightningWork):
198+ def configure_layout(self):
199+ return MyFrontend() OR 'http://some/url'
200+ """
201+ raise TypeError (m )
0 commit comments