From bc203115b13ed689a7d8626de81093c7b6071c83 Mon Sep 17 00:00:00 2001 From: Doug Wiegley Date: Sat, 16 Feb 2019 11:29:02 -0700 Subject: [PATCH] Allow lists to be generated from any non-string iterable The type check on the list field requires a list, though many objects can be trivially iterated as lists, like sqlalchemy lazy query objects. Relax the check slightly to fit a broader range of "lists". Change-Id: I514a0e6d445e9a14e3c0279b4e9bebfa0940337d --- oslo_versionedobjects/fields.py | 5 ++- oslo_versionedobjects/tests/test_fields.py | 38 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/oslo_versionedobjects/fields.py b/oslo_versionedobjects/fields.py index 8073e70b..8753f3f1 100644 --- a/oslo_versionedobjects/fields.py +++ b/oslo_versionedobjects/fields.py @@ -13,6 +13,7 @@ # under the License. import abc +import collections import datetime from distutils import versionpredicate import re @@ -642,7 +643,9 @@ class CompoundFieldType(FieldType): class List(CompoundFieldType): def coerce(self, obj, attr, value): - if not isinstance(value, list): + + if (not isinstance(value, collections.Iterable) or + isinstance(value, six.string_types + (collections.Mapping,))): raise ValueError(_('A list is required in field %(attr)s, ' 'not a %(type)s') % {'attr': attr, 'type': type(value).__name__}) diff --git a/oslo_versionedobjects/tests/test_fields.py b/oslo_versionedobjects/tests/test_fields.py index 153ad677..56561aa9 100644 --- a/oslo_versionedobjects/tests/test_fields.py +++ b/oslo_versionedobjects/tests/test_fields.py @@ -1229,3 +1229,41 @@ class TestIPV6Network(TestField): invalid_vals = [x for x in self.coerce_bad_values] for invalid_val in invalid_vals: self.assertNotRegex(str(invalid_val), pattern) + + +class FakeCounter(six.Iterator): + def __init__(self): + self.n = 0 + + def __iter__(self): + return self + + def __next__(self): + if self.n <= 4: + self.n += 1 + return self.n + else: + raise StopIteration + + +class TestListTypes(test.TestCase): + + def test_regular_list(self): + fields.List(fields.Integer).coerce(None, None, [1, 2]) + + def test_non_iterable(self): + self.assertRaises(ValueError, + fields.List(fields.Integer).coerce, None, None, 2) + + def test_string_iterable(self): + self.assertRaises(ValueError, + fields.List(fields.Integer).coerce, None, None, + 'hello') + + def test_mapping_iterable(self): + self.assertRaises(ValueError, + fields.List(fields.Integer).coerce, None, None, + {'a': 1, 'b': 2}) + + def test_iter_class(self): + fields.List(fields.Integer).coerce(None, None, FakeCounter())