Skip to content

Commit 83e68bd

Browse files
authored
Merge pull request #4070 from akesandgren/allow_dict_for_patches
fix type-checking of patches to allow dict values + correctly handle patches specified as dict values in --new-pr
2 parents 6dc5424 + 5ace8be commit 83e68bd

File tree

4 files changed

+146
-5
lines changed

4 files changed

+146
-5
lines changed

easybuild/framework/easyconfig/types.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,32 @@ def to_list_of_strings_and_tuples(spec):
349349
return str_tup_list
350350

351351

352+
def to_list_of_strings_and_tuples_and_dicts(spec):
353+
"""
354+
Convert a 'list of dicts and tuples/lists and strings' to a 'list of dicts and tuples and strings'
355+
356+
Example:
357+
['foo', ['bar', 'baz']]
358+
to
359+
['foo', ('bar', 'baz')]
360+
"""
361+
str_tup_list = []
362+
363+
if not isinstance(spec, (list, tuple)):
364+
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
365+
366+
for elem in spec:
367+
if isinstance(elem, (string_type, tuple, dict)):
368+
str_tup_list.append(elem)
369+
elif isinstance(elem, list):
370+
str_tup_list.append(tuple(elem))
371+
else:
372+
raise EasyBuildError("Expected elements to be of type string, tuple, dict or list, got %s (%s)",
373+
elem, type(elem))
374+
375+
return str_tup_list
376+
377+
352378
def to_sanity_check_paths_dict(spec):
353379
"""
354380
Convert a sanity_check_paths dict as received by yaml (a dict with list values that contain either lists or strings)
@@ -526,6 +552,7 @@ def ensure_iterable_license_specs(specs):
526552
'key_types': [str],
527553
}
528554
))
555+
STRING_OR_TUPLE_OR_DICT_LIST = (list, as_hashable({'elem_types': [str, TUPLE_OF_STRINGS, STRING_DICT]}))
529556
SANITY_CHECK_PATHS_DICT = (dict, as_hashable({
530557
'elem_types': {
531558
SANITY_CHECK_PATHS_FILES: [STRING_OR_TUPLE_LIST],
@@ -544,7 +571,8 @@ def ensure_iterable_license_specs(specs):
544571
CHECKSUMS = (list, as_hashable({'elem_types': [str, tuple, STRING_DICT, CHECKSUM_LIST]}))
545572

546573
CHECKABLE_TYPES = [CHECKSUM_LIST, CHECKSUMS, DEPENDENCIES, DEPENDENCY_DICT, LIST_OF_STRINGS,
547-
SANITY_CHECK_PATHS_DICT, STRING_DICT, STRING_OR_TUPLE_LIST, TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
574+
SANITY_CHECK_PATHS_DICT, STRING_DICT, STRING_OR_TUPLE_LIST, STRING_OR_TUPLE_OR_DICT_LIST,
575+
TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
548576

549577
# easy types, that can be verified with isinstance
550578
EASY_TYPES = [string_type, bool, dict, int, list, str, tuple]
@@ -555,7 +583,7 @@ def ensure_iterable_license_specs(specs):
555583
'docurls': LIST_OF_STRINGS,
556584
'name': string_type,
557585
'osdependencies': STRING_OR_TUPLE_LIST,
558-
'patches': STRING_OR_TUPLE_LIST,
586+
'patches': STRING_OR_TUPLE_OR_DICT_LIST,
559587
'sanity_check_paths': SANITY_CHECK_PATHS_DICT,
560588
'toolchain': TOOLCHAIN_DICT,
561589
'version': string_type,
@@ -575,4 +603,5 @@ def ensure_iterable_license_specs(specs):
575603
TOOLCHAIN_DICT: to_toolchain_dict,
576604
SANITY_CHECK_PATHS_DICT: to_sanity_check_paths_dict,
577605
STRING_OR_TUPLE_LIST: to_list_of_strings_and_tuples,
606+
STRING_OR_TUPLE_OR_DICT_LIST: to_list_of_strings_and_tuples_and_dicts,
578607
}

easybuild/tools/github.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,15 @@ def new_pr(paths, ecs, title=None, descr=None, commit_msg=None):
18001800
for patch in ec.asdict()['patches']:
18011801
if isinstance(patch, tuple):
18021802
patch = patch[0]
1803+
elif isinstance(patch, dict):
1804+
patch_info = {}
1805+
for key in patch.keys():
1806+
patch_info[key] = patch[key]
1807+
if 'name' not in patch_info.keys():
1808+
raise EasyBuildError("Wrong patch spec '%s', when using a dict 'name' entry must be supplied",
1809+
str(patch))
1810+
patch = patch_info['name']
1811+
18031812
if patch not in paths['patch_files'] and not os.path.isfile(os.path.join(os.path.dirname(ec_path),
18041813
patch)):
18051814
print_warning("new patch file %s, referenced by %s, is not included in this PR" %

test/framework/options.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4390,6 +4390,53 @@ def test_github_new_update_pr(self):
43904390
]
43914391
self._assert_regexs(regexs, txt, assert_true=False)
43924392

4393+
def test_new_pr_warning_missing_patch(self):
4394+
"""Test warning printed by --new-pr (dry run only) when a specified patch file could not be found."""
4395+
4396+
if self.github_token is None:
4397+
print("Skipping test_new_pr_warning_missing_patch, no GitHub token available?")
4398+
return
4399+
4400+
topdir = os.path.dirname(os.path.abspath(__file__))
4401+
test_ecs = os.path.join(topdir, 'easyconfigs', 'test_ecs')
4402+
test_ec = os.path.join(self.test_prefix, 'test.eb')
4403+
copy_file(os.path.join(test_ecs, 't', 'toy', 'toy-0.0-gompi-2018a-test.eb'), test_ec)
4404+
4405+
patches_regex = re.compile(r'^patches = .*', re.M)
4406+
test_ec_txt = read_file(test_ec)
4407+
4408+
patch_fn = 'this_patch_does_not_exist.patch'
4409+
test_ec_txt = patches_regex.sub('patches = ["%s"]' % patch_fn, test_ec_txt)
4410+
write_file(test_ec, test_ec_txt)
4411+
4412+
new_pr_out_regex = re.compile(r"Opening pull request", re.M)
4413+
warning_regex = re.compile("new patch file %s, referenced by .*, is not included in this PR" % patch_fn, re.M)
4414+
4415+
args = [
4416+
'--new-pr',
4417+
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
4418+
test_ec,
4419+
'-D',
4420+
]
4421+
stdout, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False)
4422+
4423+
new_pr_out_error_msg = "Pattern '%s' should be found in: %s" % (new_pr_out_regex.pattern, stdout)
4424+
self.assertTrue(new_pr_out_regex.search(stdout), new_pr_out_error_msg)
4425+
4426+
warning_error_msg = "Pattern '%s' should be found in: %s" % (warning_regex.pattern, stderr)
4427+
self.assertTrue(warning_regex.search(stderr), warning_error_msg)
4428+
4429+
# try again with patch specified via a dict value
4430+
test_ec_txt = patches_regex.sub('patches = [{"name": "%s", "alt_location": "foo"}]' % patch_fn, test_ec_txt)
4431+
write_file(test_ec, test_ec_txt)
4432+
4433+
stdout, stderr = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False)
4434+
4435+
new_pr_out_error_msg = "Pattern '%s' should be found in: %s" % (new_pr_out_regex.pattern, stdout)
4436+
self.assertTrue(new_pr_out_regex.search(stdout), new_pr_out_error_msg)
4437+
warning_error_msg = "Pattern '%s' should be found in: %s" % (warning_regex.pattern, stderr)
4438+
self.assertTrue(warning_regex.search(stderr), warning_error_msg)
4439+
43934440
def test_github_sync_pr_with_develop(self):
43944441
"""Test use of --sync-pr-with-develop (dry run only)."""
43954442
if self.github_token is None:

test/framework/type_checking.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@
3737
from easybuild.framework.easyconfig.types import LIST_OF_STRINGS, SANITY_CHECK_PATHS_DICT, STRING_OR_TUPLE_LIST
3838
from easybuild.framework.easyconfig.types import TOOLCHAIN_DICT
3939
from easybuild.framework.easyconfig.types import is_value_of_type, to_checksums, to_dependencies, to_dependency
40-
from easybuild.framework.easyconfig.types import to_list_of_strings, to_list_of_strings_and_tuples, to_toolchain_dict
41-
from easybuild.framework.easyconfig.types import to_sanity_check_paths_dict
40+
from easybuild.framework.easyconfig.types import to_list_of_strings, to_list_of_strings_and_tuples
41+
from easybuild.framework.easyconfig.types import to_list_of_strings_and_tuples_and_dicts
42+
from easybuild.framework.easyconfig.types import to_sanity_check_paths_dict, to_toolchain_dict
4243
from easybuild.tools.build_log import EasyBuildError
4344
from easybuild.tools.py2vs3 import string_type
4445

@@ -225,6 +226,25 @@ def test_check_type_of_param_value_checksums(self):
225226
for inp in inputs:
226227
self.assertEqual(check_type_of_param_value('checksums', inp), (True, inp))
227228

229+
def test_check_type_of_param_value_patches(self):
230+
"""Test check_type_of_param_value function for patches."""
231+
232+
# patches values that do not need to be converted
233+
inputs = (
234+
[], # empty list of patches
235+
# single patch, different types
236+
['foo.patch'], # only filename
237+
[('foo.patch', '1')], # filename + patch level
238+
[('foo.patch', 'subdir')], # filename + subdir to apply patch in
239+
[{'name': 'foo.patch', 'level': '1'}], # filename + patch level, as dict value
240+
# multiple patches, mix of different types
241+
['1.patch', '2.patch', '3.patch'],
242+
['1.patch', ('2.patch', '2'), {'name': '3.patch'}],
243+
['1.patch', {'name': '2.patch', 'level': '2'}, ('3.patch', '3')],
244+
)
245+
for inp in inputs:
246+
self.assertEqual(check_type_of_param_value('patches', inp), (True, inp))
247+
228248
def test_convert_value_type(self):
229249
"""Test convert_value_type function."""
230250
# to string
@@ -614,11 +634,47 @@ def test_to_list_of_strings_and_tuples(self):
614634
self.assertEqual(to_list_of_strings_and_tuples(('foo', ['bar', 'baz'])), ['foo', ('bar', 'baz')])
615635

616636
# conversion failures
617-
self.assertErrorRegex(EasyBuildError, "Expected value to be a list", to_list_of_strings_and_tuples, 'foo')
637+
error_regex = "Expected value to be a list"
638+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples, 'foo')
639+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples, 1)
640+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples, {'foo': 'bar'})
618641
error_msg = "Expected elements to be of type string, tuple or list"
619642
self.assertErrorRegex(EasyBuildError, error_msg, to_list_of_strings_and_tuples, ['foo', 1])
620643
self.assertErrorRegex(EasyBuildError, error_msg, to_list_of_strings_and_tuples, (1,))
621644

645+
def test_to_list_of_strings_and_tuples_and_dicts(self):
646+
"""Test to_list_of_strings_and_tuples_and_dicts function."""
647+
648+
# no conversion, already right type
649+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([]), [])
650+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([()]), [()])
651+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(['foo']), ['foo'])
652+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([{}]), [{}])
653+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([('foo', 'bar')]), [('foo', 'bar')])
654+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([('foo', 'bar'), 'baz']), [('foo', 'bar'), 'baz'])
655+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([('x',), 'y', {'z': 1}]), [('x',), 'y', {'z': 1}])
656+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(['y', {'z': 1}]), ['y', {'z': 1}])
657+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([{'z': 1}, ('x',)]), [{'z': 1}, ('x',)])
658+
659+
# actual conversion
660+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(()), [])
661+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(('foo',)), ['foo'])
662+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts([['bar', 'baz']]), [('bar', 'baz')])
663+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts((['bar', 'baz'],)), [('bar', 'baz')])
664+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(['foo', ['bar', 'baz']]), ['foo', ('bar', 'baz')])
665+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(('foo', ['bar', 'baz'])), ['foo', ('bar', 'baz')])
666+
self.assertEqual(to_list_of_strings_and_tuples_and_dicts(('x', ['y'], {'z': 1})), ['x', ('y',), {'z': 1}])
667+
668+
# conversion failures
669+
error_regex = "Expected value to be a list"
670+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples_and_dicts, 'foo')
671+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples_and_dicts, 1)
672+
self.assertErrorRegex(EasyBuildError, error_regex, to_list_of_strings_and_tuples_and_dicts, {'foo': 'bar'})
673+
error_msg = "Expected elements to be of type string, tuple, dict or list"
674+
self.assertErrorRegex(EasyBuildError, error_msg, to_list_of_strings_and_tuples_and_dicts, ['foo', 1])
675+
self.assertErrorRegex(EasyBuildError, error_msg, to_list_of_strings_and_tuples_and_dicts, (1,))
676+
self.assertErrorRegex(EasyBuildError, error_msg, to_list_of_strings_and_tuples_and_dicts, (1, {'foo': 'bar'}))
677+
622678
def test_to_sanity_check_paths_dict(self):
623679
"""Test to_sanity_check_paths_dict function."""
624680
# no conversion, already right type

0 commit comments

Comments
 (0)