Skip to content

Commit 7d3d457

Browse files
authored
Fix set filters to use set operations (ansible#81639)
* Fix set filters to use set operations * Fix integration tests * Update filter documentation
1 parent b1b029c commit 7d3d457

File tree

8 files changed

+102
-79
lines changed

8 files changed

+102
-79
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
bugfixes:
2+
- Set filters ``intersect``, ``difference``, ``symmetric_difference`` and ``union`` now use set operations when the given items are hashable.
3+
Previously, list operations were performed unless the inputs were a hashable type such as ``str``, instead of a collection, such as a ``list`` or ``tuple``.
4+
- Set filters ``intersect``, ``difference``, ``symmetric_difference`` and ``union`` now always return a ``list``, never a ``set``.
5+
Previously, a ``set`` would be returned if the inputs were a hashable type such as ``str``, instead of a collection, such as a ``list`` or ``tuple``.
6+
minor_changes:
7+
- Documentation for set filters ``intersect``, ``difference``, ``symmetric_difference`` and ``union`` now states
8+
that the returned list items are in arbitrary order.

lib/ansible/plugins/filter/difference.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DOCUMENTATION:
55
short_description: the difference of one list from another
66
description:
77
- Provide a unique list of all the elements of the first list that do not appear in the second one.
8+
- Items in the resulting list are returned in arbitrary order.
89
options:
910
_input:
1011
description: A list.

lib/ansible/plugins/filter/intersect.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DOCUMENTATION:
55
short_description: intersection of lists
66
description:
77
- Provide a list with the common elements from other lists.
8+
- Items in the resulting list are returned in arbitrary order.
89
options:
910
_input:
1011
description: A list.

lib/ansible/plugins/filter/mathstuff.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@
1818
# You should have received a copy of the GNU General Public License
1919
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
2020

21-
# Make coding more python3-ish
22-
from __future__ import (absolute_import, division, print_function)
23-
__metaclass__ = type
21+
from __future__ import annotations
2422

2523
import itertools
2624
import math
2725

28-
from collections.abc import Hashable, Mapping, Iterable
26+
from collections.abc import Mapping, Iterable
2927

3028
from jinja2.filters import pass_environment
3129

@@ -84,37 +82,37 @@ def _do_fail(e):
8482

8583
@pass_environment
8684
def intersect(environment, a, b):
87-
if isinstance(a, Hashable) and isinstance(b, Hashable):
88-
c = set(a) & set(b)
89-
else:
85+
try:
86+
c = list(set(a) & set(b))
87+
except TypeError:
9088
c = unique(environment, [x for x in a if x in b], True)
9189
return c
9290

9391

9492
@pass_environment
9593
def difference(environment, a, b):
96-
if isinstance(a, Hashable) and isinstance(b, Hashable):
97-
c = set(a) - set(b)
98-
else:
94+
try:
95+
c = list(set(a) - set(b))
96+
except TypeError:
9997
c = unique(environment, [x for x in a if x not in b], True)
10098
return c
10199

102100

103101
@pass_environment
104102
def symmetric_difference(environment, a, b):
105-
if isinstance(a, Hashable) and isinstance(b, Hashable):
106-
c = set(a) ^ set(b)
107-
else:
103+
try:
104+
c = list(set(a) ^ set(b))
105+
except TypeError:
108106
isect = intersect(environment, a, b)
109107
c = [x for x in union(environment, a, b) if x not in isect]
110108
return c
111109

112110

113111
@pass_environment
114112
def union(environment, a, b):
115-
if isinstance(a, Hashable) and isinstance(b, Hashable):
116-
c = set(a) | set(b)
117-
else:
113+
try:
114+
c = list(set(a) | set(b))
115+
except TypeError:
118116
c = unique(environment, a + b, True)
119117
return c
120118

lib/ansible/plugins/filter/symmetric_difference.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DOCUMENTATION:
55
short_description: different items from two lists
66
description:
77
- Provide a unique list of all the elements unique to each list.
8+
- Items in the resulting list are returned in arbitrary order.
89
options:
910
_input:
1011
description: A list.

lib/ansible/plugins/filter/union.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DOCUMENTATION:
55
short_description: union of lists
66
description:
77
- Provide a unique list of all the elements of two lists.
8+
- Items in the resulting list are returned in arbitrary order.
89
options:
910
_input:
1011
description: A list.

test/integration/targets/filter_mathstuff/tasks/main.yml

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,44 +64,44 @@
6464
that:
6565
- '[1,2,3]|intersect([4,5,6]) == []'
6666
- '[1,2,3]|intersect([3,4,5,6]) == [3]'
67-
- '[1,2,3]|intersect([3,2,1]) == [1,2,3]'
68-
- '(1,2,3)|intersect((4,5,6))|list == []'
69-
- '(1,2,3)|intersect((3,4,5,6))|list == [3]'
67+
- '[1,2,3]|intersect([3,2,1]) | sort == [1,2,3]'
68+
- '(1,2,3)|intersect((4,5,6)) == []'
69+
- '(1,2,3)|intersect((3,4,5,6)) == [3]'
7070
- '["a","A","b"]|intersect(["B","c","C"]) == []'
7171
- '["a","A","b"]|intersect(["b","B","c","C"]) == ["b"]'
72-
- '["a","A","b"]|intersect(["b","A","a"]) == ["a","A","b"]'
73-
- '("a","A","b")|intersect(("B","c","C"))|list == []'
74-
- '("a","A","b")|intersect(("b","B","c","C"))|list == ["b"]'
72+
- '["a","A","b"]|intersect(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]'
73+
- '("a","A","b")|intersect(("B","c","C")) == []'
74+
- '("a","A","b")|intersect(("b","B","c","C")) == ["b"]'
7575

7676
- name: Verify difference
7777
tags: difference
7878
assert:
7979
that:
80-
- '[1,2,3]|difference([4,5,6]) == [1,2,3]'
81-
- '[1,2,3]|difference([3,4,5,6]) == [1,2]'
80+
- '[1,2,3]|difference([4,5,6]) | sort == [1,2,3]'
81+
- '[1,2,3]|difference([3,4,5,6]) | sort == [1,2]'
8282
- '[1,2,3]|difference([3,2,1]) == []'
83-
- '(1,2,3)|difference((4,5,6))|list == [1,2,3]'
84-
- '(1,2,3)|difference((3,4,5,6))|list == [1,2]'
85-
- '["a","A","b"]|difference(["B","c","C"]) == ["a","A","b"]'
86-
- '["a","A","b"]|difference(["b","B","c","C"]) == ["a","A"]'
83+
- '(1,2,3)|difference((4,5,6)) | sort == [1,2,3]'
84+
- '(1,2,3)|difference((3,4,5,6)) | sort == [1,2]'
85+
- '["a","A","b"]|difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","a","b"]'
86+
- '["a","A","b"]|difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","a"]'
8787
- '["a","A","b"]|difference(["b","A","a"]) == []'
88-
- '("a","A","b")|difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","a","b"]'
89-
- '("a","A","b")|difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","a"]'
88+
- '("a","A","b")|difference(("B","c","C")) | sort(case_sensitive=True) == ["A","a","b"]'
89+
- '("a","A","b")|difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","a"]'
9090

9191
- name: Verify symmetric_difference
9292
tags: symmetric_difference
9393
assert:
9494
that:
95-
- '[1,2,3]|symmetric_difference([4,5,6]) == [1,2,3,4,5,6]'
96-
- '[1,2,3]|symmetric_difference([3,4,5,6]) == [1,2,4,5,6]'
95+
- '[1,2,3]|symmetric_difference([4,5,6]) | sort == [1,2,3,4,5,6]'
96+
- '[1,2,3]|symmetric_difference([3,4,5,6]) | sort == [1,2,4,5,6]'
9797
- '[1,2,3]|symmetric_difference([3,2,1]) == []'
98-
- '(1,2,3)|symmetric_difference((4,5,6))|list == [1,2,3,4,5,6]'
99-
- '(1,2,3)|symmetric_difference((3,4,5,6))|list == [1,2,4,5,6]'
100-
- '["a","A","b"]|symmetric_difference(["B","c","C"]) == ["a","A","b","B","c","C"]'
101-
- '["a","A","b"]|symmetric_difference(["b","B","c","C"]) == ["a","A","B","c","C"]'
98+
- '(1,2,3)|symmetric_difference((4,5,6)) | sort == [1,2,3,4,5,6]'
99+
- '(1,2,3)|symmetric_difference((3,4,5,6)) | sort == [1,2,4,5,6]'
100+
- '["a","A","b"]|symmetric_difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
101+
- '["a","A","b"]|symmetric_difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","c"]'
102102
- '["a","A","b"]|symmetric_difference(["b","A","a"]) == []'
103-
- '("a","A","b")|symmetric_difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
104-
- '("a","A","b")|symmetric_difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","c"]'
103+
- '("a","A","b")|symmetric_difference(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
104+
- '("a","A","b")|symmetric_difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","c"]'
105105

106106
- name: Verify union
107107
tags: union
@@ -112,11 +112,11 @@
112112
- '[1,2,3]|union([3,2,1]) == [1,2,3]'
113113
- '(1,2,3)|union((4,5,6))|list == [1,2,3,4,5,6]'
114114
- '(1,2,3)|union((3,4,5,6))|list == [1,2,3,4,5,6]'
115-
- '["a","A","b"]|union(["B","c","C"]) == ["a","A","b","B","c","C"]'
116-
- '["a","A","b"]|union(["b","B","c","C"]) == ["a","A","b","B","c","C"]'
117-
- '["a","A","b"]|union(["b","A","a"]) == ["a","A","b"]'
118-
- '("a","A","b")|union(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
119-
- '("a","A","b")|union(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
115+
- '["a","A","b"]|union(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
116+
- '["a","A","b"]|union(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
117+
- '["a","A","b"]|union(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]'
118+
- '("a","A","b")|union(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
119+
- '("a","A","b")|union(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
120120

121121
- name: Verify min
122122
tags: min

test/units/plugins/filter/test_mathstuff.py

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# Copyright: (c) 2017, Ansible Project
22
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
33

4-
# Make coding more python3-ish
5-
from __future__ import (absolute_import, division, print_function)
6-
__metaclass__ = type
4+
from __future__ import annotations
5+
76
import pytest
87

98
from jinja2 import Environment
@@ -12,54 +11,68 @@
1211
from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
1312

1413

15-
UNIQUE_DATA = (([1, 3, 4, 2], [1, 3, 4, 2]),
16-
([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]),
17-
(['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']),
18-
(['a', 'a', 'd', 'b', 'a', 'd', 'c', 'b'], ['a', 'd', 'b', 'c']),
19-
)
14+
UNIQUE_DATA = [
15+
([], []),
16+
([1, 3, 4, 2], [1, 3, 4, 2]),
17+
([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]),
18+
([1, 2, 3, 4], [1, 2, 3, 4]),
19+
([1, 1, 4, 2, 1, 4, 3, 2], [1, 4, 2, 3]),
20+
]
21+
22+
TWO_SETS_DATA = [
23+
([], [], ([], [], [])),
24+
([1, 2], [1, 2], ([1, 2], [], [])),
25+
([1, 2], [3, 4], ([], [1, 2], [1, 2, 3, 4])),
26+
([1, 2, 3], [5, 3, 4], ([3], [1, 2], [1, 2, 5, 4])),
27+
([1, 2, 3], [4, 3, 5], ([3], [1, 2], [1, 2, 4, 5])),
28+
]
29+
30+
31+
def dict_values(values: list[int]) -> list[dict[str, int]]:
32+
"""Return a list of non-hashable values derived from the given list."""
33+
return [dict(x=value) for value in values]
34+
35+
36+
for _data, _expected in list(UNIQUE_DATA):
37+
UNIQUE_DATA.append((dict_values(_data), dict_values(_expected)))
38+
39+
for _dataset1, _dataset2, _expected in list(TWO_SETS_DATA):
40+
TWO_SETS_DATA.append((dict_values(_dataset1), dict_values(_dataset2), tuple(dict_values(answer) for answer in _expected)))
2041

21-
TWO_SETS_DATA = (([1, 2], [3, 4], ([], sorted([1, 2]), sorted([1, 2, 3, 4]), sorted([1, 2, 3, 4]))),
22-
([1, 2, 3], [5, 3, 4], ([3], sorted([1, 2]), sorted([1, 2, 5, 4]), sorted([1, 2, 3, 4, 5]))),
23-
(['a', 'b', 'c'], ['d', 'c', 'e'], (['c'], sorted(['a', 'b']), sorted(['a', 'b', 'd', 'e']), sorted(['a', 'b', 'c', 'e', 'd']))),
24-
)
2542

2643
env = Environment()
2744

2845

29-
@pytest.mark.parametrize('data, expected', UNIQUE_DATA)
30-
class TestUnique:
31-
def test_unhashable(self, data, expected):
32-
assert ms.unique(env, list(data)) == expected
46+
def assert_lists_contain_same_elements(a, b) -> None:
47+
"""Assert that the two values given are lists that contain the same elements, even when the elements cannot be sorted or hashed."""
48+
assert isinstance(a, list)
49+
assert isinstance(b, list)
3350

34-
def test_hashable(self, data, expected):
35-
assert ms.unique(env, tuple(data)) == expected
51+
missing_from_a = [item for item in b if item not in a]
52+
missing_from_b = [item for item in a if item not in b]
3653

54+
assert not missing_from_a, f'elements from `b` {missing_from_a} missing from `a` {a}'
55+
assert not missing_from_b, f'elements from `a` {missing_from_b} missing from `b` {b}'
3756

38-
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
39-
class TestIntersect:
40-
def test_unhashable(self, dataset1, dataset2, expected):
41-
assert sorted(ms.intersect(env, list(dataset1), list(dataset2))) == expected[0]
4257

43-
def test_hashable(self, dataset1, dataset2, expected):
44-
assert sorted(ms.intersect(env, tuple(dataset1), tuple(dataset2))) == expected[0]
58+
@pytest.mark.parametrize('data, expected', UNIQUE_DATA, ids=str)
59+
def test_unique(data, expected):
60+
assert_lists_contain_same_elements(ms.unique(env, data), expected)
4561

4662

47-
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
48-
class TestDifference:
49-
def test_unhashable(self, dataset1, dataset2, expected):
50-
assert sorted(ms.difference(env, list(dataset1), list(dataset2))) == expected[1]
63+
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
64+
def test_intersect(dataset1, dataset2, expected):
65+
assert_lists_contain_same_elements(ms.intersect(env, dataset1, dataset2), expected[0])
5166

52-
def test_hashable(self, dataset1, dataset2, expected):
53-
assert sorted(ms.difference(env, tuple(dataset1), tuple(dataset2))) == expected[1]
5467

68+
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
69+
def test_difference(dataset1, dataset2, expected):
70+
assert_lists_contain_same_elements(ms.difference(env, dataset1, dataset2), expected[1])
5571

56-
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
57-
class TestSymmetricDifference:
58-
def test_unhashable(self, dataset1, dataset2, expected):
59-
assert sorted(ms.symmetric_difference(env, list(dataset1), list(dataset2))) == expected[2]
6072

61-
def test_hashable(self, dataset1, dataset2, expected):
62-
assert sorted(ms.symmetric_difference(env, tuple(dataset1), tuple(dataset2))) == expected[2]
73+
@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
74+
def test_symmetric_difference(dataset1, dataset2, expected):
75+
assert_lists_contain_same_elements(ms.symmetric_difference(env, dataset1, dataset2), expected[2])
6376

6477

6578
class TestLogarithm:

0 commit comments

Comments
 (0)