Skip to content

Commit a24ccbc

Browse files
authored
Optionally break version-incompatible dependencies (#45)
This flag is intended to support scenarios where a developer intentionally includes packages in a workspace where the underlying build system will not use dependencies which have incompatible versions. Because this scenario is somewhat of a deviation from the established expectations of colcon behavior, we'll print a warning to the developer (at least for now). At this stage in package enumeration, we may encounter multiple packages with a given name. This change also re-structures the enumeration so that all packages with a given name can be checked, where previously only a single instance of the package was checked.
1 parent 1c1c0ad commit a24ccbc

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

colcon_package_information/package_augmentation/check_dependency_constraint.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,37 @@ def __init__(self): # noqa: D107
2929
def augment_packages( # noqa: D102
3030
self, descs, *, additional_argument_names=None
3131
):
32-
descs_dict = {desc.name: desc for desc in descs}
33-
for name, desc in descs_dict.items():
32+
descs_dict = {}
33+
for desc in descs:
34+
descs_dict.setdefault(desc.name, []).append(desc)
35+
for desc in descs:
3436
deps = desc.get_dependencies()
3537
for dep in deps:
3638
# skip dependencies which don't have a descriptor
3739
if dep.name not in descs_dict:
3840
continue
3941

40-
self._check_version_constraints(
41-
desc, dep, descs_dict[dep.name])
42+
miss = all(
43+
self._check_version_constraints(desc, dep, dep_desc)
44+
for dep_desc in descs_dict.get(dep.name, ()))
45+
if miss and dep.metadata.get('skip_incompatible'):
46+
logger.warning(
47+
'Dropping version-incompatible dependency on ' +
48+
dep.name + ' from ' + desc.name)
49+
for category in dep.metadata.get('categories') or ():
50+
desc.dependencies[category].discard(dep)
4251

4352
def _check_version_constraints(self, desc, dep, dep_desc):
53+
"""
54+
Check dependency version constraints against another package.
55+
56+
:param desc: The package declaring the dependency being checked
57+
:param dep: The dependency being checked
58+
:param dep_desc: The package to which the dependency refers
59+
60+
:returns: True if one or more constraints fail, False if all
61+
constraints pass, and None if no constraints were checked.
62+
"""
4463
# if the dependency descriptor doesn't have a version there is nothing
4564
# to compare to
4665
if 'version' not in dep_desc.metadata:
@@ -65,6 +84,7 @@ def gte(a, b):
6584
'version_gte': (gte, 'greater than or equal to'),
6685
'version_gt': (operator.gt, 'greater than'),
6786
}
87+
miss = None
6888
for key, value in dep.metadata.items():
6989
# only consider version operator metadata
7090
if key not in operators:
@@ -81,12 +101,15 @@ def gte(a, b):
81101
continue
82102

83103
if not op(dep_version, version_constraint):
104+
miss = True
84105
logger.warning(
85106
desc.name + ' depends on ' + dep.name +
86107
' which has version ' + dep_desc.metadata['version'] +
87108
' but expects it to be ' + msg + ' ' + value)
88109
else:
110+
miss = miss or False
89111
logger.debug(
90112
desc.name + ' depends on ' + dep.name +
91113
' which has version ' + dep_desc.metadata['version'] +
92114
' which satisfies to be ' + msg + ' ' + value)
115+
return miss

test/test_check_dependency_constraint.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,47 @@ def test_dependency_invalid_version_operator():
116116

117117
extension = CheckDependencyConstraintPackageAugmentation()
118118
extension.augment_packages({pkg_a, pkg_b})
119+
120+
121+
@pytest.mark.parametrize(
122+
'a_versions,skip_incompatible,dep_categories,expect_present', [
123+
(['1.0'], True, {'build'}, False),
124+
(['1.0', '3.0'], True, {'build'}, False),
125+
(['1.0', '2.0'], True, {'build'}, True),
126+
(['1.0'], False, {'build'}, True),
127+
(['1.0'], True, {'build', 'run'}, False),
128+
([None], True, {'build'}, True),
129+
])
130+
def test_skip_incompatible(
131+
a_versions, skip_incompatible, dep_categories, expect_present
132+
):
133+
descs = set()
134+
for i, v in enumerate(a_versions):
135+
pkg_a = PackageDescriptor(f'/tmp/pkg_a_{i}')
136+
pkg_a.name = 'pkg_a'
137+
if v is not None:
138+
pkg_a.metadata['version'] = v
139+
descs.add(pkg_a)
140+
141+
metadata = {
142+
'version_eq': '2.0',
143+
'skip_incompatible': skip_incompatible,
144+
'categories': dep_categories,
145+
}
146+
pkg_a_dep = DependencyDescriptor('pkg_a', metadata=metadata)
147+
148+
pkg_b = PackageDescriptor('/tmp/pkg_b')
149+
pkg_b.name = 'pkg_b'
150+
for cat in dep_categories:
151+
pkg_b.dependencies[cat] = {pkg_a_dep}
152+
153+
descs.add(pkg_b)
154+
155+
extension = CheckDependencyConstraintPackageAugmentation()
156+
extension.augment_packages(descs)
157+
158+
for cat in dep_categories:
159+
if expect_present:
160+
assert pkg_a_dep in pkg_b.dependencies[cat]
161+
else:
162+
assert pkg_a_dep not in pkg_b.dependencies.get(cat, set())

0 commit comments

Comments
 (0)