1
1
import inspect
2
2
import os
3
3
import re
4
- from dataclasses import asdict , dataclass
5
- from types import FrameType
6
- from typing import cast , List , Optional , TYPE_CHECKING , Union
4
+ from dataclasses import asdict , dataclass , field
5
+ from pathlib import Path
6
+ from typing import Dict , List , Optional , Union
7
7
8
+ from typing_extensions import Self
9
+
10
+ import lightning_app as L
8
11
from lightning_app .utilities .app_helpers import Logger
9
12
from lightning_app .utilities .packaging .cloud_compute import CloudCompute
10
13
11
- if TYPE_CHECKING :
12
- from lightning_app import LightningWork
13
-
14
14
logger = Logger (__name__ )
15
15
16
16
@@ -19,11 +19,10 @@ def load_requirements(
19
19
) -> List [str ]:
20
20
"""Load requirements from a file.
21
21
22
- .. code-block:: python
23
-
24
- path_req = os.path.join(_PROJECT_ROOT, "requirements")
25
- requirements = load_requirements(path_req)
26
- print(requirements) # ['numpy...', 'torch...', ...]
22
+ >>> from lightning_app import _PROJECT_ROOT
23
+ >>> path_req = os.path.join(_PROJECT_ROOT, "requirements")
24
+ >>> load_requirements(path_req, "docs.txt") # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +SKIP
25
+ ['sphinx>=4.0', ...] # TODO: remove SKIP, fails on python 3.7
27
26
"""
28
27
path = os .path .join (path_dir , file_name )
29
28
if not os .path .isfile (path ):
@@ -49,12 +48,19 @@ def load_requirements(
49
48
return reqs
50
49
51
50
51
+ @dataclass
52
+ class _Dockerfile :
53
+ path : str
54
+ data : List [str ]
55
+
56
+
52
57
@dataclass
53
58
class BuildConfig :
54
59
"""The Build Configuration describes how the environment a LightningWork runs in should be set up.
55
60
56
61
Arguments:
57
- requirements: List of requirements or paths to requirement files.
62
+ requirements: List of requirements or list of paths to requirement files. If not passed, they will be
63
+ automatically extracted from a `requirements.txt` if it exists.
58
64
dockerfile: The path to a dockerfile to be used to build your container.
59
65
You need to add those lines to ensure your container works in the cloud.
60
66
@@ -64,20 +70,19 @@ class BuildConfig:
64
70
65
71
WORKDIR /gridai/project
66
72
COPY . .
67
-
68
- Learn more by checking out:
69
- https://docs.grid.ai/features/runs/running-experiments-with-a-dockerfile
70
73
image: The base image that the work runs on. This should be a publicly accessible image from a registry that
71
74
doesn't enforce rate limits (such as DockerHub) to pull this image, otherwise your application will not
72
75
start.
73
76
"""
74
77
75
- requirements : Optional [ Union [ str , List [str ]]] = None
76
- dockerfile : Optional [str ] = None
78
+ requirements : List [str ] = field ( default_factory = list )
79
+ dockerfile : Optional [Union [ str , Path , _Dockerfile ] ] = None
77
80
image : Optional [str ] = None
78
81
79
- def __post_init__ (self ):
80
- self ._call_dir = os .path .dirname (cast (FrameType , inspect .currentframe ()).f_back .f_back .f_code .co_filename )
82
+ def __post_init__ (self ) -> None :
83
+ current_frame = inspect .currentframe ()
84
+ co_filename = current_frame .f_back .f_back .f_code .co_filename # type: ignore[union-attr]
85
+ self ._call_dir = os .path .dirname (co_filename )
81
86
self ._prepare_requirements ()
82
87
self ._prepare_dockerfile ()
83
88
@@ -101,76 +106,90 @@ def build_commands(self):
101
106
"""
102
107
return []
103
108
104
- def on_work_init (self , work , cloud_compute : Optional ["CloudCompute" ] = None ):
109
+ def on_work_init (self , work : "L.LightningWork" , cloud_compute : Optional ["CloudCompute" ] = None ) -> None :
105
110
"""Override with your own logic to load the requirements or dockerfile."""
106
- try :
107
- self .requirements = sorted (self .requirements or self ._find_requirements (work ) or [])
108
- self .dockerfile = self .dockerfile or self ._find_dockerfile (work )
109
- except TypeError :
110
- logger .debug ("The provided work couldn't be found." )
111
+ found_requirements = self ._find_requirements (work )
112
+ if self .requirements :
113
+ if found_requirements and self .requirements != found_requirements :
114
+ # notify the user of this silent behaviour
115
+ logger .info (
116
+ f"A 'requirements.txt' exists with { found_requirements } but { self .requirements } was passed to"
117
+ f" the `{ type (self ).__name__ } ` in { work .name !r} . The `requirements.txt` file will be ignored."
118
+ )
119
+ else :
120
+ self .requirements = found_requirements
121
+ self ._prepare_requirements ()
111
122
112
- def _find_requirements (self , work : "LightningWork" ) -> List [str ]:
113
- # 1. Get work file
114
- file = inspect .getfile (work .__class__ )
123
+ found_dockerfile = self ._find_dockerfile (work )
124
+ if self .dockerfile :
125
+ if found_dockerfile and self .dockerfile != found_dockerfile :
126
+ # notify the user of this silent behaviour
127
+ logger .info (
128
+ f"A Dockerfile exists at { found_dockerfile !r} but { self .dockerfile !r} was passed to"
129
+ f" the `{ type (self ).__name__ } ` in { work .name !r} . { found_dockerfile !r} ` will be ignored."
130
+ )
131
+ else :
132
+ self .dockerfile = found_dockerfile
133
+ self ._prepare_dockerfile ()
115
134
116
- # 2. Try to find a requirement file associated the file.
117
- dirname = os . path . dirname ( file ) or "."
118
- requirement_files = [ os . path . join ( dirname , f ) for f in os . listdir ( dirname ) if f == "requirements.txt" ]
119
- if not requirement_files :
135
+ def _find_requirements ( self , work : "L.LightningWork" , filename : str = "requirements.txt" ) -> List [ str ]:
136
+ # 1. Get work file
137
+ file = _get_work_file ( work )
138
+ if file is None :
120
139
return []
121
- dirname , basename = os .path .dirname (requirement_files [0 ]), os .path .basename (requirement_files [0 ])
140
+ # 2. Try to find a requirement file associated the file.
141
+ dirname = os .path .dirname (file )
122
142
try :
123
- requirements = load_requirements (dirname , basename )
143
+ requirements = load_requirements (dirname , filename )
124
144
except NotADirectoryError :
125
- requirements = []
145
+ return []
126
146
return [r for r in requirements if r != "lightning" ]
127
147
128
- def _find_dockerfile (self , work : "LightningWork" ) -> List [str ]:
148
+ def _find_dockerfile (self , work : "L. LightningWork" , filename : str = "Dockerfile" ) -> Optional [str ]:
129
149
# 1. Get work file
130
- file = inspect .getfile (work .__class__ )
131
-
132
- # 2. Check for Dockerfile.
133
- dirname = os .path .dirname (file ) or "."
134
- dockerfiles = [os .path .join (dirname , f ) for f in os .listdir (dirname ) if f == "Dockerfile" ]
135
-
136
- if not dockerfiles :
137
- return []
138
-
139
- # 3. Read the dockerfile
140
- with open (dockerfiles [0 ]) as f :
141
- dockerfile = list (f .readlines ())
142
- return dockerfile
143
-
144
- def _prepare_requirements (self ) -> Optional [Union [str , List [str ]]]:
145
- if not self .requirements :
150
+ file = _get_work_file (work )
151
+ if file is None :
146
152
return None
153
+ # 2. Check for Dockerfile.
154
+ dirname = os .path .dirname (file )
155
+ dockerfile = os .path .join (dirname , filename )
156
+ if os .path .isfile (dockerfile ):
157
+ return dockerfile
147
158
159
+ def _prepare_requirements (self ) -> None :
148
160
requirements = []
149
161
for req in self .requirements :
150
162
# 1. Check for relative path
151
163
path = os .path .join (self ._call_dir , req )
152
- if os .path .exists (path ):
164
+ if os .path .isfile (path ):
153
165
try :
154
- requirements .extend (
155
- load_requirements (os .path .dirname (path ), os .path .basename (path )),
156
- )
166
+ new_requirements = load_requirements (self ._call_dir , req )
157
167
except NotADirectoryError :
158
- pass
168
+ continue
169
+ requirements .extend (new_requirements )
159
170
else :
160
171
requirements .append (req )
161
-
162
172
self .requirements = requirements
163
173
164
- def _prepare_dockerfile (self ):
165
- if self .dockerfile :
166
- dockerfile_path = os .path .join (self ._call_dir , self .dockerfile )
167
- if os .path .exists (dockerfile_path ):
168
- with open (dockerfile_path ) as f :
169
- self .dockerfile = list ( f .readlines ())
174
+ def _prepare_dockerfile (self ) -> None :
175
+ if isinstance ( self .dockerfile , ( str , Path )) :
176
+ path = os .path .join (self ._call_dir , self .dockerfile )
177
+ if os .path .exists (path ):
178
+ with open (path ) as f :
179
+ self .dockerfile = _Dockerfile ( path , f .readlines ())
170
180
171
- def to_dict (self ):
181
+ def to_dict (self ) -> Dict :
172
182
return {"__build_config__" : asdict (self )}
173
183
174
184
@classmethod
175
- def from_dict (cls , d ):
185
+ def from_dict (cls , d : Dict ) -> Self : # type: ignore[valid-type]
176
186
return cls (** d ["__build_config__" ])
187
+
188
+
189
+ def _get_work_file (work : "L.LightningWork" ) -> Optional [str ]:
190
+ cls = work .__class__
191
+ try :
192
+ return inspect .getfile (cls )
193
+ except TypeError :
194
+ logger .debug (f"The { cls .__name__ } file couldn't be found." )
195
+ return None
0 commit comments