New release for zope.schema

Jan-Carel Brand <[email protected]>
Newsgroups gmane.comp.web.zope.devel
Organization Opkode
Message-ID <1332347384.11511.23.camel@crunchbang>
Hi folks

What needs to be done for a new release of zope.schema? (4.1)
And is there anything I can do to help speed it up?

Even just an alpha/beta release would be very helpful.

Thanks
JC

_______________________________________________
Zope-Dev maillist  -  [email protected]
https://mail.zope.org/mailman/listinfo/zope-dev
**  No cross posts or HTML encoding!  **
(Related lists -
 https://mail.zope.org/mailman/listinfo/zope-announce
 https://mail.zope.org/mailman/listinfo/zope )
zope.schema.diff (text/x-patch, 29 KB)
svn --non-interactive diff http://svn.zope.org/repos/main/zope.schema/tags/4.0.1 http://svn.zope.org/repos/main/zope.schema/trunk
Index: CHANGES.txt
===================================================================
--- CHANGES.txt	(.../tags/4.0.1)	(revision 124658)
+++ CHANGES.txt	(.../trunk)	(revision 124658)
@@ -2,6 +2,18 @@
 CHANGES
 =======
 
+4.1 (unreleased)
+------------------
+
+- Add TreeVocabulary for nested tree-like vocabularies.
+
+- Fix broken Object field validation where the schema contains a Choice with
+  ICountextSourceBinder source. In this case the vocabulary was not iterable
+  because the field was not bound and the source binder dien't return the 
+  real vocabulary. Added simple test for IContextSourceBinder validation. But a
+  test with an Object field with a schema using a Choice with
+  IContextSourceBinder is still missing.
+
 4.0.1 (2011-11-14)
 ------------------
 
Index: setup.py
===================================================================
--- setup.py	(.../tags/4.0.1)	(revision 124658)
+++ setup.py	(.../trunk)	(revision 124658)
@@ -19,6 +19,7 @@
 """Setup for zope.schema package
 """
 import os
+import sys
 from setuptools import setup, find_packages
 
 def read(*rnames):
@@ -60,8 +61,18 @@
             suite.addTest(mod.test_suite())
     return suite
 
+REQUIRES = [
+        'setuptools',
+        'zope.interface >= 3.6.0',
+        'zope.event',
+        'six',
+        ]
+
+if sys.version_info < (2 , 7):
+    REQUIRES += ['ordereddict'],
+
 setup(name='zope.schema',
-      version = '4.0.1',
+      version = '4.1dev',
       url='http://pypi.python.org/pypi/zope.schema',
       license='ZPL 2.1',
       description='zope.interface extension for defining data schemas',
@@ -81,11 +92,8 @@
       namespace_packages=['zope',],
       extras_require={'test': ['zope.testing'],
                       'docs': ['z3c.recipe.sphinxdoc']},
-      install_requires=['setuptools',
-                        'zope.interface >= 3.6.0',
-                        'zope.event',
-                        'six',
-                       ],
+      install_requires=REQUIRES,
+      
       classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Intended Audience :: Developers",
Index: src/zope/schema/fields.txt
===================================================================
--- src/zope/schema/fields.txt	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/fields.txt	(.../trunk)	(revision 124658)
@@ -116,6 +116,9 @@
 The vocabulary interface is simple enough that writing a custom vocabulary is
 not too difficult itself.
 
+See for example zope.schema.vocabulary.TreeVocabulary for another
+IBaseVocabulary supporting vocabulary that provides a nested, tree-like structure.
+
 Choices and Collections
 -----------------------
 
@@ -156,3 +159,4 @@
 
 This level of indirection may be unnecessary for some applications, and can be
 disabled with simple ZCML changes within `zope.app`.
+
Index: src/zope/schema/vocabulary.py
===================================================================
--- src/zope/schema/vocabulary.py	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/vocabulary.py	(.../trunk)	(revision 124658)
@@ -13,14 +13,19 @@
 ##############################################################################
 """Vocabulary support for schema.
 """
+try:
+    from collections import OrderedDict
+except:
+    from ordereddict import OrderedDict
+
 from zope.interface.declarations import directlyProvides, implementer
 from zope.schema.interfaces import ValidationError
 from zope.schema.interfaces import IVocabularyRegistry
 from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
+from zope.schema.interfaces import ITreeVocabulary
 from zope.schema.interfaces import ITokenizedTerm, ITitledTokenizedTerm
 
 # simple vocabularies performing enumerated-like tasks
-
 _marker = object()
 
 @implementer(ITokenizedTerm)
@@ -68,6 +73,7 @@
         if interfaces:
             directlyProvides(self, *interfaces)
 
+    @classmethod
     def fromItems(cls, items, *interfaces):
         """Construct a vocabulary from a list of (token, value) pairs.
 
@@ -80,8 +86,8 @@
         """
         terms = [cls.createTerm(value, token) for (token, value) in items]
         return cls(terms, *interfaces)
-    fromItems = classmethod(fromItems)
 
+    @classmethod
     def fromValues(cls, values, *interfaces):
         """Construct a vocabulary from a simple list.
 
@@ -96,8 +102,8 @@
         """
         terms = [cls.createTerm(value) for value in values]
         return cls(terms, *interfaces)
-    fromValues = classmethod(fromValues)
 
+    @classmethod
     def createTerm(cls, *args):
         """Create a single term from data.
 
@@ -105,7 +111,6 @@
         a term of the appropriate type from the arguments.
         """
         return SimpleTerm(*args)
-    createTerm = classmethod(createTerm)
 
     def __contains__(self, value):
         """See zope.schema.interfaces.IBaseVocabulary"""
@@ -138,8 +143,195 @@
         return len(self.by_value)
 
 
+def _createTermTree(ttree, dict_):
+    """ Helper method that creates a tree-like dict with ITokenizedTerm 
+    objects as keys from a similar tree with tuples as keys.
+
+    See fromDict for more details.
+    """
+    for key in dict_.keys():
+        term = SimpleTerm(key[1], key[0], key[-1])
+        ttree[term] = TreeVocabulary.terms_factory()
+        _createTermTree(ttree[term], dict_[key])
+    return ttree 
+
+
+@implementer(ITreeVocabulary)
+class TreeVocabulary(object):
+    """ Vocabulary that relies on a tree (i.e nested) structure.
+    """
+    # The default implementation uses a dict to create the tree structure. This
+    # can however be overridden in a subclass by any other IEnumerableMapping
+    # compliant object type. Python 2.7's OrderableDict for example.
+    terms_factory = OrderedDict
+
+    def __init__(self, terms, *interfaces):
+        """Initialize the vocabulary given a recursive dict (i.e a tree) with 
+        ITokenizedTerm objects for keys and self-similar dicts representing the 
+        branches for values.
+
+        Refer to the method fromDict for more details.
+
+        Concerning the ITokenizedTerm keys, the 'value' and 'token' attributes of
+        each key (including nested ones) must be unique.
+
+        One or more interfaces may also be provided so that alternate
+        widgets may be bound without subclassing.
+        """
+        self._terms = self.terms_factory()
+        self._terms.update(terms)
+
+        self.path_by_value = {}
+        self.term_by_value = {}
+        self.term_by_token = {}
+        self._populateIndexes(terms)
+
+        if interfaces:
+            directlyProvides(self, *interfaces)
+            
+    def __contains__(self, value):
+        """ See zope.schema.interfaces.IBaseVocabulary
+
+        D.__contains__(k) -> True if D has a key k, else False
+        """
+        try:
+            return value in self.term_by_value
+        except TypeError:
+            # sometimes values are not hashable
+            return False
+    
+    def __getitem__(self, key):
+        """x.__getitem__(y) <==> x[y]
+        """
+        return self._terms.__getitem__(key)
+
+    def __iter__(self):
+        """See zope.schema.interfaces.IIterableVocabulary
+        
+        x.__iter__() <==> iter(x)
+        """
+        return self._terms.__iter__()
+
+    def __len__(self):
+        """x.__iter__() <==> iter(x)
+        """
+        return self._terms.__len__()
+
+    def get(self, key, default=None):
+        """Get a value for a key
+
+        The default is returned if there is no value for the key.
+        """
+        return self._terms.get(key, default)
+        
+    def keys(self):
+        """Return the keys of the mapping object.
+        """
+        return self._terms.keys()
+
+    def values(self):
+        """Return the values of the mapping object.
+        """
+        return self._terms.values()
+
+    def items(self):
+        """Return the items of the mapping object.
+        """
+        return self._terms.items()
+
+    @classmethod
+    def fromDict(cls, dict_, *interfaces):
+        """Constructs a vocabulary from a dictionary-like object (like dict or
+        OrderedDict), that has tuples for keys.
+
+        The tuples should have either 2 or 3 values, i.e: 
+        (token, value, title) or (token, value)
+        
+        For example, a dict with 2-valued tuples:  
+
+        dict_ = {
+            ('exampleregions', 'Regions used in ATVocabExample'): {
+                ('aut', 'Austria'): {
+                    ('tyr', 'Tyrol'): {
+                        ('auss', 'Ausserfern'): {},
+                    }
+                },
+                ('ger', 'Germany'): {
+                    ('bav', 'Bavaria'):{}
+                },
+            }
+        }
+        One or more interfaces may also be provided so that alternate
+        widgets may be bound without subclassing.
+        """
+        return cls(_createTermTree(cls.terms_factory(), dict_), *interfaces)
+
+    def _populateIndexes(self, tree):
+        """ The TreeVocabulary contains three helper indexes for quick lookups.
+        They are: term_by_value, term_by_token and path_by_value
+
+        This method recurses through the tree and populates these indexes.
+        
+        tree:  The tree (a nested/recursive dictionary).
+        """
+        for term in tree.keys():
+            value = getattr(term, 'value')
+            token = getattr(term, 'token')
+
+            if value in self.term_by_value: 
+                raise ValueError(
+                    "Term values must be unique: '%s'" % value)
+
+            if token in self.term_by_token:
+                raise ValueError(
+                    "Term tokens must be unique: '%s'" % token)
+
+            self.term_by_value[value] = term
+            self.term_by_token[token] = term
+
+            if value not in self.path_by_value:
+               self.path_by_value[value] = self._getPathToTreeNode(self, value)
+            self._populateIndexes(tree[term])
+    
+    def getTerm(self, value):
+        """See zope.schema.interfaces.IBaseVocabulary"""
+        try:
+            return self.term_by_value[value]
+        except KeyError:
+            raise LookupError(value)
+
+    def getTermByToken(self, token):
+        """See zope.schema.interfaces.IVocabularyTokenized"""
+        try:
+            return self.term_by_token[token]
+        except KeyError:
+            raise LookupError(token)
+
+    def _getPathToTreeNode(self, tree, node):
+        """Helper method that computes the path in the tree from the root
+        to the given node.
+
+        The tree must be a recursive IEnumerableMapping object.
+        """
+        path = []
+        for parent, child in tree.items():
+            if node == parent.value:
+                return [node]
+            path = self._getPathToTreeNode(child, node)
+            if path:
+                path.insert(0, parent.value)
+                break
+        return path 
+
+    def getTermPath(self, value):
+        """Returns a list of strings representing the path from the root node 
+        to the node with the given value in the tree. 
+
+        Returns an empty string if no node has that value.
+        """
+        return self.path_by_value.get(value, [])
+
 # registry code
-
 class VocabularyRegistryError(LookupError):
     def __init__(self, name):
         self.name = name
Index: src/zope/schema/tests/test_choice.py
===================================================================
--- src/zope/schema/tests/test_choice.py	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/tests/test_choice.py	(.../trunk)	(revision 124658)
@@ -16,11 +16,13 @@
 import unittest
 
 from six import u
+from zope.interface import implements
 from zope.schema import vocabulary
 from zope.schema import Choice
 from zope.schema.interfaces import ConstraintNotSatisfied
 from zope.schema.interfaces import ValidationError
 from zope.schema.interfaces import InvalidValue, NotAContainer, NotUnique
+from zope.schema.interfaces import IContextSourceBinder
 
 from zope.schema.tests.test_vocabulary import SampleVocabulary, DummyRegistry
 
@@ -112,10 +114,37 @@
         self.assertRaises(ValueError, choice.validate, "value")
 
 
+class SampleContextSourceBinder(object):
+    implements(IContextSourceBinder)
+    def __call__(self, context):
+        return SampleVocabulary()
+
+class ContextSourceBinder_ChoiceFieldTests(unittest.TestCase):
+    """Tests of the Choice Field using IContextSourceBinder as source."""
+
+    def setUp(self):
+        vocabulary._clear()
+
+    def tearDown(self):
+        vocabulary._clear()
+
+    def test_validate_source(self):
+        s = SampleContextSourceBinder()
+        choice = Choice(source=s)
+        # raises not iterable with unbound field
+        self.assertRaises(TypeError, choice.validate, 1)
+        o = object()
+        clone = choice.bind(o)
+        clone.validate(1)
+        clone.validate(3)
+        self.assertRaises(ConstraintNotSatisfied, clone.validate, 42)
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(Vocabulary_ChoiceFieldTests))
     suite.addTest(unittest.makeSuite(Value_ChoiceFieldTests))
+    suite.addTest(unittest.makeSuite(ContextSourceBinder_ChoiceFieldTests))
     return suite
 
 if __name__ == "__main__":
Index: src/zope/schema/tests/test_vocabulary.py
===================================================================
--- src/zope/schema/tests/test_vocabulary.py	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/tests/test_vocabulary.py	(.../trunk)	(revision 124658)
@@ -15,9 +15,15 @@
 """
 import unittest
 
+try:
+    from collections import OrderedDict
+except:
+    from ordereddict import OrderedDict
+
 from zope.interface.verify import verifyObject
 from zope.interface.exceptions import DoesNotImplement
 from zope.interface import Interface, implementer
+from zope.interface.common.mapping import IEnumerableMapping
 
 from zope.schema import interfaces
 from zope.schema import vocabulary
@@ -187,10 +193,315 @@
             self.assertEqual(term.value + 1, term.nextvalue)
 
 
+class TreeVocabularyTests(unittest.TestCase):
+    region_tree = { 
+            ('regions', 'Regions'): {
+                ('aut', 'Austria'): {
+                    ('tyr', 'Tyrol'): {
+                        ('auss', 'Ausserfern'): {},
+                    }
+                },
+                ('ger', 'Germany'): {
+                    ('bav', 'Bavaria'):{}
+                },
+            }
+        }
+    tree_vocab_2 = vocabulary.TreeVocabulary.fromDict(region_tree)
+
+    business_tree = {
+            ('services', 'services', 'Services'): {
+                ('reservations', 'reservations', 'Reservations'): {
+                    ('res_host', 'res_host', 'Res Host'): {},
+                    ('res_gui', 'res_gui', 'Res GUI'): {},
+                },
+                ('check_in', 'check_in', 'Check-in'): {
+                    ('dcs_host', 'dcs_host', 'DCS Host'): {},
+                },
+            },
+            ('infrastructure', 'infrastructure', 'Infrastructure'): {
+                ('communication_network', 'communication_network', 'Communication/Network'): {
+                    ('messaging', 'messaging', 'Messaging'): {},
+                },
+                ('data_transaction', 'data_transaction', 'Data/Transaction'): {
+                    ('database', 'database', 'Database'): {},
+                },
+                ('security', 'security', 'Security'): {},
+            },
+        }
+    tree_vocab_3 = vocabulary.TreeVocabulary.fromDict(business_tree)
+
+    def test_implementation(self):
+        for v in [self.tree_vocab_2, self.tree_vocab_3]:
+            self.assertTrue(verifyObject(IEnumerableMapping, v))
+            self.assertTrue(verifyObject(interfaces.IVocabulary, v))
+            self.assertTrue(verifyObject(interfaces.IVocabularyTokenized, v))
+            self.assertTrue(verifyObject(interfaces.ITreeVocabulary, v))
+
+    def test_addt_interfaces(self):
+        class IStupid(Interface):
+            pass
+        v = vocabulary.TreeVocabulary.fromDict({('one', '1'): {}}, IStupid)
+        self.assertTrue(IStupid.providedBy(v))
+
+    def test_ordering(self):
+        """The TreeVocabulary makes use of an OrderedDict to store it's
+           internal tree representation.
+
+           Check that they keys are indeed oredered.
+        """
+
+        d = {   (1, 'new_york', 'New York'): {
+                    (2, 'ny_albany', 'Albany'): {},
+                    (3, 'ny_new_york', 'New York'): {},
+                },
+                (4, 'california', 'California'): {
+                    (5, 'ca_los_angeles', 'Los Angeles'): {},
+                    (6, 'ca_san_francisco', 'San Francisco'): {},
+                },
+                (7, 'texas', 'Texas'): {},
+                (8, 'florida', 'Florida'): {},
+                (9, 'utah', 'Utah'): {},
+            }
+        dict_ = OrderedDict(sorted(d.items(), key=lambda t: t[0]))
+        vocab = vocabulary.TreeVocabulary.fromDict(dict_)
+        # Test keys
+        self.assertEqual([k.token for k in vocab.keys()], ['1', '4', '7', '8', '9'])
+        # Test __iter__
+        self.assertEqual([k.token for k in vocab], ['1', '4', '7', '8', '9'])
+
+        self.assertEqual([k.token for k in vocab[vocab.keys()[0]].keys()], ['2', '3'])
+        self.assertEqual([k.token for k in vocab[vocab.keys()[1]].keys()], ['5', '6'])
+
+    def test_indexes(self):
+        """ The TreeVocabulary creates three indexes for quick lookups,
+        term_by_value, term_by_value and path_by_value.
+        """
+        self.assertEqual(
+            self.tree_vocab_2.term_by_value.keys(), 
+            ['Tyrol', 'Bavaria', 'Regions', 'Austria', 'Germany', 'Ausserfern'])
+
+        self.assertEqual(
+            self.tree_vocab_2.term_by_token.keys(),
+            ['bav', 'ger', 'auss', 'regions', 'aut', 'tyr'])
+
+        self.assertEqual(
+            self.tree_vocab_2.path_by_value.keys(), 
+            ['Tyrol', 'Bavaria', 'Regions', 'Austria', 'Germany', 'Ausserfern'])
+
+        self.assertEqual(
+            self.tree_vocab_2.path_by_value.values(), 
+            [
+                ['Regions', 'Austria', 'Tyrol'], 
+                ['Regions', 'Germany', 'Bavaria'], 
+                ['Regions'], 
+                ['Regions', 'Austria'], 
+                ['Regions', 'Germany'], 
+                ['Regions', 'Austria', 'Tyrol', 'Ausserfern']
+            ])
+
+        self.assertEqual(
+            self.tree_vocab_3.term_by_value.keys(), 
+            [   'data_transaction', 
+                'check_in', 
+                'infrastructure', 
+                'res_gui', 
+                'database', 
+                'reservations', 
+                'dcs_host', 
+                'communication_network', 
+                'res_host', 
+                'services', 
+                'messaging', 
+                'security'
+            ])
+
+        self.assertEqual(
+            self.tree_vocab_3.term_by_token.keys(),
+            [   'data_transaction', 
+                'check_in', 
+                'infrastructure', 
+                'res_gui', 
+                'database', 
+                'reservations', 
+                'dcs_host', 
+                'communication_network', 
+                'res_host', 
+                'services', 
+                'messaging', 
+                'security'
+            ])
+
+        self.assertEqual(
+            self.tree_vocab_3.path_by_value.values(), 
+            [   ['infrastructure', 'data_transaction'], 
+                ['services', 'check_in'],
+                ['infrastructure'], 
+                ['services', 'reservations', 'res_gui'],
+                ['infrastructure', 'data_transaction', 'database'], 
+                ['services', 'reservations'], 
+                ['services', 'check_in', 'dcs_host'],
+                ['infrastructure', 'communication_network'], 
+                ['services', 'reservations', 'res_host'], 
+                ['services'], 
+                ['infrastructure', 'communication_network', 'messaging'], 
+                ['infrastructure', 'security']
+            ])
+
+    def test_termpath(self):
+        self.assertEqual(
+                    self.tree_vocab_2.getTermPath('Bavaria'), 
+                    ['Regions', 'Germany', 'Bavaria'])
+        self.assertEqual(
+                    self.tree_vocab_2.getTermPath('Austria'), 
+                    ['Regions', 'Austria'])
+        self.assertEqual(
+                    self.tree_vocab_2.getTermPath('Ausserfern'), 
+                    ['Regions', 'Austria', 'Tyrol', 'Ausserfern'])
+        self.assertEqual(
+                    self.tree_vocab_2.getTermPath('Non-existent'), 
+                    [])
+        self.assertEqual(
+                    self.tree_vocab_3.getTermPath('database'),
+                    ["infrastructure", "data_transaction", "database"])
+
+    def test_len(self):
+        """ len returns the number of all nodes in the dict
+        """
+        self.assertEqual(len(self.tree_vocab_2), 1)
+        self.assertEqual(len(self.tree_vocab_3), 2)
+
+    def test_contains(self):
+        self.assertTrue('Regions' in self.tree_vocab_2 and 
+                        'Austria' in self.tree_vocab_2 and 
+                        'Bavaria' in self.tree_vocab_2)
+
+        self.assertTrue('bav' not in self.tree_vocab_2)
+        self.assertTrue('foo' not in self.tree_vocab_2)
+
+        self.assertTrue('database' in self.tree_vocab_3 and 
+                        'security' in self.tree_vocab_3 and 
+                        'services' in self.tree_vocab_3)
+
+        self.assertTrue('Services' not in self.tree_vocab_3)
+        self.assertTrue('Database' not in self.tree_vocab_3)
+
+    def test_values_and_items(self):
+        for v in (self.tree_vocab_2, self.tree_vocab_3):
+            for term in v:
+                self.assertEqual(v.values(), v._terms.values())
+                self.assertEqual(v.items(), v._terms.items())
+
+    def test_get(self):
+        for v in [self.tree_vocab_2, self.tree_vocab_3]:
+            for key, value in v.items():
+                self.assertEqual(v.get(key), value)
+                self.assertEqual(v[key], value)
+
+    def test_get_term(self):
+        for v in (self.tree_vocab_2, self.tree_vocab_3):
+            for term in v:
+                self.assertTrue(v.getTerm(term.value) is term)
+                self.assertTrue(v.getTermByToken(term.token) is term)
+            self.assertRaises(LookupError, v.getTerm, 'non-present-value')
+            self.assertRaises(LookupError, v.getTermByToken, 'non-present-token')
+
+    def test_nonunique_values_and_tokens(self):
+        """Since we do term and value lookups, all terms' values and tokens
+        must be unique. This rule applies recursively.
+        """
+        self.assertRaises(
+            ValueError, vocabulary.TreeVocabulary.fromDict,
+            { ('one', '1'): {},
+              ('two', '1'): {},
+            })
+        self.assertRaises(
+            ValueError, vocabulary.TreeVocabulary.fromDict,
+            { ('one', '1'): {},
+              ('one', '2'): {},
+            })
+        # Even nested tokens must be unique.
+        self.assertRaises(
+            ValueError, vocabulary.TreeVocabulary.fromDict,
+            { ('new_york', 'New York'): {
+                    ('albany', 'Albany'): {},
+                    ('new_york', 'New York'): {},
+                },
+            })
+        # The same applies to nested values.
+        self.assertRaises(
+            ValueError, vocabulary.TreeVocabulary.fromDict,
+            { ('1', 'new_york'): {
+                    ('2', 'albany'): {},
+                    ('3', 'new_york'): {},
+                },
+            })
+        # The title attribute does however not have to be unique.
+        vocabulary.TreeVocabulary.fromDict(
+            { ('1', 'new_york', 'New York'): {
+                    ('2', 'ny_albany', 'Albany'): {},
+                    ('3', 'ny_new_york', 'New York'): {},
+                },
+            })
+        vocabulary.TreeVocabulary.fromDict({
+                ('one', '1', 'One'): {},
+                ('two', '2', 'One'): {},
+            })
+
+    def test_nonunique_value_message(self):
+        try:
+            vocabulary.TreeVocabulary.fromDict(
+            { ('one', '1'): {},
+              ('two', '1'): {},
+            })
+        except ValueError as e:
+            self.assertEqual(str(e), "Term values must be unique: '1'")
+
+    def test_nonunique_token_message(self):
+        try:
+            vocabulary.TreeVocabulary.fromDict(
+            { ('one', '1'): {},
+              ('one', '2'): {},
+            })
+        except ValueError as e:
+            self.assertEqual(str(e), "Term tokens must be unique: 'one'")
+
+    def test_recursive_methods(self):
+        """Test the _createTermTree and _getPathToTreeNode methods
+        """
+        tree = vocabulary._createTermTree({}, self.business_tree)
+        vocab = vocabulary.TreeVocabulary.fromDict(self.business_tree)
+
+        term_path = vocab._getPathToTreeNode(tree, "infrastructure")
+        vocab_path = vocab._getPathToTreeNode(vocab, "infrastructure")
+        self.assertEqual(term_path, vocab_path)
+        self.assertEqual(term_path, ["infrastructure"])
+
+        term_path = vocab._getPathToTreeNode(tree, "security")
+        vocab_path = vocab._getPathToTreeNode(vocab, "security")
+        self.assertEqual(term_path, vocab_path)
+        self.assertEqual(term_path, ["infrastructure", "security"])
+
+        term_path = vocab._getPathToTreeNode(tree, "database")
+        vocab_path = vocab._getPathToTreeNode(vocab, "database")
+        self.assertEqual(term_path, vocab_path)
+        self.assertEqual(term_path, ["infrastructure", "data_transaction", "database"])
+
+        term_path = vocab._getPathToTreeNode(tree, "dcs_host")
+        vocab_path = vocab._getPathToTreeNode(vocab, "dcs_host")
+        self.assertEqual(term_path, vocab_path)
+        self.assertEqual(term_path, ["services", "check_in", "dcs_host"])
+
+        term_path = vocab._getPathToTreeNode(tree, "dummy")
+        vocab_path = vocab._getPathToTreeNode(vocab, "dummy")
+        self.assertEqual(term_path, vocab_path)
+        self.assertEqual(term_path, [])
+
 def test_suite():
     suite = unittest.makeSuite(RegistryTests)
     suite.addTest(unittest.makeSuite(SimpleVocabularyTests))
+    suite.addTest(unittest.makeSuite(TreeVocabularyTests))
     return suite
 
 if __name__ == "__main__":
     unittest.main(defaultTest="test_suite")
+
Index: src/zope/schema/_field.py
===================================================================
--- src/zope/schema/_field.py	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/_field.py	(.../trunk)	(revision 124658)
@@ -492,7 +492,12 @@
             if not IMethod.providedBy(schema[name]):
                 try:
                     attribute = schema[name]
-                    if IField.providedBy(attribute):
+                    if IChoice.providedBy(attribute):
+                        # Choice must be bound before validation otherwise
+                        # IContextSourceBinder is not iterable in validation
+                        bound = attribute.bind(value)
+                        bound.validate(getattr(value, name))
+                    elif IField.providedBy(attribute):
                         # validate attributes that are fields
                         attribute.validate(getattr(value, name))
                 except ValidationError as error:
Index: src/zope/schema/interfaces.py
===================================================================
--- src/zope/schema/interfaces.py	(.../tags/4.0.1)	(revision 124658)
+++ src/zope/schema/interfaces.py	(.../trunk)	(revision 124658)
@@ -16,6 +16,7 @@
 __docformat__ = "reStructuredText"
 
 from zope.interface import Interface, Attribute
+from zope.interface.common.mapping import IEnumerableMapping
 from six import u, PY3
 
 from zope.schema._messageid import _
@@ -27,7 +28,6 @@
 from zope.schema._bootstrapfields import Iterable
 from zope.schema._bootstrapfields import Text
 from zope.schema._bootstrapfields import TextLine
-from zope.schema._bootstrapfields import TextLine
 from zope.schema._bootstrapfields import Bool
 from zope.schema._bootstrapfields import Int
 from zope.schema._bootstrapinterfaces import StopValidation
@@ -45,6 +45,7 @@
 from zope.schema._bootstrapinterfaces import InvalidValue
 from zope.schema._bootstrapinterfaces import IContextAwareDefaultFactory
 
+
 class WrongContainedType(ValidationError):
     __doc__ = _("""Wrong contained type""")
 
@@ -653,6 +654,13 @@
         is raised.
         """
 
+class ITreeVocabulary(IVocabularyTokenized, IEnumerableMapping):
+    """A tokenized vocabulary with a tree-like structure. 
+    
+       The tree is implemented as dictionary, with keys being ITokenizedTerm
+       terms and the values being similar dictionaries. Leaf values are empty
+       dictionaries.
+    """
 
 class IVocabularyRegistry(Interface):
     """Registry that provides IBaseVocabulary objects for specific fields.
@@ -665,6 +673,7 @@
         When the vocabulary cannot be found, LookupError is raised.
         """
 
+
 class IVocabularyFactory(Interface):
     """Can create vocabularies."""
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.