summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst10
-rw-r--r--src/zope/schema/tests/test__field.py7
-rw-r--r--src/zope/schema/tests/test_vocabulary.py110
-rw-r--r--src/zope/schema/vocabulary.py89
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)