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