diff options
| -rw-r--r-- | CHANGES.rst | 10 | ||||
| -rw-r--r-- | src/zope/schema/tests/test__field.py | 7 | ||||
| -rw-r--r-- | src/zope/schema/tests/test_vocabulary.py | 110 | ||||
| -rw-r--r-- | src/zope/schema/vocabulary.py | 89 |
4 files changed, 192 insertions, 24 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 624d87f..5eada5d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -97,6 +97,16 @@ - Make ``Iterable`` and ``Container`` properly implement ``IIterable`` and ``IContainer``, respectively. +- Make ``SimpleVocabulary.fromItems`` accept triples to allow + specifying the title of terms. See `issue 18 + <https://github.com/zopefoundation/zope.schema/issues/18>`_. + +- Make ``TreeVocabulary.fromDict`` only create + ``ITitledTokenizedTerms`` when a title is actually provided. + +- Make ``SimpleVocabulary`` and ``SimpleTerm`` have value-based + equality and hashing methods. + 4.5.0 (2017-07-10) ================== diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index cb0e127..0d12ad7 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -705,16 +705,13 @@ class ChoiceTests(EqualityTestsMixin, from zope.schema._field import Choice return Choice - from zope.schema.vocabulary import SimpleVocabulary - # SimpleVocabulary uses identity semantics for equality - _default_vocabulary = SimpleVocabulary.fromValues([1, 2, 3]) - def _makeOneFromClass(self, cls, *args, **kwargs): if (not args and 'vocabulary' not in kwargs and 'values' not in kwargs and 'source' not in kwargs): - kwargs['vocabulary'] = self._default_vocabulary + from zope.schema.vocabulary import SimpleVocabulary + kwargs['vocabulary'] = SimpleVocabulary.fromValues([1, 2, 3]) return super(ChoiceTests, self)._makeOneFromClass(cls, *args, **kwargs) def _getTargetInterface(self): diff --git a/src/zope/schema/tests/test_vocabulary.py b/src/zope/schema/tests/test_vocabulary.py index 86e8164..63904b6 100644 --- a/src/zope/schema/tests/test_vocabulary.py +++ b/src/zope/schema/tests/test_vocabulary.py @@ -66,6 +66,41 @@ class SimpleTermTests(unittest.TestCase): self.assertFalse(ITitledTokenizedTerm.providedBy(term)) + def test__eq__and__hash__(self): + from zope import interface + + term = self._makeOne('value') + # Equal to itself + self.assertEqual(term, term) + # Not equal to a different class + self.assertNotEqual(term, object()) + self.assertNotEqual(object(), term) + + term2 = self._makeOne('value') + # Equal to another with the same value + self.assertEqual(term, term2) + # equal objects hash the same + self.assertEqual(hash(term), hash(term2)) + + # Providing tokens or titles that differ + # changes equality + term = self._makeOne('value', 'token') + self.assertNotEqual(term, term2) + self.assertNotEqual(hash(term), hash(term2)) + + term2 = self._makeOne('value', 'token') + self.assertEqual(term, term2) + self.assertEqual(hash(term), hash(term2)) + + term = self._makeOne('value', 'token', 'title') + self.assertNotEqual(term, term2) + self.assertNotEqual(hash(term), hash(term2)) + + term2 = self._makeOne('value', 'token', 'title') + self.assertEqual(term, term2) + self.assertEqual(hash(term), hash(term2)) + + class SimpleVocabularyTests(unittest.TestCase): def _getTargetClass(self): @@ -102,8 +137,8 @@ class SimpleVocabularyTests(unittest.TestCase): self.assertTrue(value in vocabulary) self.assertFalse('ABC' in vocabulary) for term in vocabulary: - self.assertTrue(vocabulary.getTerm(term.value) is term) - self.assertTrue(vocabulary.getTermByToken(term.token) is term) + self.assertIs(vocabulary.getTerm(term.value), term) + self.assertIs(vocabulary.getTermByToken(term.token), term) def test_fromValues(self): from zope.interface import Interface @@ -119,7 +154,7 @@ class SimpleVocabularyTests(unittest.TestCase): self.assertTrue(ITokenizedTerm.providedBy(term)) self.assertEqual(term.value, value) for value in VALUES: - self.assertTrue(value in vocabulary) + self.assertIn(value, vocabulary) def test_fromItems(self): from zope.interface import Interface @@ -136,7 +171,30 @@ class SimpleVocabularyTests(unittest.TestCase): self.assertEqual(term.token, item[0]) self.assertEqual(term.value, item[1]) for item in ITEMS: - self.assertTrue(item[1] in vocabulary) + self.assertIn(item[1], vocabulary) + + def test_fromItems_triples(self): + from zope.interface import Interface + from zope.schema.interfaces import ITitledTokenizedTerm + + class IStupid(Interface): + pass + + ITEMS = [ + ('one', 1, 'title 1'), + ('two', 2, 'title 2'), + ('three', 3, 'title 3'), + ('fore!', 4, 'title four') + ] + vocabulary = self._getTargetClass().fromItems(ITEMS) + self.assertEqual(len(vocabulary), len(ITEMS)) + for item, term in zip(ITEMS, vocabulary): + self.assertTrue(ITitledTokenizedTerm.providedBy(term)) + self.assertEqual(term.token, item[0]) + self.assertEqual(term.value, item[1]) + self.assertEqual(term.title, item[2]) + for item in ITEMS: + self.assertIn(item[1], vocabulary) def test_createTerm(self): from zope.schema.vocabulary import SimpleTerm @@ -204,6 +262,38 @@ class SimpleVocabularyTests(unittest.TestCase): for term in vocab: self.assertEqual(term.value + 1, term.nextvalue) + def test__eq__and__hash__(self): + from zope import interface + + values = [1, 4, 2, 9] + vocabulary = self._getTargetClass().fromValues(values) + + # Equal to itself + self.assertEqual(vocabulary, vocabulary) + # Not to other classes + self.assertNotEqual(vocabulary, object()) + self.assertNotEqual(object(), vocabulary) + + # Equal to another object with the same values + vocabulary2 = self._getTargetClass().fromValues(values) + self.assertEqual(vocabulary, vocabulary2) + self.assertEqual(hash(vocabulary), hash(vocabulary2)) + + # Changing the values or the interfaces changes + # equality + class IFoo(interface.Interface): + "an interface" + + vocabulary = self._getTargetClass().fromValues(values, IFoo) + self.assertNotEqual(vocabulary, vocabulary2) + # Interfaces are not taken into account in the hash; that's + # OK: equal hashes do not imply equal objects + self.assertEqual(hash(vocabulary), hash(vocabulary2)) + + vocabulary2 = self._getTargetClass().fromValues(values, IFoo) + self.assertEqual(vocabulary, vocabulary2) + self.assertEqual(hash(vocabulary), hash(vocabulary2)) + # Test _createTermTree via TreeVocabulary.fromDict @@ -256,6 +346,18 @@ class TreeVocabularyTests(unittest.TestCase): def tree_vocab_3(self): return self._getTargetClass().fromDict(self.business_tree()) + def test_only_titled_if_triples(self): + from zope.schema.interfaces import ITitledTokenizedTerm + no_titles = self.tree_vocab_2() + for term in no_titles: + self.assertIsNone(term.title) + self.assertFalse(ITitledTokenizedTerm.providedBy(term)) + + all_titles = self.tree_vocab_3() + for term in all_titles: + self.assertIsNotNone(term.title) + self.assertTrue(ITitledTokenizedTerm.providedBy(term)) + def test_implementation(self): from zope.interface.verify import verifyObject from zope.interface.common.mapping import IEnumerableMapping diff --git a/src/zope/schema/vocabulary.py b/src/zope/schema/vocabulary.py index 9649547..a1211e4 100644 --- a/src/zope/schema/vocabulary.py +++ b/src/zope/schema/vocabulary.py @@ -17,6 +17,7 @@ from collections import OrderedDict from zope.interface import directlyProvides from zope.interface import implementer +from zope.interface import providedBy from zope.schema._compat import text_type from zope.schema.interfaces import ITitledTokenizedTerm @@ -32,14 +33,20 @@ _marker = object() @implementer(ITokenizedTerm) class SimpleTerm(object): - """Simple tokenized term used by SimpleVocabulary.""" + """ + Simple tokenized term used by SimpleVocabulary. + + .. versionchanged:: 4.6.0 + Implement equality and hashing based on the value, token and title. + """ def __init__(self, value, token=None, title=None): """Create a term for *value* and *token*. If *token* is omitted, str(value) is used for the token, escaping any non-ASCII characters. - If *title* is provided, term implements `ITitledTokenizedTerm`. + If *title* is provided, term implements + :class:`zope.schema.interfaces.ITitledTokenizedTerm`. """ self.value = value if token is None: @@ -64,10 +71,35 @@ class SimpleTerm(object): if title is not None: directlyProvides(self, ITitledTokenizedTerm) + def __eq__(self, other): + if other is self: + return True + + if not isinstance(other, SimpleTerm): + return False + + return ( + self.value == other.value + and self.token == other.token + and self.title == other.title + ) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.value, self.token, self.title)) + @implementer(IVocabularyTokenized) class SimpleVocabulary(object): - """Vocabulary that works from a sequence of terms.""" + """ + Vocabulary that works from a sequence of terms. + + .. versionchanged:: 4.6.0 + Implement equality and hashing based on the terms list + and interfaces implemented by this object. + """ def __init__(self, terms, *interfaces, **kwargs): """Initialize the vocabulary given a list of terms. @@ -80,14 +112,14 @@ class SimpleVocabulary(object): By default, ValueErrors are thrown if duplicate values or tokens are passed in. If you want to swallow these exceptions, pass - in swallow_duplicates=True. In this case, the values will + in ``swallow_duplicates=True``. In this case, the values will override themselves. """ self.by_value = {} self.by_token = {} self._terms = terms + swallow_dupes = kwargs.get('swallow_duplicates', False) for term in self._terms: - swallow_dupes = kwargs.get('swallow_duplicates', False) if not swallow_dupes: if term.value in self.by_value: raise ValueError( @@ -102,16 +134,23 @@ class SimpleVocabulary(object): @classmethod def fromItems(cls, items, *interfaces): - """Construct a vocabulary from a list of (token, value) pairs. + """ + Construct a vocabulary from a list of (token, value) pairs or + (token, value, title) triples. The list does not have to be + homogeneous. The order of the items is preserved as the order of the terms - in the vocabulary. Terms are created by calling the class - method createTerm() with the pair (value, token). + in the vocabulary. Terms are created by calling the class + method :meth:`createTerm`` with the pair or triple. One or more interfaces may also be provided so that alternate widgets may be bound without subclassing. + + .. versionchanged:: 4.6.0 + Allow passing in triples to set item titles. """ - terms = [cls.createTerm(value, token) for (token, value) in items] + terms = [cls.createTerm(item[1], item[0], *item[2:]) + for item in items] return cls(terms, *interfaces) @classmethod @@ -119,10 +158,10 @@ class SimpleVocabulary(object): """Construct a vocabulary from a simple list. Values of the list become both the tokens and values of the - terms in the vocabulary. The order of the values is preserved - as the order of the terms in the vocabulary. Tokens are - created by calling the class method createTerm() with the - value as the only parameter. + terms in the vocabulary. The order of the values is preserved + as the order of the terms in the vocabulary. Tokens are + created by calling the class method :meth:`createTerm()` with + the value as the only parameter. One or more interfaces may also be provided so that alternate widgets may be bound without subclassing. @@ -169,6 +208,21 @@ class SimpleVocabulary(object): """See zope.schema.interfaces.IIterableVocabulary""" return len(self.by_value) + def __eq__(self, other): + if other is self: + return True + + if not isinstance(other, SimpleVocabulary): + return False + + return self._terms == other._terms and providedBy(self) == providedBy(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(tuple(self._terms)) + def _createTermTree(ttree, dict_): """ Helper method that creates a tree-like dict with ITokenizedTerm @@ -177,7 +231,7 @@ def _createTermTree(ttree, dict_): See fromDict for more details. """ for key in sorted(dict_.keys()): - term = SimpleTerm(key[1], key[0], key[-1]) + term = SimpleTerm(key[1], key[0], *key[2:]) ttree[term] = TreeVocabulary.terms_factory() _createTermTree(ttree[term], dict_[key]) return ttree @@ -272,7 +326,8 @@ class TreeVocabulary(object): OrderedDict), that has tuples for keys. The tuples should have either 2 or 3 values, i.e: - (token, value, title) or (token, value) + (token, value, title) or (token, value). Only tuples that have + three values will create a :class:`zope.schema.interfaces.ITitledTokenizedTerm`. For example, a dict with 2-valued tuples: @@ -290,6 +345,10 @@ class TreeVocabulary(object): } One or more interfaces may also be provided so that alternate widgets may be bound without subclassing. + + .. versionchanged:: 4.6.0 + Only create ``ITitledTokenizedTerm`` when a title is actually + provided. """ return cls(_createTermTree(cls.terms_factory(), dict_), *interfaces) |
