Skip to content

Commit a8258a6

Browse files
committed
Show the application order for mixins referencing other mixins
Print an additional 'application order' line for every mixin which references other mixins through a 'mixin' key, so the resolved composition is visible without following the references by hand. The order is computed with colcon_mixin.mixin.order and therefore matches the order which is applied at build time. Circular, unknown and malformed references are reported inline as 'unavailable (<reason>)' rather than raised, so the remaining mixins are still being shown. Mixins which don't reference other mixins are shown exactly as before. Add tests covering the added line and its indentation in both the list and the single mixin output, its absence for mixins without references and for an empty reference list, re-application through several paths and the three error cases.
1 parent df953e1 commit a8258a6

2 files changed

Lines changed: 169 additions & 1 deletion

File tree

colcon_mixin/subverb/show.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,35 @@
33

44
from colcon_core.plugin_system import satisfies_version
55
from colcon_mixin.mixin import get_mixins
6+
from colcon_mixin.mixin.order import CircularMixinError
7+
from colcon_mixin.mixin.order import compute_application_order
8+
from colcon_mixin.mixin.order import InvalidMixinError
9+
from colcon_mixin.mixin.order import MissingMixinError
610
from colcon_mixin.subverb import MixinSubverbExtensionPoint
711

812

13+
def _get_application_order(mixins, mixin_name):
14+
"""
15+
Get the order in which a mixin and the mixins it references are applied.
16+
17+
:param dict mixins: The mixins of a single verb
18+
:param str mixin_name: The name of the mixin to resolve
19+
:returns: The application order joined by arrows, or None if the mixin
20+
doesn't reference any other mixins
21+
:rtype: str
22+
"""
23+
if not mixins[mixin_name].get('mixin'):
24+
return None
25+
try:
26+
order = compute_application_order(mixins, mixin_name)
27+
except (CircularMixinError, InvalidMixinError, MissingMixinError) as e:
28+
# an unresolvable reference is reported here rather than raised, so
29+
# that the remaining mixins are still being shown
30+
reason = str(e)
31+
return 'unavailable ({reason})'.format_map(locals())
32+
return ' -> '.join(order)
33+
34+
935
def _get_mixin_name_completer(verb_key, mixins_by_verb):
1036
def mixin_name_completer(prefix, **kwargs):
1137
"""Callable returning a list of mixin names."""
@@ -71,8 +97,15 @@ def main(self, *, context): # noqa: D102
7197
else:
7298
print('- {mixin_name}'.format_map(locals()))
7399
mixin_value = mixins[mixin_name]
100+
indent = ' ' if context.args.mixin_name is None else ''
74101
for arg_key, arg_value in mixin_value.items():
75-
indent = ' ' if context.args.mixin_name is None else ''
76102
print(
77103
'{indent}{arg_key}: {arg_value}'
78104
.format_map(locals()))
105+
# only shown for mixins referencing other mixins, to make the
106+
# resolved order of the composition visible
107+
application_order = _get_application_order(mixins, mixin_name)
108+
if application_order is not None:
109+
print(
110+
'{indent}application order: {application_order}'
111+
.format_map(locals()))

test/test_show.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2026 Innocent Pious
2+
# Licensed under the Apache License, Version 2.0
3+
4+
"""Tests for the output of the mixin show subverb."""
5+
6+
import argparse
7+
from unittest.mock import patch
8+
9+
from colcon_mixin.subverb.show import ShowMixinSubverb
10+
11+
12+
REFERENCING_MIXINS = {
13+
('build', ): {
14+
'base': {'cmake-args': ['-DA=1']},
15+
'debug': {'mixin': ['base'], 'cmake-args': ['-DB=1']},
16+
'strict': {'mixin': ['debug'], 'cmake-args': ['-DC=1']},
17+
},
18+
}
19+
20+
21+
PLAIN_MIXINS = {
22+
('build', ): {
23+
'debug': {'cmake-args': ['-DB=1']},
24+
'release': {'cmake-args': ['-DD=1']},
25+
},
26+
}
27+
28+
29+
def _show(capsys, mixins_by_verb, verb=None, mixin_name=None):
30+
with patch(
31+
'colcon_mixin.subverb.show.get_mixins', return_value=mixins_by_verb
32+
):
33+
extension = ShowMixinSubverb()
34+
extension.add_arguments(parser=argparse.ArgumentParser())
35+
context = argparse.Namespace(
36+
args=argparse.Namespace(verb=verb, mixin_name=mixin_name))
37+
rc = extension.main(context=context)
38+
return capsys.readouterr().out.splitlines(), rc
39+
40+
41+
def test_application_order_for_referencing_mixins(capsys):
42+
lines, rc = _show(capsys, REFERENCING_MIXINS)
43+
assert rc is None
44+
assert lines == [
45+
'build:',
46+
'- base',
47+
" cmake-args: ['-DA=1']",
48+
'- debug',
49+
" mixin: ['base']",
50+
" cmake-args: ['-DB=1']",
51+
' application order: base -> debug',
52+
'- strict',
53+
" mixin: ['debug']",
54+
" cmake-args: ['-DC=1']",
55+
' application order: base -> debug -> strict',
56+
]
57+
58+
59+
def test_no_application_order_without_references(capsys):
60+
# mixins which don't reference others are shown as before
61+
lines, _ = _show(capsys, PLAIN_MIXINS)
62+
assert lines == [
63+
'build:',
64+
'- debug',
65+
" cmake-args: ['-DB=1']",
66+
'- release',
67+
" cmake-args: ['-DD=1']",
68+
]
69+
70+
71+
def test_no_application_order_for_empty_reference_list(capsys):
72+
mixins = {('build', ): {'a': {'mixin': [], 'cmake-args': ['-DA=1']}}}
73+
lines, _ = _show(capsys, mixins)
74+
assert not any('application order' in line for line in lines)
75+
76+
77+
def test_application_order_for_single_mixin_is_not_indented(capsys):
78+
lines, _ = _show(
79+
capsys, REFERENCING_MIXINS, verb='build', mixin_name='strict')
80+
assert lines == [
81+
"mixin: ['debug']",
82+
"cmake-args: ['-DC=1']",
83+
'application order: base -> debug -> strict',
84+
]
85+
86+
87+
def test_application_order_repeats_mixins_reached_twice(capsys):
88+
mixins = {
89+
('build', ): {
90+
'a': {'mixin': ['b', 'c']},
91+
'b': {'mixin': ['d']},
92+
'c': {'mixin': ['d']},
93+
'd': {},
94+
},
95+
}
96+
lines, _ = _show(capsys, mixins, verb='build', mixin_name='a')
97+
assert 'application order: d -> b -> d -> c -> a' in lines
98+
99+
100+
def test_missing_reference_is_reported_inline(capsys):
101+
mixins = {
102+
('build', ): {
103+
'broken': {'mixin': ['nope']},
104+
'fine': {'cmake-args': ['-DA=1']},
105+
},
106+
}
107+
lines, rc = _show(capsys, mixins)
108+
assert rc is None
109+
order_line = [x for x in lines if 'application order' in x][0]
110+
assert 'unavailable' in order_line
111+
assert "unknown mixin 'nope'" in order_line
112+
# the remaining mixins are still being shown
113+
assert '- fine' in lines
114+
115+
116+
def test_circular_reference_is_reported_inline(capsys):
117+
mixins = {
118+
('build', ): {
119+
'a': {'mixin': ['b']},
120+
'b': {'mixin': ['a']},
121+
},
122+
}
123+
lines, rc = _show(capsys, mixins)
124+
assert rc is None
125+
assert ' application order: unavailable (Circular mixin reference: ' \
126+
'a -> b -> a)' in lines
127+
128+
129+
def test_invalid_reference_key_is_reported_inline(capsys):
130+
mixins = {('build', ): {'a': {'mixin': 'b'}, 'b': {}}}
131+
lines, rc = _show(capsys, mixins)
132+
assert rc is None
133+
order_line = [x for x in lines if 'application order' in x][0]
134+
assert 'unavailable' in order_line
135+
assert 'expected a list of strings' in order_line

0 commit comments

Comments
 (0)