Skip to content

Commit 954d5a2

Browse files
authored
Improve 'duplicate dependency' error messages (#688)
2 parents 3345c28 + e276e65 commit 954d5a2

4 files changed

Lines changed: 82 additions & 30 deletions

File tree

pym/bob/errors.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,14 @@ def __str__(self):
2222
ret = ret + "\n" + self.help
2323
return ret
2424

25-
def pushFrame(self, frame):
26-
if not self.stack or (self.stack[0] != frame):
27-
self.stack.insert(0, frame)
28-
29-
def setStack(self, stack):
30-
if not self.stack: self.stack = stack[:]
31-
3225
class ParseError(BobError):
3326
def __init__(self, slogan, *args, **kwargs):
3427
BobError.__init__(self, slogan, "Parse", "Processing stack", *args, **kwargs)
3528

29+
def pushFrame(self, frame):
30+
if not self.stack or (self.stack[0] != frame):
31+
self.stack.insert(0, frame)
32+
3633
def setPath(self, path):
3734
self.stackSlogan = "Offending file"
3835
self.stack = [path]
@@ -41,6 +38,9 @@ class BuildError(BobError):
4138
def __init__(self, slogan, *args, **kwargs):
4239
BobError.__init__(self, slogan, "Build", "Failed package", *args, **kwargs)
4340

41+
def setStack(self, stack):
42+
if not self.stack: self.stack = stack[:]
43+
4444

4545
class MultiBobError(BobError):
4646
def __init__(self, others):

pym/bob/input.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,10 +1977,11 @@ def result(self):
19771977

19781978
class DepTracker:
19791979

1980-
__slots__ = ('item', 'isNew', 'usedResult')
1980+
__slots__ = ('item', 'isNew', 'usedResult', 'depEntry')
19811981

1982-
def __init__(self, item):
1982+
def __init__(self, item, depEntry):
19831983
self.item = item
1984+
self.depEntry = depEntry
19841985
self.isNew = True
19851986
self.usedResult = False
19861987

@@ -2084,9 +2085,10 @@ class Dependency(object):
20842085
__slots__ = ('recipe', 'envOverride', 'provideGlobal', 'inherit',
20852086
'use', 'useEnv', 'useTools', 'useBuildResult', 'useDeps',
20862087
'useSandbox', 'condition', 'toolOverride', 'checkoutDep',
2087-
'alias')
2088+
'alias', 'origin')
20882089

2089-
def __init__(self, recipe, env, fwd, use, cond, tools, checkoutDep, inherit, alias):
2090+
def __init__(self, origin, recipe, env, fwd, use, cond, tools, checkoutDep, inherit, alias):
2091+
self.origin = origin
20902092
self.recipe = recipe
20912093
self.envOverride = env
20922094
self.provideGlobal = fwd
@@ -2103,9 +2105,9 @@ def __init__(self, recipe, env, fwd, use, cond, tools, checkoutDep, inherit, ali
21032105
self.alias = alias
21042106

21052107
@staticmethod
2106-
def __parseEntry(dep, env, fwd, use, cond, tools, checkoutDep, inherit):
2108+
def __parseEntry(origin, dep, env, fwd, use, cond, tools, checkoutDep, inherit):
21072109
if isinstance(dep, str):
2108-
return [ Recipe.Dependency(dep, env, fwd, use, cond, tools, checkoutDep,
2110+
return [ Recipe.Dependency(origin, dep, env, fwd, use, cond, tools, checkoutDep,
21092111
inherit, None) ]
21102112
else:
21112113
envOverride = dep.get("environment")
@@ -2126,23 +2128,24 @@ def __parseEntry(dep, env, fwd, use, cond, tools, checkoutDep, inherit):
21262128
name = dep.get("name")
21272129
if name:
21282130
if "depends" in dep:
2129-
raise ParseError("A dependency must not use 'name' and 'depends' at the same time!")
2130-
return [ Recipe.Dependency(name, env, fwd, use, cond, tools,
2131+
raise ParseError("A dependency must not use 'name' and 'depends' at the same time!",
2132+
help=f"The offending entries 'name' attribute is '{name}'")
2133+
return [ Recipe.Dependency(origin, name, env, fwd, use, cond, tools,
21312134
checkoutDep, inherit, dep.get("alias")) ]
21322135
dependencies = dep.get("depends")
21332136
if dependencies is None:
21342137
raise ParseError("Either 'name' or 'depends' required for dependencies!")
2135-
return Recipe.Dependency.parseEntries(dependencies, env, fwd,
2138+
return Recipe.Dependency.parseEntries(origin, dependencies, env, fwd,
21362139
use, cond, tools,
21372140
checkoutDep, inherit)
21382141

21392142
@staticmethod
2140-
def parseEntries(deps, env={}, fwd=False, use=["result", "deps"],
2143+
def parseEntries(origin, deps, env={}, fwd=False, use=["result", "deps"],
21412144
cond=None, tools={}, checkoutDep=False, inherit=True):
21422145
"""Returns an iterator yielding all dependencies as flat list"""
21432146
# return flattened list of dependencies
21442147
return chain.from_iterable(
2145-
Recipe.Dependency.__parseEntry(dep, env, fwd, use, cond, tools,
2148+
Recipe.Dependency.__parseEntry(origin, dep, env, fwd, use, cond, tools,
21462149
checkoutDep, inherit)
21472150
for dep in deps )
21482151

@@ -2205,7 +2208,7 @@ def __init__(self, recipeSet, recipe, layer, sourceFile, baseDir, packageName, b
22052208
self.__inherit = recipe.get("inherit", [])
22062209
self.__anonBaseClass = anonBaseClass
22072210
self.__defaultScriptLanguage = scriptLanguage
2208-
self.__deps = list(Recipe.Dependency.parseEntries(recipe.get("depends", [])))
2211+
self.__deps = list(Recipe.Dependency.parseEntries(self, recipe.get("depends", [])))
22092212
self.__packageName = packageName
22102213
self.__baseName = baseName
22112214
self.__root = recipe.get("root")
@@ -2628,14 +2631,16 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
26282631
# A dependency should be named only once. Hence we can
26292632
# optimistically create the DepTracker object. If the dependency is
26302633
# named more than one we make sure that it is the same variant.
2631-
depTrack = thisDeps.setdefault(p.getName(), DepTracker(depRef))
2634+
depTrack = thisDeps.setdefault(p.getName(), DepTracker(depRef, dep))
26322635
if depTrack.prime():
26332636
directPackages.append(depRef)
26342637
elif depCoreStep.variantId != depTrack.item.refGetDestination().variantId:
26352638
self.__raiseIncompatibleLocal(depCoreStep)
26362639
else:
2640+
sources = " and ".join(set([dep.origin.getPrimarySource(), depTrack.depEntry.origin.getPrimarySource()]))
26372641
raise ParseError("Duplicate dependency '{}'. Each dependency must only be named once!"
2638-
.format(p.getName()))
2642+
.format(p.getName()),
2643+
help=f"The dependencies were declared in {sources}.")
26392644

26402645
# Remember dependency diffs before changing them
26412646
origDepDiffTools = thisDepDiffTools
@@ -2704,7 +2709,7 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
27042709
name = depCoreStep.corePackage.getName()
27052710
depTrack = thisDeps.get(name)
27062711
if depTrack is None:
2707-
thisDeps[name] = depTrack = DepTracker(depRef)
2712+
thisDeps[name] = depTrack = DepTracker(depRef, None)
27082713

27092714
if depTrack.prime():
27102715
indirectPackages.append(depRef)

test/unit/test_input_recipe.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ def cmpEntry(self, entry, name, env={}, fwd=False, use=["result", "deps"],
2525

2626
def testSimpleList(self):
2727
deps = [ "a", "b" ]
28-
res = list(Recipe.Dependency.parseEntries(deps))
28+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
2929

3030
self.assertEqual(len(res), 2)
3131
self.cmpEntry(res[0], "a")
3232
self.cmpEntry(res[1], "b")
3333

3434
def testMixedList(self):
3535
deps = [ "a", { "name" : "b", "environment" : { "foo" : ("bar", None) }} ]
36-
res = list(Recipe.Dependency.parseEntries(deps))
36+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
3737

3838
self.assertEqual(len(res), 2)
3939
self.cmpEntry(res[0], "a")
@@ -47,7 +47,7 @@ def testNestedList(self):
4747
{ "depends" : [ "c" ] }
4848
]}
4949
]
50-
res = list(Recipe.Dependency.parseEntries(deps))
50+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
5151

5252
self.assertEqual(len(res), 3)
5353
self.cmpEntry(res[0], "a")
@@ -70,7 +70,7 @@ def testNestedEnv(self):
7070
},
7171
"e"
7272
]
73-
res = list(Recipe.Dependency.parseEntries(deps))
73+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
7474

7575
self.assertEqual(len(res), 5)
7676
self.cmpEntry(res[0], "a")
@@ -95,7 +95,7 @@ def testNestedIf(self):
9595
},
9696
"e"
9797
]
98-
res = list(Recipe.Dependency.parseEntries(deps))
98+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
9999

100100
self.assertEqual(len(res), 5)
101101
self.cmpEntry(res[0], "a")
@@ -120,7 +120,7 @@ def testNestedUse(self):
120120
},
121121
"e"
122122
]
123-
res = list(Recipe.Dependency.parseEntries(deps))
123+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
124124

125125
self.assertEqual(len(res), 5)
126126
self.cmpEntry(res[0], "a")
@@ -145,7 +145,7 @@ def testNestedFwd(self):
145145
},
146146
"e"
147147
]
148-
res = list(Recipe.Dependency.parseEntries(deps))
148+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
149149

150150
self.assertEqual(len(res), 5)
151151
self.cmpEntry(res[0], "a")
@@ -174,7 +174,7 @@ def testNestedCheckoutDep(self):
174174
"checkoutDep" : True,
175175
}
176176
]
177-
res = list(Recipe.Dependency.parseEntries(deps))
177+
res = list(Recipe.Dependency.parseEntries(MagicMock(), deps))
178178

179179
self.assertEqual(len(res), 6)
180180
self.cmpEntry(res[0], "a")
@@ -184,6 +184,27 @@ def testNestedCheckoutDep(self):
184184
self.cmpEntry(res[4], "e")
185185
self.cmpEntry(res[5], "f", checkoutDep=True)
186186

187+
def testNameAndDepends(self):
188+
"""A dependency must not use 'name' and 'depends' at the same time"""
189+
deps = [
190+
{
191+
"name" : "a",
192+
"depends" : [],
193+
}
194+
]
195+
with self.assertRaises(ParseError):
196+
list(Recipe.Dependency.parseEntries(MagicMock(), deps))
197+
198+
def testNeitherNameNorDepends(self):
199+
"""A dependency must use 'name' or 'depends'"""
200+
deps = [
201+
{
202+
"if" : "a",
203+
}
204+
]
205+
with self.assertRaises(ParseError):
206+
list(Recipe.Dependency.parseEntries(MagicMock(), deps))
207+
187208

188209
class RecipeCommon:
189210

test/unit/test_input_recipeset.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,32 @@ def testPackageDepends(self):
877877
self.assertEqual(p.getPackageStep().getArguments()[2].getPackage().getName(),
878878
"lib2")
879879

880+
def testDuplicateDep(self):
881+
"""Dependencies must only be named once"""
882+
self.writeRecipe("root", """\
883+
root: True
884+
depends: [a, a]
885+
""")
886+
self.writeRecipe("a", "")
887+
888+
packages = self.generate()
889+
self.assertRaises(ParseError, packages.getRootPackage)
890+
891+
def testDuplicateDepWithClass(self):
892+
"""Dependencies must only be named once"""
893+
self.writeRecipe("root", """\
894+
root: True
895+
inherit: [cls]
896+
depends: [a]
897+
""")
898+
self.writeClass("cls", """\
899+
depends: [a]
900+
""")
901+
self.writeRecipe("a", "")
902+
903+
packages = self.generate()
904+
self.assertRaises(ParseError, packages.getRootPackage)
905+
880906
class TestDependencyEnv(RecipesTmp, TestCase):
881907
"""Tests related to "environment" block in dependencies"""
882908

0 commit comments

Comments
 (0)