Skip to content

Commit

Permalink
Make resolving a vocabulary consistent for bound and unbound fields s…
Browse files Browse the repository at this point in the history
…o we get good error messages always. Fixes #54.
  • Loading branch information
jamadden committed Sep 4, 2018
1 parent 1b3fc07 commit 59f7eca
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 20 deletions.
56 changes: 36 additions & 20 deletions src/zope/schema/_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@ class MissingVocabularyError(ValidationError,
"""Raised when a named vocabulary cannot be found."""
# Subclasses ValueError and LookupError for backwards compatibility

class InvalidVocabularyError(ValidationError,
ValueError,
TypeError):
"""Raised when the vocabulary is not an ISource."""
# Subclasses TypeError and ValueError for backwards compatibility

def __init__(self, vocabulary):
super(InvalidVocabularyError, self).__init__("Invalid vocabulary %r" % (vocabulary,))


@implementer(IChoice, IFromUnicode)
class Choice(Field):
Expand Down Expand Up @@ -371,7 +380,7 @@ def __init__(self, values=None, vocabulary=None, source=None, **kw):
else:
if (not ISource.providedBy(vocabulary)
and not IContextSourceBinder.providedBy(vocabulary)):
raise ValueError('Invalid vocabulary')
raise InvalidVocabularyError(vocabulary)
self.vocabulary = vocabulary
# Before a default value is checked, it is validated. However, a
# named vocabulary is usually not complete when these fields are
Expand All @@ -385,19 +394,34 @@ def __init__(self, values=None, vocabulary=None, source=None, **kw):

source = property(lambda self: self.vocabulary)

def bind(self, object):
"""See zope.schema._bootstrapinterfaces.IField."""
clone = super(Choice, self).bind(object)
# get registered vocabulary if needed:
if IContextSourceBinder.providedBy(self.vocabulary):
clone.vocabulary = self.vocabulary(object)
elif clone.vocabulary is None and self.vocabularyName is not None:
def _resolve_vocabulary(self, value):
# Find the vocabulary we should use, raising
# an exception if this isn't possible, and returning
# an ISource otherwise.
vocabulary = self.vocabulary
if IContextSourceBinder.providedBy(vocabulary) and self.context is not None:
vocabulary = vocabulary(self.context)
elif vocabulary is None and self.vocabularyName is not None:
vr = getVocabularyRegistry()
clone.vocabulary = vr.get(object, self.vocabularyName)
try:
vocabulary = vr.get(self.context, self.vocabularyName)
except LookupError:
raise MissingVocabularyError(
"Can't validate value without vocabulary named %r" % (self.vocabularyName,)
).with_field_and_value(self, value)

if not ISource.providedBy(vocabulary):
raise InvalidVocabularyError(vocabulary).with_field_and_value(self, value)

if not ISource.providedBy(clone.vocabulary):
raise ValueError('Invalid clone vocabulary')
return vocabulary

def bind(self, context):
"""See zope.schema._bootstrapinterfaces.IField."""
clone = super(Choice, self).bind(context)
# Eagerly get registered vocabulary if needed;
# once that's done, just return it
clone.vocabulary = clone._resolve_vocabulary(None)
clone._resolve_vocabulary = lambda value: clone.vocabulary
return clone

def fromUnicode(self, str):
Expand All @@ -411,15 +435,7 @@ def _validate(self, value):
if self._init_field:
return
super(Choice, self)._validate(value)
vocabulary = self.vocabulary
if vocabulary is None:
vr = getVocabularyRegistry()
try:
vocabulary = vr.get(None, self.vocabularyName)
except LookupError:
raise MissingVocabularyError(
"Can't validate value without vocabulary named %r" % (self.vocabularyName,)
).with_field_and_value(self, value)
vocabulary = self._resolve_vocabulary(value)
if value not in vocabulary:
raise ConstraintNotSatisfied(value, self.__name__).with_field_and_value(self, value)

Expand Down
19 changes: 19 additions & 0 deletions src/zope/schema/tests/test__field.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,25 @@ def get(*args):
setVocabularyRegistry(Reg())
self.test__validate_w_named_vocabulary_invalid()

def test__validate_w_named_vocabulary_passes_context(self):
from zope.schema.vocabulary import setVocabularyRegistry
context = object()
choice = self._makeOne(vocabulary='vocab')

class Reg(object):
called_with = ()
def get(self, *args):
self.called_with += args
return _makeSampleVocabulary()

reg = Reg()
setVocabularyRegistry(reg)

choice = choice.bind(context)
choice._validate(1)

self.assertEqual(reg.called_with, (context, 'vocab'))

def test__validate_w_named_vocabulary(self):
from zope.schema.interfaces import ConstraintNotSatisfied
from zope.schema.vocabulary import setVocabularyRegistry
Expand Down

0 comments on commit 59f7eca

Please sign in to comment.