Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions pym/bob/scm/scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@

SYNTHETIC_SCM_PROPS = frozenset(('__source', 'recipe', 'overridden'))

def _freezeValue(value):
"""Recursively turn a (possibly nested) value into a hashable one.

scmOverrides 'match' and 'set' allow arbitrary values (e.g. the 'headers'
dict of an url SCM). Such nested dicts/lists are unhashable, so convert
them into frozensets/tuples to make ScmOverride hashable.
"""
if isinstance(value, dict):
return frozenset((k, _freezeValue(v)) for k, v in value.items())
if isinstance(value, list):
return tuple(_freezeValue(v) for v in value)
return value

class ScmOverride:
def __init__(self, override):
self.__match = override.get("match", {})
Expand Down Expand Up @@ -54,8 +67,8 @@ def __doesMatch(self, scm, env):
return True

def __hash__(self):
return hash((frozenset(self.__match.items()), frozenset(self.__del),
frozenset(self.__set.items()), frozenset(self.__replace.items())))
return hash((_freezeValue(self.__match), frozenset(self.__del),
_freezeValue(self.__set), frozenset(self.__replace.items())))

def __eq__(self, other):
return ((self.__match, self.__del, self.__set, self.__replace) ==
Expand Down
33 changes: 33 additions & 0 deletions test/unit/test_input_scmoverride.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,36 @@ def testDump(self):

o = ScmOverride(spec)
self.assertEqual(spec, yaml.load(str(o), Loader=yaml.Loader))

def testHashableNestedValue(self):
"""A scmOverride with a nested dict value (e.g. url SCM headers) must
still be hashable. The builder collects active overrides in a set()."""
o = ScmOverride({
'match' : { 'url' : "https://gitea.example.com/*" },
'set' : { 'headers' : { 'Authorization' : "token 1234" } },
})
# Must not raise "unhashable type: 'dict'".
self.assertIn(o, { o })

# Equal overrides hash equally, unequal ones stay distinct.
same = ScmOverride({
'match' : { 'url' : "https://gitea.example.com/*" },
'set' : { 'headers' : { 'Authorization' : "token 1234" } },
})
other = ScmOverride({
'match' : { 'url' : "https://gitea.example.com/*" },
'set' : { 'headers' : { 'Authorization' : "token 4321" } },
})
self.assertEqual(hash(o), hash(same))
self.assertEqual(len({ o, same, other }), 2)

def testMangleNestedValue(self):
"""A nested dict value is injected verbatim into the matched SCM."""
o = ScmOverride({
'match' : { 'scm' : "url" },
'set' : { 'headers' : { 'Authorization' : "token 1234" } },
})
scm = { 'scm' : "url", 'url' : "https://gitea.example.com/x.tar" }
match, mangled = o.mangle(scm, Env())
self.assertTrue(match)
self.assertEqual(mangled['headers'], { 'Authorization' : "token 1234" })
Loading