From bbf31b05bc9dc6ee68484a39d723d7ad2aab29fe Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Thu, 15 Apr 2021 16:39:38 +0200 Subject: [PATCH 01/13] add NoDuplicates new validator --- AUTHORS.rst | 1 + src/marshmallow/validate.py | 44 +++++++++++++++++++++++++++++++++++++ tests/test_validate.py | 36 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1dee23ddc..0065af836 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -161,3 +161,4 @@ Contributors (chronological) - Stephen Eaton `@madeinoz67 `_ - Antonio Lassandro `@lassandroan `_ - Javier Fernández `@jfernandz `_ +- Midokura `@midokura `_ diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 9c637fc92..09d87e872 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -644,3 +644,47 @@ def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]: if val in self.iterable: raise ValidationError(self._format_error(value)) return value + + +class NoDuplicates(Validator): + """Validator which succeeds if the ``value`` is an ``iterable`` and has no duplicate + elements. In case of a list of objects, it can easy check an internal + attribute by passing the ``attribute`` parameter. + Validator which fails if ``value`` is not a member of ``iterable``. + + :param str attribute: The name of the attribute of the object you want to check. + """ + + default_message = "Invalid input. Supported lists or str." + error = "Found a duplicate value: {value}." + attribute_error = "Found a duplicate object attribute ({attribute}): {value}." + + def __init__(self, attribute: typing.Optional[str] = None): + self.attribute = attribute + + def _repr_args(self) -> str: + return "attribute={!r}".format(self.attribute) + + def _format_error(self, value) -> str: + if self.attribute: + return self.attribute_error.format(attribute=self.attribute, value=value) + return self.error.format(value=value) + + def __call__(self, value): + set_item = set() + try: + for item in value: + if self.attribute: + attribute = getattr(item, self.attribute) + if attribute in set_item: + raise ValidationError(self._format_error(attribute)) + set_item.add(attribute) + else: + if item in set_item: + raise ValidationError(self._format_error(item)) + set_item.add(item) + + except TypeError as error: + raise ValidationError(self.default_message) from error + + return value \ No newline at end of file diff --git a/tests/test_validate.py b/tests/test_validate.py index 0dc70c1e3..85018986b 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -912,3 +912,39 @@ def test_and(): errors = excinfo.value.messages assert errors == ["Not an even value.", "Must be less than or equal to 6."] + + +def test_noduplicates(): + class Mock: + def __init__(self, name): + self.name = name + + mock_object_1 = Mock("a") + mock_object_2 = Mock("b") + + assert validate.NoDuplicates()("d") == "d" + assert validate.NoDuplicates()([]) == [] + assert validate.NoDuplicates()({}) == {} + assert validate.NoDuplicates()(["a", "b"]) == ["a", "b"] + assert validate.NoDuplicates()([1, 2]) == [1, 2] + assert validate.NoDuplicates(attribute="name")([mock_object_1, mock_object_2]) == [ + mock_object_1, + mock_object_2, + ] + + with pytest.raises(ValidationError, match="Invalid input."): + validate.NoDuplicates()(3) + with pytest.raises(ValidationError, match="Invalid input."): + validate.NoDuplicates()(1.1) + with pytest.raises(ValidationError, match="Invalid input."): + validate.NoDuplicates()(True) + with pytest.raises(ValidationError, match="Invalid input."): + validate.NoDuplicates()(None) + with pytest.raises(ValidationError, match="Found a duplicate value: 1."): + validate.NoDuplicates()([1, 1, 2]) + with pytest.raises(ValidationError, match="Found a duplicate value: a."): + validate.NoDuplicates()("aab") + with pytest.raises(ValidationError, match="Found a duplicate value: a."): + validate.NoDuplicates()(["a", "a", "b"]) + with pytest.raises(ValidationError, match="Found a duplicate object attribute"): + validate.NoDuplicates(attribute="name")([mock_object_1, mock_object_1]) From fe5b613575159aae58669aa8c0d631732fa58b92 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Thu, 15 Apr 2021 16:46:44 +0200 Subject: [PATCH 02/13] add new line --- src/marshmallow/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 09d87e872..30ad958e7 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -687,4 +687,4 @@ def __call__(self, value): except TypeError as error: raise ValidationError(self.default_message) from error - return value \ No newline at end of file + return value From aa82f30022d4f56139a1624f6fc7aa8a7ab04fa0 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Thu, 15 Apr 2021 17:06:52 +0200 Subject: [PATCH 03/13] add feature to CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 261ba45a7..4d939a08d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Features: - Add ``validate.And`` (:issue:`1768`). Thanks :user:`rugleb` for the suggestion. - Let ``Field``s be accessed by name as ``Schema`` attributes (:pr:`1631`). +- Add a `NoDuplicates` validator in ``marshmallow.validate`` (:pr:`1793`). Other changes: From 1114b9740c7119b0b14d4647f7d230c7617875c5 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Mon, 19 Apr 2021 17:21:39 +0200 Subject: [PATCH 04/13] using Unique instead of NoDuplicates --- CHANGELOG.rst | 2 +- src/marshmallow/validate.py | 4 ++-- tests/test_validate.py | 31 ++++++++++++++++--------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d939a08d..1f312eea8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,7 +9,7 @@ Features: - Add ``validate.And`` (:issue:`1768`). Thanks :user:`rugleb` for the suggestion. - Let ``Field``s be accessed by name as ``Schema`` attributes (:pr:`1631`). -- Add a `NoDuplicates` validator in ``marshmallow.validate`` (:pr:`1793`). +- Add a `Unique` validator in ``marshmallow.validate`` (:pr:`1793`). Other changes: diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 30ad958e7..89cbc6f3d 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -646,8 +646,8 @@ def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]: return value -class NoDuplicates(Validator): - """Validator which succeeds if the ``value`` is an ``iterable`` and has no duplicate +class Unique(Validator): + """Validator which succeeds if the ``value`` is an ``iterable`` and has unique elements. In case of a list of objects, it can easy check an internal attribute by passing the ``attribute`` parameter. Validator which fails if ``value`` is not a member of ``iterable``. diff --git a/tests/test_validate.py b/tests/test_validate.py index 85018986b..7cf5ad556 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -914,7 +914,7 @@ def test_and(): assert errors == ["Not an even value.", "Must be less than or equal to 6."] -def test_noduplicates(): +def test_contains_unique(): class Mock: def __init__(self, name): self.name = name @@ -922,29 +922,30 @@ def __init__(self, name): mock_object_1 = Mock("a") mock_object_2 = Mock("b") - assert validate.NoDuplicates()("d") == "d" - assert validate.NoDuplicates()([]) == [] - assert validate.NoDuplicates()({}) == {} - assert validate.NoDuplicates()(["a", "b"]) == ["a", "b"] - assert validate.NoDuplicates()([1, 2]) == [1, 2] - assert validate.NoDuplicates(attribute="name")([mock_object_1, mock_object_2]) == [ + assert validate.Unique()("d") == "d" + assert validate.Unique()([]) == [] + assert validate.Unique()({}) == {} + assert validate.Unique()(["a", "b"]) == ["a", "b"] + assert validate.Unique()([1, 2]) == [1, 2] + assert validate.Unique(attribute="name")([mock_object_1, mock_object_2]) == [ mock_object_1, mock_object_2, ] with pytest.raises(ValidationError, match="Invalid input."): - validate.NoDuplicates()(3) + validate.Unique()(3) with pytest.raises(ValidationError, match="Invalid input."): - validate.NoDuplicates()(1.1) + validate.Unique()(1.1) with pytest.raises(ValidationError, match="Invalid input."): - validate.NoDuplicates()(True) + validate.Unique()(True) with pytest.raises(ValidationError, match="Invalid input."): - validate.NoDuplicates()(None) + validate.Unique()(None) with pytest.raises(ValidationError, match="Found a duplicate value: 1."): - validate.NoDuplicates()([1, 1, 2]) + validate.Unique()([1, 1, 2]) with pytest.raises(ValidationError, match="Found a duplicate value: a."): - validate.NoDuplicates()("aab") + validate.Unique()("aab") with pytest.raises(ValidationError, match="Found a duplicate value: a."): - validate.NoDuplicates()(["a", "a", "b"]) + validate.Unique()(["a", "a", "b"]) with pytest.raises(ValidationError, match="Found a duplicate object attribute"): - validate.NoDuplicates(attribute="name")([mock_object_1, mock_object_1]) + validate.Unique(attribute="name")([mock_object_1, mock_object_1]) + From a29991eafe3d36ef78082f57461d6ce1a8ba1439 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Mon, 19 Apr 2021 17:41:28 +0200 Subject: [PATCH 05/13] simplify logic --- src/marshmallow/validate.py | 15 +++++---------- tests/test_validate.py | 1 - 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 89cbc6f3d..a156ff0d7 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -671,18 +671,13 @@ def _format_error(self, value) -> str: return self.error.format(value=value) def __call__(self, value): - set_item = set() + used = set() try: for item in value: - if self.attribute: - attribute = getattr(item, self.attribute) - if attribute in set_item: - raise ValidationError(self._format_error(attribute)) - set_item.add(attribute) - else: - if item in set_item: - raise ValidationError(self._format_error(item)) - set_item.add(item) + _id = getattr(item, self.attribute) if self.attribute else item + if _id in used: + raise ValidationError(self._format_error(_id)) + used.add(_id) except TypeError as error: raise ValidationError(self.default_message) from error diff --git a/tests/test_validate.py b/tests/test_validate.py index 7cf5ad556..03439a376 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -948,4 +948,3 @@ def __init__(self, name): validate.Unique()(["a", "a", "b"]) with pytest.raises(ValidationError, match="Found a duplicate object attribute"): validate.Unique(attribute="name")([mock_object_1, mock_object_1]) - From 890b74f0d6e3c87e2df60df968633a79f4579667 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Mon, 19 Apr 2021 18:06:15 +0200 Subject: [PATCH 06/13] check for hashable and equal --- src/marshmallow/validate.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index a156ff0d7..4fdd91e30 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -2,6 +2,7 @@ import re import typing from abc import ABC, abstractmethod +from collections import Iterable from itertools import zip_longest from operator import attrgetter @@ -671,15 +672,28 @@ def _format_error(self, value) -> str: return self.error.format(value=value) def __call__(self, value): - used = set() + if not isinstance(value, Iterable): + raise ValidationError(self.default_message) + ids = [ + getattr(item, self.attribute) if self.attribute else item for item in value + ] try: - for item in value: - _id = getattr(item, self.attribute) if self.attribute else item - if _id in used: - raise ValidationError(self._format_error(_id)) - used.add(_id) - - except TypeError as error: - raise ValidationError(self.default_message) from error + self._duplicate_hash(ids) + except TypeError: + self._duplicate_equal(ids) return value + + def _duplicate_hash(self, ids): + used = set() + for _id in ids: + if _id in used: + raise ValidationError(self._format_error(_id)) + used.add(_id) + + def _duplicate_equal(self, ids): + used = [] + for _id in ids: + if _id in used: + raise ValidationError(self._format_error(_id)) + used.append(_id) From 68d57b20ed65e8a0dfbbc81092ecad36a61ce288 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 23 Apr 2021 09:35:18 +0200 Subject: [PATCH 07/13] using utils.get_value() --- src/marshmallow/validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 4fdd91e30..109ef8430 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -7,6 +7,7 @@ from operator import attrgetter from marshmallow import types +from marshmallow import utils from marshmallow.exceptions import ValidationError _T = typing.TypeVar("_T") @@ -675,7 +676,7 @@ def __call__(self, value): if not isinstance(value, Iterable): raise ValidationError(self.default_message) ids = [ - getattr(item, self.attribute) if self.attribute else item for item in value + utils.get_value(item, self.attribute) if self.attribute else item for item in value ] try: self._duplicate_hash(ids) From c366187dc401f54d1c223ccfb10dbd2049f5112c Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 23 Apr 2021 10:23:45 +0200 Subject: [PATCH 08/13] adding hashable tests --- tests/test_validate.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_validate.py b/tests/test_validate.py index 03439a376..08c4803e1 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -931,6 +931,9 @@ def __init__(self, name): mock_object_1, mock_object_2, ] + assert validate.Unique()([[1, 2], [3, 4]]) == [[1, 2], [3, 4]] + assert validate.Unique()([{1, 2}, {3, 4}]) == [{1, 2}, {3, 4}] + assert validate.Unique()([{"a": 1}, {"b": 2}]) == [{"a": 1}, {"b": 2}] with pytest.raises(ValidationError, match="Invalid input."): validate.Unique()(3) @@ -940,11 +943,18 @@ def __init__(self, name): validate.Unique()(True) with pytest.raises(ValidationError, match="Invalid input."): validate.Unique()(None) - with pytest.raises(ValidationError, match="Found a duplicate value: 1."): + with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()([1, 1, 2]) - with pytest.raises(ValidationError, match="Found a duplicate value: a."): + with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()("aab") - with pytest.raises(ValidationError, match="Found a duplicate value: a."): + with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()(["a", "a", "b"]) with pytest.raises(ValidationError, match="Found a duplicate object attribute"): validate.Unique(attribute="name")([mock_object_1, mock_object_1]) + with pytest.raises(ValidationError, match="Found a duplicate value"): + validate.Unique()([[1, 2], [1, 2]]) + with pytest.raises(ValidationError, match="Found a duplicate value"): + validate.Unique()([{1, 2}, {1, 2}]) + with pytest.raises(ValidationError, match="Found a duplicate value"): + validate.Unique()([{"a": 1, "b": 2}, {"a": 1, "b": 2}]) + From 53cc047f0efed92c5aa2bc0090459943a190b1b9 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 23 Apr 2021 10:28:03 +0200 Subject: [PATCH 09/13] improving typing --- src/marshmallow/validate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 109ef8430..88af9f592 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -2,7 +2,6 @@ import re import typing from abc import ABC, abstractmethod -from collections import Iterable from itertools import zip_longest from operator import attrgetter @@ -672,8 +671,8 @@ def _format_error(self, value) -> str: return self.attribute_error.format(attribute=self.attribute, value=value) return self.error.format(value=value) - def __call__(self, value): - if not isinstance(value, Iterable): + def __call__(self, value: typing.Iterable) -> typing.Iterable: + if not isinstance(value, typing.Iterable): raise ValidationError(self.default_message) ids = [ utils.get_value(item, self.attribute) if self.attribute else item for item in value @@ -685,14 +684,14 @@ def __call__(self, value): return value - def _duplicate_hash(self, ids): + def _duplicate_hash(self, ids: typing.List) -> None: used = set() for _id in ids: if _id in used: raise ValidationError(self._format_error(_id)) used.add(_id) - def _duplicate_equal(self, ids): + def _duplicate_equal(self, ids: typing.List) -> None: used = [] for _id in ids: if _id in used: From ca6dec06ea1a280cd39753cee1bf54fe2ab4a589 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 23 Apr 2021 10:28:38 +0200 Subject: [PATCH 10/13] pre-commit --- src/marshmallow/validate.py | 3 ++- tests/test_validate.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py index 88af9f592..143bc639a 100644 --- a/src/marshmallow/validate.py +++ b/src/marshmallow/validate.py @@ -675,7 +675,8 @@ def __call__(self, value: typing.Iterable) -> typing.Iterable: if not isinstance(value, typing.Iterable): raise ValidationError(self.default_message) ids = [ - utils.get_value(item, self.attribute) if self.attribute else item for item in value + utils.get_value(item, self.attribute) if self.attribute else item + for item in value ] try: self._duplicate_hash(ids) diff --git a/tests/test_validate.py b/tests/test_validate.py index 08c4803e1..7a4d35b1a 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -957,4 +957,3 @@ def __init__(self, name): validate.Unique()([{1, 2}, {1, 2}]) with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()([{"a": 1, "b": 2}, {"a": 1, "b": 2}]) - From 9df2b6624fef9d595153a507ca6c30d747d17ae4 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 30 Apr 2021 15:21:47 +0200 Subject: [PATCH 11/13] adding multilevel and dict unittests --- tests/test_validate.py | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/test_validate.py b/tests/test_validate.py index 7a4d35b1a..e8fc7e30b 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -915,21 +915,42 @@ def test_and(): def test_contains_unique(): + class Bar: + def __init__(self, num): + self.num = num + class Mock: - def __init__(self, name): + def __init__(self, name, bar): self.name = name + self.bar = bar - mock_object_1 = Mock("a") - mock_object_2 = Mock("b") + mock_object_a_1 = Mock("a", Bar(1)) + mock_object_a_2 = Mock("a", Bar(2)) + mock_object_b_1 = Mock("b", Bar(1)) + mock_dict_a_1 = {"name": "a", "bar": {"num": 1}} + mock_dict_a_2 = {"name": "a", "bar": {"num": 2}} + mock_dict_b_1 = {"name": "b", "bar": {"num": 1}} assert validate.Unique()("d") == "d" assert validate.Unique()([]) == [] assert validate.Unique()({}) == {} assert validate.Unique()(["a", "b"]) == ["a", "b"] assert validate.Unique()([1, 2]) == [1, 2] - assert validate.Unique(attribute="name")([mock_object_1, mock_object_2]) == [ - mock_object_1, - mock_object_2, + assert validate.Unique(attribute="name")([mock_object_a_1, mock_object_b_1]) == [ + mock_object_a_1, + mock_object_b_1, + ] + assert validate.Unique(attribute="bar.num")([mock_object_a_1, mock_object_a_2]) == [ + mock_object_a_1, + mock_object_a_2, + ] + assert validate.Unique(attribute="name")([mock_dict_a_1, mock_dict_b_1]) == [ + mock_dict_a_1, + mock_dict_b_1, + ] + assert validate.Unique(attribute="bar.num")([mock_dict_a_1, mock_dict_a_2]) == [ + mock_dict_a_1, + mock_dict_a_2, ] assert validate.Unique()([[1, 2], [3, 4]]) == [[1, 2], [3, 4]] assert validate.Unique()([{1, 2}, {3, 4}]) == [{1, 2}, {3, 4}] @@ -950,7 +971,13 @@ def __init__(self, name): with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()(["a", "a", "b"]) with pytest.raises(ValidationError, match="Found a duplicate object attribute"): - validate.Unique(attribute="name")([mock_object_1, mock_object_1]) + validate.Unique(attribute="name")([mock_object_a_1, mock_object_a_2]) + with pytest.raises(ValidationError, match="Found a duplicate object attribute"): + validate.Unique(attribute="bar.num")([mock_object_a_1, mock_object_b_1]) + with pytest.raises(ValidationError, match="Found a duplicate object attribute"): + validate.Unique(attribute="name")([mock_dict_a_1, mock_dict_a_2]) + with pytest.raises(ValidationError, match="Found a duplicate object attribute"): + validate.Unique(attribute="bar.num")([mock_dict_a_1, mock_dict_b_1]) with pytest.raises(ValidationError, match="Found a duplicate value"): validate.Unique()([[1, 2], [1, 2]]) with pytest.raises(ValidationError, match="Found a duplicate value"): From dae42a2418f29318f745cb01f3f059637a6d2a10 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Fri, 30 Apr 2021 15:22:40 +0200 Subject: [PATCH 12/13] rename test --- tests/test_validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_validate.py b/tests/test_validate.py index e8fc7e30b..e6dd99755 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -914,7 +914,7 @@ def test_and(): assert errors == ["Not an even value.", "Must be less than or equal to 6."] -def test_contains_unique(): +def test_unique(): class Bar: def __init__(self, num): self.num = num From 03006297d5d00d64198fc8a33e58aacd64c46f05 Mon Sep 17 00:00:00 2001 From: bonastreyair Date: Thu, 13 May 2021 16:10:20 +0200 Subject: [PATCH 13/13] adapt changelog format --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2281f29a8..f5aaf392e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,8 @@ Changelog Features: -- Add a `Unique` validator in ``marshmallow.validate`` (:pr:`1793`). +- Add ``validate.Unique`` (:pr:`1793`). + Thanks :user:`bonastreyair` for the PR. 3.12.1 (2021-05-10) *******************