pytorch-lightning
215 строк · 8.7 Кб
1# Copyright The Lightning AI team.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import inspect16import warnings17from typing import Dict, List, Union18
19import lightning.app20from lightning.app.frontend.frontend import Frontend21from lightning.app.utilities.app_helpers import _MagicMockJsonSerializable, is_overridden22from lightning.app.utilities.cloud import is_running_in_cloud23
24
25def _add_comment_to_literal_code(method, contains, comment):26"""Inspects a method's code and adds a message to it.27
28This is a nice to have, so if it fails for some reason, it shouldn't affect the program.
29
30"""
31try:32lines = inspect.getsource(method)33lines = lines.split("\n")34idx_list = [i for i, x in enumerate(lines) if contains in x]35for i in idx_list:36line = lines[i]37line += comment38lines[i] = line39
40return "\n".join(lines)41
42except Exception:43return ""44
45
46def _collect_layout(app: "lightning.app.LightningApp", flow: "lightning.app.LightningFlow") -> Union[Dict, List[Dict]]:47"""Process the layout returned by the ``configure_layout()`` method in each flow."""48layout = flow.configure_layout()49
50if isinstance(layout, Frontend):51frontend = layout52frontend.flow = flow53app.frontends.setdefault(flow.name, frontend)54
55# When running locally, the target will get overwritten by the dispatcher when launching the frontend servers56# When running in the cloud, the frontend code will construct the URL based on the flow name57return flow._layout58if isinstance(layout, _MagicMockJsonSerializable):59# The import was mocked, we set a dummy `Frontend` so that `is_headless` knows there is a UI60app.frontends.setdefault(flow.name, "mock")61return flow._layout62if isinstance(layout, dict):63layout = _collect_content_layout([layout], app, flow)64elif isinstance(layout, (list, tuple)) and all(isinstance(item, dict) for item in layout):65layout = _collect_content_layout(layout, app, flow)66else:67lines = _add_comment_to_literal_code(flow.configure_layout, contains="return", comment=" <------- this guy")68raise TypeError(69f"""70The return value of configure_layout() in `{flow.__class__.__name__}` is an unsupported layout format:71\n{lines}72
73Return either an object of type {Frontend} (e.g., StreamlitFrontend, StaticWebFrontend):74def configure_layout(self):
75return la.frontend.Frontend(...)
76
77OR a single dict:
78def configure_layout(self):
79tab1 = {{'name': 'tab name', 'content': self.a_component}}
80return tab1
81
82OR a list of dicts:
83def configure_layout(self):
84tab1 = {{'name': 'tab name 1', 'content': self.component_a}}
85tab2 = {{'name': 'tab name 2', 'content': self.component_b}}
86return [tab1, tab2]
87
88(see the docs for `LightningFlow.configure_layout`).
89"""
90)91
92return layout93
94
95def _collect_content_layout(96layout: List[Dict], app: "lightning.app.LightningApp", flow: "lightning.app.LightningFlow"97) -> Union[List[Dict], Dict]:98"""Process the layout returned by the ``configure_layout()`` method if the returned format represents an99aggregation of child layouts."""
100for entry in layout:101if "content" not in entry:102raise ValueError(103f"A dictionary returned by `{flow.__class__.__name__}.configure_layout()` is missing a key 'content'."104f" For the value, choose either a reference to a child flow or a URla."105)106if isinstance(entry["content"], str): # assume this is a URL107url = entry["content"]108if url.startswith("/"):109# The URL isn't fully defined yet. Looks something like ``self.work.url + /something``.110entry["target"] = ""111else:112entry["target"] = url113if url.startswith("http://") and is_running_in_cloud():114warnings.warn(115f"You configured an http link {url[:32]}... but it won't be accessible in the cloud."116f" Consider replacing 'http' with 'https' in the link above."117)118
119elif isinstance(entry["content"], lightning.app.LightningFlow):120entry["content"] = entry["content"].name121
122elif isinstance(entry["content"], lightning.app.LightningWork):123work = entry["content"]124work_layout = _collect_work_layout(work)125
126if work_layout is None:127entry["content"] = ""128elif isinstance(work_layout, str):129entry["content"] = work_layout130entry["target"] = work_layout131elif isinstance(work_layout, (Frontend, _MagicMockJsonSerializable)):132if len(layout) > 1:133lines = _add_comment_to_literal_code(134flow.configure_layout, contains="return", comment=" <------- this guy"135)136m = f"""137The return value of configure_layout() in `{flow.__class__.__name__}` is an138unsupported format:
139\n{lines}140
141The tab containing a `{work.__class__.__name__}` must be the only tab in the142layout of this flow.
143
144(see the docs for `LightningWork.configure_layout`).
145"""
146raise TypeError(m)147
148if isinstance(work_layout, Frontend):149# If the work returned a frontend, treat it as belonging to the flow.150# NOTE: This could evolve in the future to run the Frontend directly in the work machine.151frontend = work_layout152frontend.flow = flow153elif isinstance(work_layout, _MagicMockJsonSerializable):154# The import was mocked, we set a dummy `Frontend` so that `is_headless` knows there is a UI.155frontend = "mock"156
157app.frontends.setdefault(flow.name, frontend)158return flow._layout159
160elif isinstance(entry["content"], _MagicMockJsonSerializable):161# The import was mocked, we just record dummy content so that `is_headless` knows there is a UI162entry["content"] = "mock"163entry["target"] = "mock"164else:165m = f"""166A dictionary returned by `{flow.__class__.__name__}.configure_layout()` contains an unsupported entry.167
168{{'content': {repr(entry['content'])}}}169
170Set the `content` key to a child flow or a URL, for example:
171
172class {flow.__class__.__name__}(LightningFlow):173def configure_layout(self):
174return {{'content': childFlow OR childWork OR 'http://some/url'}}
175"""
176raise ValueError(m)177return layout178
179
180def _collect_work_layout(work: "lightning.app.LightningWork") -> Union[None, str, Frontend, _MagicMockJsonSerializable]:181"""Check if ``configure_layout`` is overridden on the given work and return the work layout (either a string, a182``Frontend`` object, or an instance of a mocked import).
183
184Args:
185work: The work to collect the layout for.
186
187Raises:
188TypeError: If the value returned by ``configure_layout`` is not of a supported format.
189
190"""
191work_layout = work.configure_layout() if is_overridden("configure_layout", work) else work.url192
193if work_layout is None:194return None195if isinstance(work_layout, str):196url = work_layout197# The URL isn't fully defined yet. Looks something like ``self.work.url + /something``.198if url and not url.startswith("/"):199return url200return ""201if isinstance(work_layout, (Frontend, _MagicMockJsonSerializable)):202return work_layout203raise TypeError(204f"""205The value returned by `{work.__class__.__name__}.configure_layout()` is of an unsupported type.206
207{repr(work_layout)}208
209Return a `Frontend` or a URL string, for example:
210
211class {work.__class__.__name__}(LightningWork):212def configure_layout(self):
213return MyFrontend() OR 'http://some/url'
214"""
215)216