Skip to content

Commit 66f95c0

Browse files
authored
v0.4.2 (#171)
* resolve #168 * resolve #169 * resolve #165 * resolve #173
1 parent ec47633 commit 66f95c0

File tree

13 files changed

+229
-27
lines changed

13 files changed

+229
-27
lines changed

.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,5 @@ venv.bak/
108108

109109
Dockerfile
110110
dr
111+
112+
!htmap-exec/singularity.d/*

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,5 @@ venv.bak/
106106
# Editors
107107
.idea/
108108
.vscode/
109+
110+
!htmap-exec/singularity.d/*

docs/source/versions/v0_4_2.rst

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
v0.4.2
2+
======
3+
4+
New Features
5+
------------
6+
7+
Bug Fixes
8+
---------
9+
10+
* `Map.errors` and `Map.error_reports()` now work when there is a mix of holds
11+
and errors in the map. Previously, held components would cause both of these to
12+
raise `MapComponentHeld` when trying to access them in that situation.
13+
Issue: https://github.com/htcondor/htmap/issues/165
14+
* Requirements statement merging was broken when any of the three sources of requirements
15+
(settings, function-level map options, and individual-map map options) were not given.
16+
Requirements from all source are now properly merged, regardless of whether any of them
17+
actually exist.
18+
Issue: https://github.com/htcondor/htmap/issues/168
19+
* Top-level settings that were dictionaries (like ``MAP_OPTIONS``) did not behave
20+
correctly when elements of them were set; they did not inherit the old settings.
21+
These kinds of settings are now properly inherited, but expect breaking changes in the
22+
`Settings` API next release to resolve the underlying issues.
23+
Issue: https://github.com/htcondor/htmap/issues/169
24+
* The ``htmap-exec`` Docker image should now cleanly export to Singularity.
25+
Issue: https://github.com/htcondor/htmap/issues/173
26+
27+
Known Issues
28+
------------
29+
30+
* Execution errors that result in the job being terminated but no output being
31+
produced are still not handled entirely gracefully. Right now, the component
32+
state will just show as ``ERRORED``, but there won't be an actual error report.
33+
* Map component state may become corrupted when a map is manually vacated.
34+
Force-removal may be needed to clean up maps if HTCondor and HTMap disagree
35+
about the state of their components.
36+
Issue: https://github.com/htcondor/htmap/issues/129

htmap-exec/Dockerfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
FROM continuumio/anaconda3:latest
16+
FROM continuumio/anaconda3:2019.03
1717

1818
LABEL maintainer="[email protected]"
1919

2020
COPY . /tmp/htmap
21-
RUN pip install --no-cache-dir /tmp/htmap/ \
21+
RUN python3 -m pip install --no-cache-dir /tmp/htmap/ \
2222
&& rm -rf /tmp/htmap
2323

24+
COPY htmap-exec/singularity.d /.singularity.d
25+
2426
ARG USER=htmap
2527
RUN groupadd ${USER} \
2628
&& useradd -m -g ${USER} ${USER}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export PATH=/opt/conda/bin:$PATH

htmap/maps.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,9 @@ def errors(self) -> Dict[int, errors.ComponentError]:
672672
for idx in self.components:
673673
try:
674674
err[idx] = self.get_err(idx)
675-
except (exceptions.OutputNotFound, exceptions.ExpectedError) as e:
675+
except (exceptions.OutputNotFound,
676+
exceptions.ExpectedError,
677+
exceptions.MapComponentHeld) as e:
676678
pass
677679

678680
return err
@@ -684,7 +686,10 @@ def error_reports(self) -> Iterator[str]:
684686
for idx in self.components:
685687
try:
686688
yield self.get_err(idx, timeout = 0).report()
687-
except (exceptions.OutputNotFound, exceptions.ExpectedError, exceptions.TimeoutError) as e:
689+
except (exceptions.OutputNotFound,
690+
exceptions.ExpectedError,
691+
exceptions.TimeoutError,
692+
exceptions.MapComponentHeld) as e:
688693
pass
689694

690695
@property

htmap/options.py

+24-11
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,10 @@ def create_submit_object_and_itemdata(
173173
settings['DELIVERY_METHOD'],
174174
)
175175

176-
# todo: needs test
177-
base_requirements = descriptors.get('requirements', None)
178-
extra_requirements = map_options.pop('requirements', None)
179-
if base_requirements is not None and extra_requirements is not None:
180-
descriptors['requirements'] = f'({base_requirements}) && ({extra_requirements})'
176+
descriptors['requirements'] = merge_requirements(
177+
descriptors.get('requirements', None),
178+
map_options.get('requirements', None),
179+
)
181180

182181
itemdata = [{'component': str(idx)} for idx in range(num_components)]
183182

@@ -214,6 +213,9 @@ def create_submit_object_and_itemdata(
214213
else:
215214
descriptors[opt_key] = opt_value
216215

216+
if descriptors['requirements'] is None:
217+
descriptors.pop('requirements')
218+
217219
sub = htcondor.Submit(descriptors)
218220

219221
return sub, itemdata
@@ -236,6 +238,13 @@ def unregister_delivery_mechanism(name: str) -> None:
236238
SETUP_FUNCTION_BY_DELIVERY.pop(name)
237239

238240

241+
def merge_requirements(*requirements: Optional[str]) -> Optional[str]:
242+
requirements = [req for req in requirements if req is not None]
243+
if len(requirements) == 0:
244+
return None
245+
return ' && '.join(f'({req})' for req in requirements)
246+
247+
239248
def get_base_descriptors(
240249
tag: str,
241250
map_dir: Path,
@@ -265,17 +274,21 @@ def get_base_descriptors(
265274

266275
from_settings = settings.get('MAP_OPTIONS', default = {})
267276

268-
base_requirements = base.pop('requirements', None)
269-
settings_requirements = from_settings.pop('requirements', None)
270-
if base_requirements is not None and settings_requirements is not None:
271-
core['requirements'] = f'({base_requirements}) && ({settings_requirements})'
272-
273-
return {
277+
merged = {
274278
**core,
275279
**base,
276280
**from_settings,
277281
}
278282

283+
# manually fix-up requirements
284+
merged['requirements'] = merge_requirements(
285+
core.get('requirements', None),
286+
base.get('requirements', None),
287+
from_settings.get('requirements', None),
288+
)
289+
290+
return merged
291+
279292

280293
def _copy_run_scripts():
281294
run_script_source_dir = Path(__file__).parent / names.RUN_DIR

htmap/settings.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,14 @@ def __init__(self, *settings):
4949
self.maps = list(settings)
5050

5151
def __getitem__(self, key: str):
52-
for map in self.maps:
52+
try:
5353
path = key.split('.')
54-
r = map
55-
try:
56-
for component in path:
57-
r = r[component]
58-
except (KeyError, TypeError):
59-
continue
54+
r = self.to_dict()
55+
for component in path:
56+
r = r[component]
6057
return r
61-
62-
raise exceptions.MissingSetting()
58+
except (KeyError, TypeError):
59+
raise exceptions.MissingSetting()
6360

6461
def __eq__(self, other: Any) -> bool:
6562
return type(self) is type(other) and self.to_dict() == other.to_dict()
@@ -145,7 +142,7 @@ def __str__(self) -> str:
145142
return utils.rstr(toml.dumps(self.to_dict()))
146143

147144
def __repr__(self) -> str:
148-
return utils.rstr(f'<{self.__class__.__name__}>')
145+
return f'<{self.__class__.__name__}>'
149146

150147

151148
htmap_dir = Path(os.getenv('HTMAP_DIR', Path.home() / '.htmap'))

htmap/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from typing import Tuple
1717

18-
__version__ = '0.4.1'
18+
__version__ = '0.4.2'
1919

2020

2121
def version() -> str:

tests/_inf/.htmaprc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
DELIVERY_METHOD = "assume"
22

33
[MAP_OPTIONS]
4-
REQUEST_DISK = "100MB"
4+
request_disk = "100MB"
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2019 HTCondor Team, Computer Sciences Department,
2+
# University of Wisconsin-Madison, WI.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from pathlib import Path
17+
18+
import pytest
19+
20+
import htmap
21+
22+
import htcondor
23+
24+
25+
@pytest.fixture(scope = 'function')
26+
def hold_before_error():
27+
map = htmap.map(lambda x: 1 / x, [1, 0])
28+
29+
schedd = htcondor.Schedd()
30+
cluster_id = map._cluster_ids[0]
31+
schedd.act(htcondor.JobAction.Hold, f"(ClusterID == {cluster_id}) && (ProcID == 0)")
32+
33+
map.wait(holds_ok = True, errors_ok = True)
34+
35+
assert map.component_statuses == [htmap.ComponentStatus.HELD, htmap.ComponentStatus.ERRORED]
36+
37+
return map
38+
39+
40+
@pytest.fixture(scope = 'function')
41+
def error_before_hold():
42+
map = htmap.map(lambda x: 1 / x, [0, 1])
43+
44+
schedd = htcondor.Schedd()
45+
cluster_id = map._cluster_ids[0]
46+
schedd.act(htcondor.JobAction.Hold, f"(ClusterID == {cluster_id}) && (ProcID == 1)")
47+
48+
map.wait(holds_ok = True, errors_ok = True)
49+
50+
assert map.component_statuses == [htmap.ComponentStatus.ERRORED, htmap.ComponentStatus.HELD]
51+
52+
return map
53+
54+
55+
def test_can_get_error_if_hold_in_front(hold_before_error):
56+
hold_before_error.get_err(1)
57+
58+
59+
def test_can_get_error_if_error_in_front(error_before_hold):
60+
error_before_hold.get_err(0)
61+
62+
63+
def test_can_iterate_over_errors_if_hold_in_front(hold_before_error):
64+
assert len(list(hold_before_error.error_reports())) == 1
65+
66+
67+
def test_can_iterate_over_errors_if_error_in_front(error_before_hold):
68+
assert len(list(error_before_hold.error_reports())) == 1
69+
70+
71+
def test_can_get_errors_if_hold_in_front(hold_before_error):
72+
assert len(list(hold_before_error.errors)) == 1
73+
74+
75+
def test_can_get_errors_if_error_in_front(error_before_hold):
76+
assert len(list(error_before_hold.errors)) == 1

tests/unit/test_requirements.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2018 HTCondor Team, Computer Sciences Department,
2+
# University of Wisconsin-Madison, WI.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import pytest
17+
18+
from htmap.options import merge_requirements
19+
20+
21+
def test_merge_nothing():
22+
req = merge_requirements()
23+
24+
assert req is None
25+
26+
27+
def test_merge_one():
28+
req = merge_requirements(
29+
'HasPie == true',
30+
)
31+
32+
assert req == '(HasPie == true)'
33+
34+
35+
def test_merge_two():
36+
req = merge_requirements(
37+
'HasPie == true',
38+
'HasCake == false',
39+
)
40+
41+
assert req == '(HasPie == true) && (HasCake == false)'
42+
43+
44+
def test_merge_three():
45+
req = merge_requirements(
46+
'HasPie == true',
47+
'HasCake == false',
48+
'IsHappy == true',
49+
)
50+
51+
assert req == '(HasPie == true) && (HasCake == false) && (IsHappy == true)'
52+
53+
54+
def test_merge_with_none():
55+
req = merge_requirements(
56+
'HasPie == true',
57+
None,
58+
)
59+
60+
assert req == '(HasPie == true)'

tests/unit/test_settings.py

+8
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,11 @@ def test_can_load_saved_settings(tmpdir):
216216

217217
assert loaded == s
218218
assert loaded is not s
219+
220+
221+
def test_nested_dicts_are_not_hidden_by_set():
222+
s = Settings({'top': {'hidden': 0}})
223+
224+
s['top.new'] = 5
225+
226+
assert s['top'] == {'hidden': 0, 'new': 5}

0 commit comments

Comments
 (0)