Skip to content

Commit 540748f

Browse files
committed
Fixed a few issues with serialization.
Mainly this switches to a introspection-based serialization scheme that is implemented outside of the core psij classes.
1 parent 2913333 commit 540748f

File tree

9 files changed

+468
-216
lines changed

9 files changed

+468
-216
lines changed

.mypy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ ignore_missing_imports = True
1717
[mypy-pystache.*]
1818
ignore_missing_imports = True
1919

20+
[mypy-typing_compat.*]
21+
ignore_missing_imports = True

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ filelock~=3.4
22
psutil~=5.9
33
pystache>=0.6.0
44
typeguard~=2.12
5+
typing-compat

src/psij/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717
from .job_state import JobState
1818
from .job_status import JobStatus
1919
from .resource_spec import ResourceSpec, ResourceSpecV1
20-
from .serialize import Export, Import
2120
from .version import VERSION
2221

2322
__version__ = VERSION
2423

2524
__all__ = [
2625
'JobExecutor', 'JobExecutorConfig', 'Job', 'JobStatusCallback', 'JobSpec', 'JobAttributes',
2726
'JobStatus', 'JobState', 'ResourceSpec', 'ResourceSpecV1', 'Launcher', 'SubmitException',
28-
'InvalidJobException', 'UnreachableStateException', 'Export', 'Import'
27+
'InvalidJobException', 'UnreachableStateException'
2928
]
3029

3130
logger = logging.getLogger(__name__)

src/psij/job_attributes.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,44 @@ def get_custom_attribute(self, name: str) -> Optional[object]:
5151
return None
5252
return self._custom_attributes[name]
5353

54+
@property
55+
def custom_attributes(self) -> Optional[Dict[str, object]]:
56+
"""Returns a dictionary with the custom attributes."""
57+
return self._custom_attributes
58+
59+
@custom_attributes.setter
60+
def custom_attributes(self, attrs: Optional[Dict[str, object]]) -> None:
61+
"""
62+
Sets all custom attributes from the given dictionary.
63+
64+
Existing custom attributes defined on this instance of `JobAttributes`
65+
are removed.
66+
67+
Parameters
68+
----------
69+
attrs
70+
A dictionary with the custom attributes to set.
71+
"""
72+
self._custom_attributes = attrs
73+
5474
def __repr__(self) -> str:
5575
"""Returns a string representation of this object."""
5676
return 'JobAttributes(duration={}, queue_name={}, project_name={}, reservation_id={}, ' \
5777
'custom_attributes={})'.format(self.duration, self.queue_name, self.project_name,
5878
self.reservation_id, self._custom_attributes)
79+
80+
def __eq__(self, o: object) -> bool:
81+
"""
82+
Tests if this JobAttributes object is equal to another object.
83+
84+
The objects are equal if all their properties are equal.
85+
"""
86+
if not isinstance(o, JobAttributes):
87+
return False
88+
89+
for prop_name in ['duration', 'queue_name', 'project_name', 'reservation_id',
90+
'custom_attributes']:
91+
if getattr(self, prop_name) != getattr(o, prop_name):
92+
return False
93+
94+
return True

src/psij/job_spec.py

Lines changed: 66 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
from __future__ import annotations
22

3-
import sys
3+
import pathlib
44
from pathlib import Path
5-
from typing import Any, Dict, List, Optional, Union
5+
from typing import Dict, List, Optional, Union
66

77
from 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, name: Optional[str] = None, executable: Optional[str] = None,
31-
arguments: Optional[List[str]] = None, directory: Optional[StrOrPath] = None,
27+
arguments: Optional[List[str]] = None,
28+
# For some odd reason, and only in the constructor, if Path is used directly,
29+
# sphinx fails to find the class. Using Path in the getters and setters does not
30+
# appear to trigger a problem.
31+
directory: Optional[Union[str, pathlib.Path]] = 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, name: Optional[str] = None, executable: Optional[str] = None,
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

src/psij/resource_spec.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ def version(self) -> int:
2828
"""Returns the version of this resource specification class."""
2929
pass
3030

31+
@staticmethod
32+
def get_instance(version: int) -> 'ResourceSpec':
33+
"""
34+
Creates an instance of a `ResourceSpec` of the specified version.
35+
36+
Parameters
37+
----------
38+
version
39+
The version of `ResourceSpec` to instantiate. For example, if `version == 1`, this
40+
method will return a new instance of `ResourceSpecV1`.
41+
"""
42+
if version == 1:
43+
return ResourceSpecV1()
44+
else:
45+
raise ValueError()
46+
3147

3248
class ResourceSpecV1(ResourceSpec):
3349
"""This class implements V1 of the PSI/J resource specification."""
@@ -168,3 +184,20 @@ def computed_processes_per_node(self) -> int:
168184
def version(self) -> int:
169185
"""Returns the version of this `ResourceSpec`, which is 1 for this class."""
170186
return 1
187+
188+
def __eq__(self, o: object) -> bool:
189+
"""
190+
Tests if this ResourceSpecV1 is equal to another object.
191+
192+
The objects are equal if all their properties are equal.
193+
"""
194+
if not isinstance(o, ResourceSpecV1):
195+
return False
196+
197+
for prop_name in ['node_count', 'process_count', 'processes_per_node',
198+
'cpu_cores_per_process', 'gpu_cores_per_process',
199+
'exclusive_node_use']:
200+
if getattr(self, prop_name) != getattr(o, prop_name):
201+
return False
202+
203+
return True

0 commit comments

Comments
 (0)