diff --git a/ament_package/__init__.py b/ament_package/__init__.py index 329648d..f819bf5 100644 --- a/ament_package/__init__.py +++ b/ament_package/__init__.py @@ -94,7 +94,6 @@ def parse_package_string(data, *, filename=None): from .exceptions import InvalidPackage from .export import Export - from .group_dependency import GroupDependency from .package import Package from .person import Person from .url import Url @@ -212,10 +211,9 @@ def parse_package_string(data, *, filename=None): pkg.conflicts = _get_dependencies(root, 'conflict') pkg.replaces = _get_dependencies(root, 'replace') - # group dependencies and membership - pkg.group_depends = [ - GroupDependency(g) for g in _get_groups(root, 'group_depend')] - pkg.member_of_groups = _get_groups(root, 'member_of_group') + # group dependencies and memberships + pkg.group_depends = _get_group_dependencies(root, 'group_depend') + pkg.member_of_groups = _get_group_memberships(root, 'member_of_group') # exports export_node = _get_optional_node(root, 'export') @@ -374,8 +372,23 @@ def _get_dependencies(parent, tagname): return depends -def _get_groups(parent, tagname): - groups = [] +def _get_group_dependencies(parent, tagname): + from .group_dependency import GroupDependency + depends = [] + for node in _get_nodes(parent, tagname): + depends.append( + GroupDependency( + _get_node_value(node), + condition=_get_node_attr(node, 'condition', default=None))) + return depends + + +def _get_group_memberships(parent, tagname): + from .group_membership import GroupMembership + memberships = [] for node in _get_nodes(parent, tagname): - groups.append(_get_node_value(node)) - return groups + memberships.append( + GroupMembership( + _get_node_value(node), + condition=_get_node_attr(node, 'condition', default=None))) + return memberships diff --git a/ament_package/condition.py b/ament_package/condition.py new file mode 100644 index 0000000..2f3fbd2 --- /dev/null +++ b/ament_package/condition.py @@ -0,0 +1,76 @@ +# Copyright 2014 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pyparsing + + +def evaluate_condition(condition, context): + if condition is None: + return True + expr = _get_condition_expression() + try: + parse_results = expr.parseString(condition, parseAll=True) + except pyparsing.ParseException as e: + raise ValueError( + "condition '%s' failed to parse: %s" % (condition, e)) + return _evaluate(parse_results.asList()[0], context) + + +_condition_expression = None + + +def _get_condition_expression(): + global _condition_expression + if not _condition_expression: + pp = pyparsing + operator = pp.Regex('==|!=').setName('operator') + identifier = pp.Word('$', pp.alphanums + '_', min=2) + value = pp.Word(pp.alphanums + '_-') + comparison_term = identifier | value + condition = pp.Group(comparison_term + operator + comparison_term) + _condition_expression = pp.operatorPrecedence( + condition, [ + ('and', 2, pp.opAssoc.LEFT, ), + ('or', 2, pp.opAssoc.LEFT, ), + ]) + return _condition_expression + + +def _evaluate(parse_results, context): + if not isinstance(parse_results, list): + if parse_results.startswith('$'): + # get variable from context + return str(context.get(parse_results[1:], '')) + # return literal value + return parse_results + + # recursion + assert len(parse_results) == 3 + + # handle logical operators + if parse_results[1] == 'and': + return _evaluate(parse_results[0], context) and \ + _evaluate(parse_results[2], context) + if parse_results[1] == 'or': + return _evaluate(parse_results[0], context) or \ + _evaluate(parse_results[2], context) + + # handle comparison operators + assert parse_results[1] in ('==', '!=') + if parse_results[1] == '==': + return _evaluate(parse_results[0], context) == \ + _evaluate(parse_results[2], context) + if parse_results[1] == '!=': + return _evaluate(parse_results[0], context) != \ + _evaluate(parse_results[2], context) diff --git a/ament_package/dependency.py b/ament_package/dependency.py index 9f43463..884fa0b 100644 --- a/ament_package/dependency.py +++ b/ament_package/dependency.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ament_package.condition import evaluate_condition + class Dependency: __slots__ = [ @@ -21,9 +23,12 @@ class Dependency: 'version_eq', 'version_gte', 'version_gt', + 'condition', + 'evaluated_condition', ] def __init__(self, name, **kwargs): + self.evaluated_condition = None for attr in self.__slots__: value = kwargs[attr] if attr in kwargs else None setattr(self, attr, value) @@ -41,3 +46,18 @@ def __eq__(self, other): def __str__(self): return self.name + + def evaluate_condition(self, context): + """ + Evaluate the condition. + + The result is also stored in the member variable `evaluated_condition`. + + :param context: A dictionary with key value pairs to replace variables + starting with $ in the condition. + + :returns: True if the condition evaluates to True, else False + :raises: :exc:`ValueError` if the condition fails to parse + """ + self.evaluated_condition = evaluate_condition(self.condition, context) + return self.evaluated_condition diff --git a/ament_package/group_dependency.py b/ament_package/group_dependency.py index 42edbcf..0e7838d 100644 --- a/ament_package/group_dependency.py +++ b/ament_package/group_dependency.py @@ -12,16 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ament_package.condition import evaluate_condition + class GroupDependency: __slots__ = [ 'name', + 'condition', + 'evaluated_condition', 'members', ] - def __init__(self, name, members=None): + def __init__(self, name, condition=None, members=None): self.name = name + self.condition = condition self.members = members + self.evaluated_condition = None def __eq__(self, other): if not isinstance(other, GroupDependency): @@ -32,8 +38,23 @@ def __eq__(self, other): def __str__(self): return self.name + def evaluate_condition(self, context): + """ + Evaluate the condition. + + The result is also stored in the member variable `evaluated_condition`. + + :param context: A dictionary with key value pairs to replace variables + starting with $ in the condition. + + :returns: True if the condition evaluates to True, else False + :raises: :exc:`ValueError` if the condition fails to parse + """ + self.evaluated_condition = evaluate_condition(self.condition, context) + return self.evaluated_condition + def extract_group_members(self, packages): self.members = set() for pkg in packages: - if self.name in pkg.member_of_groups: + if self.name in [g.name for g in pkg.member_of_groups]: self.members.add(pkg.name) diff --git a/ament_package/group_membership.py b/ament_package/group_membership.py new file mode 100644 index 0000000..6aec2ec --- /dev/null +++ b/ament_package/group_membership.py @@ -0,0 +1,52 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_package.condition import evaluate_condition + + +class GroupMembership: + __slots__ = [ + 'name', + 'condition', + 'evaluated_condition', + ] + + def __init__(self, name, condition=None): + self.name = name + self.condition = condition + self.evaluated_condition = None + + def __eq__(self, other): + if not isinstance(other, GroupMembership): + return False + return all(getattr(self, attr) == getattr(other, attr) + for attr in self.__slots__) + + def __str__(self): + return self.name + + def evaluate_condition(self, context): + """ + Evaluate the condition. + + The result is also stored in the member variable `evaluated_condition`. + + :param context: A dictionary with key value pairs to replace variables + starting with $ in the condition. + + :returns: True if the condition evaluates to True, else False + :raises: :exc:`ValueError` if the condition fails to parse + """ + self.evaluated_condition = evaluate_condition(self.condition, context) + return self.evaluated_condition diff --git a/ament_package/package.py b/ament_package/package.py index 779cf2a..1c23f5e 100644 --- a/ament_package/package.py +++ b/ament_package/package.py @@ -101,6 +101,32 @@ def get_build_type(self): return build_type_exports[0] raise InvalidPackage('Only one element is permitted.') + def evaluate_conditions(self, context): + """ + Evaluate the conditions of all dependencies and memberships. + + :param context: A dictionary with key value pairs to replace variables + starting with $ in the condition. + + :raises: :exc:`ValueError` if any condition fails to parse + """ + for attr in ( + 'build_depends', + 'buildtool_depends', + 'build_export_depends', + 'buildtool_export_depends', + 'exec_depends', + 'test_depends', + 'doc_depends', + 'conflicts', + 'replaces', + 'group_depends', + 'member_of_groups', + ): + conditionals = getattr(self, attr) + for conditional in conditionals: + conditional.evaluate_condition(context) + def validate(self): """ Ensure that all standards for packages are met. @@ -179,7 +205,10 @@ def validate(self): "The package must not '%s_depend' on a package with " 'the same name as this package' % dep_type) - if {d.name for d in self.group_depends} & set(self.member_of_groups): + if ( + {d.name for d in self.group_depends} & + {g.name for g in self.member_of_groups} + ): errors.append( "The package must not 'group_depend' on a package which it " 'also declares to be a member of') diff --git a/test/test_package.py b/test/test_package.py index 7371209..c0b6eb7 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -74,13 +74,18 @@ def test_init_dependency(self): version_lte=2, version_eq=3, version_gte=4, - version_gt=5) + version_gt=5, + condition='$foo == 23 and $bar != 42') self.assertEqual('foo', dep.name) self.assertEqual(1, dep.version_lt) self.assertEqual(2, dep.version_lte) self.assertEqual(3, dep.version_eq) self.assertEqual(4, dep.version_gte) self.assertEqual(5, dep.version_gt) + self.assertFalse(dep.evaluate_condition({'foo': 23, 'bar': 42})) + self.assertFalse(dep.evaluated_condition) + self.assertTrue(dep.evaluate_condition({'foo': 23, 'bar': 43})) + self.assertTrue(dep.evaluated_condition) self.assertRaises(TypeError, Dependency, 'foo', unknownattribute=42) def test_init_kwargs_string(self):