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
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."""
 

_______________________________________________
Zope-Dev maillist  -  Zope-Dev@zope.org
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 )

Reply via email to