11from __future__ import annotations
22
3- import sys
3+ import pathlib
44from pathlib import Path
5- from typing import Any , Dict , List , Optional , Union
5+ from typing import Dict , List , Optional , Union
66
77from typeguard import check_argument_types
88
9- from psij .job_attributes import JobAttributes
10- from psij .resource_spec import ResourceSpec
11- from psij .utils import path_object_to_full_path as o2p
9+ import psij .resource_spec
10+ import psij .job_attributes
1211
1312
14- StrOrPath = Union [str , Path ]
15-
16-
17- def _to_path (arg : Optional [StrOrPath ]) -> Optional [Path ] :
13+ def _to_path (arg : Optional [Union [str , Path ]]) -> Optional [Path ]:
1814 if isinstance (arg , Path ):
1915 return arg
2016 elif arg is None :
@@ -28,12 +24,24 @@ class JobSpec(object):
2824 """A class to hold information about the characteristics of a:class:`~psij.Job`."""
2925
3026 def __init__ (self , executable : Optional [str ] = None , arguments : Optional [List [str ]] = None ,
31- directory : Optional [StrOrPath ] = None , name : Optional [str ] = None ,
27+ # For some odd reason, and only in the constructor, if Path is used directly,
28+ # sphinx fails to find the class. Using Path in the getters and setters does not
29+ # appear to trigger a problem.
30+ directory : Optional [Union [str , pathlib .Path ]] = None ,
31+ name : Optional [str ] = None ,
3232 inherit_environment : bool = True , environment : Optional [Dict [str , str ]] = None ,
33- stdin_path : Optional [StrOrPath ] = None , stdout_path : Optional [StrOrPath ] = None ,
34- stderr_path : Optional [StrOrPath ] = None , resources : Optional [ResourceSpec ] = None ,
35- attributes : Optional [JobAttributes ] = None , pre_launch : Optional [StrOrPath ] = None ,
36- post_launch : Optional [StrOrPath ] = None , launcher : Optional [str ] = None ):
33+ stdin_path : Optional [Union [str , pathlib .Path ]] = None ,
34+ stdout_path : Optional [Union [str , pathlib .Path ]] = None ,
35+ stderr_path : Optional [Union [str , pathlib .Path ]] = None ,
36+ # Importing ResourceSpec directly used to work, but for some unclear reason
37+ # sphinx started complaining about finding duplicate ResourceSpec classes,
38+ # psij.resource_spec.ResourceSpec and psij.ResourceSpec, despite the latter not
39+ # being imported.
40+ resources : Optional [psij .resource_spec .ResourceSpec ] = None ,
41+ attributes : Optional [psij .job_attributes .JobAttributes ] = None ,
42+ pre_launch : Optional [Union [str , pathlib .Path ]] = None ,
43+ post_launch : Optional [Union [str , pathlib .Path ]] = None ,
44+ launcher : Optional [str ] = None ):
3745 """
3846 Constructs a `JobSpec` object while allowing its properties to be initialized.
3947
@@ -123,7 +131,8 @@ def __init__(self, executable: Optional[str] = None, arguments: Optional[List[st
123131 self ._stdout_path = _to_path (stdout_path )
124132 self ._stderr_path = _to_path (stderr_path )
125133 self .resources = resources
126- self .attributes = attributes if attributes is not None else JobAttributes ()
134+ self .attributes = attributes if attributes is not None else \
135+ psij .job_attributes .JobAttributes ()
127136 self ._pre_launch = _to_path (pre_launch )
128137 self ._post_launch = _to_path (post_launch )
129138 self .launcher = launcher
@@ -140,146 +149,88 @@ def name(self) -> Optional[str]:
140149 else :
141150 return self ._name
142151
152+ @name .setter
153+ def name (self , value : Optional [str ]) -> None :
154+ self ._name = value
155+
143156 @property
144157 def directory (self ) -> Optional [Path ]:
158+ """The directory, on the compute side, in which the executable is to be run."""
145159 return self ._directory
146160
147161 @directory .setter
148- def directory (self , directory : Optional [StrOrPath ]) -> None :
162+ def directory (self , directory : Optional [Union [ str , Path ] ]) -> None :
149163 self ._directory = _to_path (directory )
150164
151165 @property
152166 def stdin_path (self ) -> Optional [Path ]:
167+ """Path to a file whose contents will be sent to the job's standard input."""
153168 return self ._stdin_path
154169
155170 @stdin_path .setter
156- def stdin_path (self , stdin_path : Optional [StrOrPath ]) -> None :
171+ def stdin_path (self , stdin_path : Optional [Union [ str , Path ] ]) -> None :
157172 self ._stdin_path = _to_path (stdin_path )
158173
159174 @property
160175 def stdout_path (self ) -> Optional [Path ]:
176+ """A path to a file in which to place the standard output stream of the job."""
161177 return self ._stdout_path
162178
163179 @stdout_path .setter
164- def stdout_path (self , stdout_path : Optional [StrOrPath ]) -> None :
180+ def stdout_path (self , stdout_path : Optional [Union [ str , Path ] ]) -> None :
165181 self ._stdout_path = _to_path (stdout_path )
166182
167183 @property
168184 def stderr_path (self ) -> Optional [Path ]:
185+ """A path to a file in which to place the standard error stream of the job."""
169186 return self ._stderr_path
170187
171188 @stderr_path .setter
172- def stderr_path (self , stderr_path : Optional [StrOrPath ]) -> None :
189+ def stderr_path (self , stderr_path : Optional [Union [ str , Path ] ]) -> None :
173190 self ._stderr_path = _to_path (stderr_path )
174191
175192 @property
176193 def pre_launch (self ) -> Optional [Path ]:
194+ """
195+ An optional path to a pre-launch script.
196+
197+ The pre-launch script is sourced before the launcher is invoked. It, therefore, runs on
198+ the service node of the job rather than on all of the compute nodes allocated to the job.
199+ """
177200 return self ._pre_launch
178201
179202 @pre_launch .setter
180- def pre_launch (self , pre_launch : Optional [StrOrPath ]) -> None :
203+ def pre_launch (self , pre_launch : Optional [Union [ str , Path ] ]) -> None :
181204 self ._pre_launch = _to_path (pre_launch )
182205
183206 @property
184207 def post_launch (self ) -> Optional [Path ]:
208+ """
209+ An optional path to a post-launch script.
210+
211+ The post-launch script is sourced after all the ranks of the job executable complete and
212+ is sourced on the same node as the pre-launch script.
213+ """
185214 return self ._post_launch
186215
187216 @post_launch .setter
188- def post_launch (self , post_launch : Optional [StrOrPath ]) -> None :
217+ def post_launch (self , post_launch : Optional [Union [ str , Path ] ]) -> None :
189218 self ._post_launch = _to_path (post_launch )
190219
191- def _init_job_spec_dict (self ) -> Dict [str , Any ]:
192- """Returns jobspec structure as dict."""
193- # convention:
194- # - if expected value is a string then the dict is initialized with an empty string
195- # - if the expected value is an object than the key is initialzied with None
196-
197- job_spec : Dict [str , Any ]
198- job_spec = {
199- 'name' : '' ,
200- 'executable' : '' ,
201- 'arguments' : [],
202- 'directory' : None ,
203- 'inherit_environment' : True ,
204- 'environment' : {},
205- 'stdin_path' : None ,
206- 'stdout_path' : None ,
207- 'stderr_path' : None ,
208- 'resources' : None ,
209- 'attributes' : None ,
210- 'launcher' : None
211- }
212-
213- return job_spec
220+ def __eq__ (self , o : object ) -> bool :
221+ """
222+ Tests if this JobSpec is equal to another.
214223
215- @property
216- def to_dict (self ) -> Dict [str , Any ]:
217- """Returns a dictionary representation of this object."""
218- d = self ._init_job_spec_dict
219-
220- # Map properties to keys
221- d ['name' ] = self .name
222- d ['executable' ] = self .executable
223- d ['arguments' ] = self .arguments
224- d ['directory' ] = o2p (self .directory )
225- d ['inherit_environment' ] = self .inherit_environment
226- d ['environment' ] = self .environment
227- d ['stdin_path' ] = o2p (self .stdin_path )
228- d ['stdout_path' ] = o2p (self .stdout_path )
229- d ['stderr_path' ] = o2p (self .stderr_path )
230- d ['resources' ] = self .resources
231-
232- # Handle attributes property
233- if self .attributes :
234- d ['attributes' ] = {
235- 'duration' : '' ,
236- 'queue_name' : '' ,
237- 'project_name' : '' ,
238- 'reservation_id' : '' ,
239- 'custom_attributes' : {},
240- }
241- for k , v in self .attributes .__dict__ .items ():
242- if k in ['duration' , 'queue_name' , 'project_name' , 'reservation_id' ]:
243- if v :
244- d ['attributes' ][k ] = str (v )
245- else :
246- d ['attributes' ][k ] = v
247- elif k == "_custom_attributes" :
248- if v :
249- for ck , cv in v .items ():
250- if not type (cv ).__name__ in ['str' ,
251- 'list' ,
252- 'dict' ,
253- 'NoneType' ,
254- 'bool' ,
255- 'int' ]:
256- sys .stderr .write ("Unsupported type "
257- + type (cv ).__name__
258- + " in JobAttributes.custom_attributes for key "
259- + ck
260- + ", skipping\n " )
261- else :
262- if ck :
263- d ['attributes' ]['custom_attributes' ][ck ] = str (cv )
264- else :
265- d ['attributes' ]['custom_attributes' ][ck ] = cv
266- else :
267- sys .stderr .write ("Unsupported attribute " + k + ", skipping attribute\n " )
268- else :
269- d ['attributes' ] = None
270-
271- if self .resources :
272-
273- d ['resources' ] = {
274- 'node_count' : None ,
275- 'process_count' : None ,
276- 'process_per_node' : None ,
277- 'cpu_cores_per_process' : None ,
278- 'gpu_cores_per_process' : None ,
279- 'exclusive_node_use' : None
280- }
281- r = self .resources .__dict__
282- for k in d ['resources' ].keys ():
283- d ['resources' ][k ] = r [k ] if k in r else None
284-
285- return d
224+ Two job specifications are equal if they represent the same job. That is, if all
225+ properties are pair-wise equal.
226+ """
227+ if not isinstance (o , JobSpec ):
228+ return False
229+
230+ for prop_name in ['name' , 'executable' , 'arguments' , 'directory' , 'inherit_environment' ,
231+ 'environment' , 'stdin_path' , 'stdout_path' , 'stderr_path' , 'resources' ,
232+ 'attributes' , 'pre_launch' , 'post_launch' , 'launcher' ]:
233+ if getattr (self , prop_name ) != getattr (o , prop_name ):
234+ return False
235+
236+ return True
0 commit comments