PYTHON-839 - Better validation error messages.

This commit is contained in:
Bernie Hackett 2015-02-23 17:32:51 -08:00
parent caf69240ce
commit 7164ba2f41
10 changed files with 107 additions and 104 deletions

View File

@ -43,10 +43,10 @@ class CodecOptions(_options_base):
def __new__(cls, as_class=dict,
tz_aware=False, uuid_representation=PYTHON_LEGACY):
if not issubclass(as_class, MutableMapping):
raise TypeError("document_class must be a "
"subclass of MutableMapping")
raise TypeError("document_class must be dict, bson.son.SON, or "
"another subclass of collections.MutableMapping")
if not isinstance(tz_aware, bool):
raise TypeError("tz_aware must be a boolean")
raise TypeError("tz_aware must be True or False")
if uuid_representation not in ALL_UUID_REPRESENTATIONS:
raise ValueError("uuid_representation must be a value "
"from bson.binary.ALL_UUID_REPRESENTATIONS")

View File

@ -20,6 +20,8 @@ The :mod:`gridfs` package is an implementation of GridFS on top of
.. mongodoc:: gridfs
"""
from collections import Mapping
from gridfs.errors import NoFile
from gridfs.grid_file import (GridIn,
GridOut,
@ -275,7 +277,7 @@ class GridFS(object):
- `**kwargs` (optional): any additional keyword arguments
are the same as the arguments to :meth:`find`.
"""
if filter is not None and not isinstance(filter, dict):
if filter is not None and not isinstance(filter, Mapping):
filter = {"_id": filter}
for f in self.find(filter, *args, **kwargs):

View File

@ -19,11 +19,12 @@
from __future__ import unicode_literals
import collections
from bson.objectid import ObjectId
from bson.son import SON
from pymongo import helpers
from pymongo.common import (validate_is_mapping,
validate_is_mutable_mapping,
validate_ok_for_replace,
validate_ok_for_update)
from pymongo.errors import (BulkWriteError,
DocumentTooLarge,
InvalidOperation,
@ -200,8 +201,7 @@ class _Bulk(object):
def add_insert(self, document):
"""Add an insert document to the list of ops.
"""
if not isinstance(document, collections.MutableMapping):
raise TypeError('document must be a mapping type.')
validate_is_mutable_mapping("document", document)
# Generate ObjectId client side.
if '_id' not in document:
document['_id'] = ObjectId()
@ -210,7 +210,7 @@ class _Bulk(object):
def add_update(self, selector, update, multi=False, upsert=False):
"""Create an update document and add it to the list of ops.
"""
helpers._check_ok_for_update(update)
validate_ok_for_update(update)
cmd = SON([('q', selector), ('u', update),
('multi', multi), ('upsert', upsert)])
self.ops.append((_UPDATE, cmd))
@ -218,7 +218,7 @@ class _Bulk(object):
def add_replace(self, selector, replacement, upsert=False):
"""Create a replace document and add it to the list of ops.
"""
helpers._check_ok_for_replace(replacement)
validate_ok_for_replace(replacement)
cmd = SON([('q', selector), ('u', replacement),
('multi', False), ('upsert', upsert)])
self.ops.append((_UPDATE, cmd))
@ -559,8 +559,7 @@ class BulkOperationBuilder(object):
- A :class:`BulkWriteOperation` instance, used to add
update and remove operations to this bulk operation.
"""
if not isinstance(selector, collections.Mapping):
raise TypeError('selector must be a mapping type.')
validate_is_mapping("selector", selector)
return BulkWriteOperation(selector, self.__bulk)
def insert(self, document):
@ -578,7 +577,6 @@ class BulkOperationBuilder(object):
- write_concern (optional): the write concern for this bulk
execution.
"""
if (write_concern and not
isinstance(write_concern, collections.Mapping)):
raise TypeError('write_concern must be a mapping type.')
if write_concern is not None:
validate_is_mapping("write_concern", write_concern)
return self.__bulk.execute(write_concern)

View File

@ -17,7 +17,7 @@
from bson.codec_options import _parse_codec_options
from bson.py3compat import iteritems
from pymongo.auth import _build_credentials_tuple
from pymongo.common import validate
from pymongo.common import validate, validate_boolean
from pymongo.errors import ConfigurationError
from pymongo.pool import PoolOptions
from pymongo.read_preferences import make_read_preference
@ -57,8 +57,8 @@ def _parse_write_concern(options):
def _parse_ssl_options(options):
"""Parse ssl options."""
use_ssl = options.get('ssl')
if use_ssl is not None and not isinstance(use_ssl, bool):
raise TypeError("ssl must be a boolean")
if use_ssl is not None:
validate_boolean('ssl', use_ssl)
certfile = options.get('ssl_certfile')
keyfile = options.get('ssl_keyfile')

View File

@ -395,8 +395,7 @@ class Collection(common.BaseObject):
:Returns:
- An instance of :class:`~pymongo.results.InsertOneResult`.
"""
if not isinstance(document, collections.MutableMapping):
raise TypeError("document must be a mutable mapping type")
common.validate_is_mutable_mapping("document", document)
if "_id" not in document:
document["_id"] = ObjectId()
return InsertOneResult(self._insert(document),
@ -422,9 +421,7 @@ class Collection(common.BaseObject):
def gen():
"""A generator that validates documents and handles _ids."""
for document in documents:
if not isinstance(document, collections.MutableMapping):
raise TypeError("document must be a dict or other "
"subclass of collections.MutableMapping")
common.validate_is_mutable_mapping("document", document)
if "_id" not in document:
document["_id"] = ObjectId()
inserted_ids.append(document["_id"])
@ -438,10 +435,8 @@ class Collection(common.BaseObject):
def _update(self, filter, document, upsert=False, check_keys=True,
multi=False, manipulate=False, write_concern=None):
"""Internal update / replace helper."""
if not isinstance(filter, collections.Mapping):
raise TypeError("filter must be a mapping type")
if not isinstance(upsert, bool):
raise TypeError("upsert must be an instance of bool")
common.validate_is_mapping("filter", filter)
common.validate_boolean("upsert", upsert)
if manipulate:
document = self.__database._fix_incoming(document, self)
@ -495,7 +490,7 @@ class Collection(common.BaseObject):
:Returns:
- An instance of :class:`~pymongo.results.UpdateResult`.
"""
helpers._check_ok_for_replace(replacement)
common.validate_ok_for_replace(replacement)
result = self._update(filter, replacement, upsert)
return UpdateResult(result, self.write_concern.acknowledged)
@ -511,7 +506,7 @@ class Collection(common.BaseObject):
:Returns:
- An instance of :class:`~pymongo.results.UpdateResult`.
"""
helpers._check_ok_for_update(update)
common.validate_ok_for_update(update)
result = self._update(filter, update, upsert, False)
return UpdateResult(result, self.write_concern.acknowledged)
@ -527,7 +522,7 @@ class Collection(common.BaseObject):
:Returns:
- An instance of :class:`~pymongo.results.UpdateResult`.
"""
helpers._check_ok_for_update(update)
common.validate_ok_for_update(update)
result = self._update(filter, update, upsert, False, True)
return UpdateResult(result, self.write_concern.acknowledged)
@ -543,8 +538,7 @@ class Collection(common.BaseObject):
def _delete(self, filter, multi, write_concern=None):
"""Internal delete helper."""
if not isinstance(filter, collections.Mapping):
raise TypeError("filter must be a mapping type")
common.validate_is_mapping("filter", filter)
concern = (write_concern or self.write_concern).document
safe = concern.get("w") != 0
@ -1284,8 +1278,7 @@ class Collection(common.BaseObject):
use_cursor = kwargs.pop(
"useCursor",
("cursor" in kwargs or client._writable_max_wire_version() > 0))
if not isinstance(use_cursor, bool):
raise ValueError("useCursor must be a bool")
common.validate_boolean("useCursor", use_cursor)
if batch_size is not None and not use_cursor:
raise ConfigurationError("batchSize requires the use of a cursor")
@ -1536,8 +1529,7 @@ class Collection(common.BaseObject):
def __find_and_modify(self, filter, projection, sort, upsert=None,
return_document=ReturnDocument.Before, **kwargs):
"""Internal findAndModify helper."""
if not isinstance(filter, collections.Mapping):
raise TypeError("filter must be a mapping type")
common.validate_is_mapping("filter", filter)
if not isinstance(return_document, bool):
raise ValueError("return_document must be "
"ReturnDocument.Before or ReturnDocument.After")
@ -1551,8 +1543,7 @@ class Collection(common.BaseObject):
if sort is not None:
cmd["sort"] = helpers._index_document(sort)
if upsert is not None:
if not isinstance(upsert, bool):
raise ValueError("upsert must be True or False")
common.validate_boolean("upsert", upsert)
cmd["upsert"] = upsert
out = self._command(cmd,
ReadPreference.PRIMARY,
@ -1609,7 +1600,7 @@ class Collection(common.BaseObject):
as keyword arguments (for example maxTimeMS can be used with
recent server versions).
"""
helpers._check_ok_for_replace(replacement)
common.validate_ok_for_replace(replacement)
kwargs['update'] = replacement
return self.__find_and_modify(filter, projection,
sort, upsert, return_document, **kwargs)
@ -1643,7 +1634,7 @@ class Collection(common.BaseObject):
as keyword arguments (for example maxTimeMS can be used with
recent server versions).
"""
helpers._check_ok_for_update(update)
common.validate_ok_for_update(update)
kwargs['update'] = update
return self.__find_and_modify(filter, projection,
sort, upsert, return_document, **kwargs)
@ -1658,8 +1649,7 @@ class Collection(common.BaseObject):
"""
warnings.warn("save is deprecated. Use insert_one or replace_one "
"instead", DeprecationWarning, stacklevel=2)
if not isinstance(to_save, collections.MutableMapping):
raise TypeError("cannot save object of type %s" % type(to_save))
common.validate_is_mutable_mapping("to_save", to_save)
write_concern = None
if kwargs:
@ -1701,10 +1691,8 @@ class Collection(common.BaseObject):
"""
warnings.warn("update is deprecated. Use replace_one, update_one or "
"update_many instead.", DeprecationWarning, stacklevel=2)
if not isinstance(spec, collections.Mapping):
raise TypeError("spec must be a mapping type")
if not isinstance(document, collections.Mapping):
raise TypeError("document must be a mapping type")
common.validate_is_mapping("spec", spec)
common.validate_is_mapping("document", document)
if document:
# If a top level key begins with '$' this is a modify operation
# and we should skip key validation. It doesn't matter which key

View File

@ -101,16 +101,20 @@ _UUID_REPRESENTATIONS = {
def validate_boolean(option, value):
"""Validates that 'value' is 'true' or 'false'.
"""
"""Validates that 'value' is True or False."""
if isinstance(value, bool):
return value
elif isinstance(value, string_type):
raise TypeError("%s must be True or False" % (option,))
def validate_boolean_or_string(option, value):
"""Validates that value is True, False, 'true', or 'false'."""
if isinstance(value, string_type):
if value not in ('true', 'false'):
raise ConfigurationError("The value of %s must be "
"'true' or 'false'" % (option,))
return value == 'true'
raise TypeError("Wrong type for %s, value must be a boolean" % (option,))
return validate_boolean(option, value)
def validate_integer(option, value):
@ -300,11 +304,49 @@ def validate_auth_mechanism_properties(option, value):
def validate_document_class(option, value):
"""Validate the document_class option."""
if not issubclass(value, collections.MutableMapping):
raise ConfigurationError("%s must be a sublass of "
raise ConfigurationError("%s must be dict, bson.son.SON, or another "
"sublass of "
"collections.MutableMapping" % (option,))
return value
def validate_is_mapping(option, value):
"""Validate the type of method arguments that expect a document."""
if not isinstance(value, collections.Mapping):
raise TypeError("%s must be an instance of dict, bson.son.SON, or "
"other type that inherits from "
"collections.Mapping" % (option,))
def validate_is_mutable_mapping(option, value):
"""Validate the type of method arguments that expect a mutable document."""
if not isinstance(value, collections.MutableMapping):
raise TypeError("%s must be an instance of dict, bson.son.SON, or "
"other type that inherits from "
"collections.MutableMapping" % (option,))
def validate_ok_for_replace(replacement):
"""Validate a replacement document."""
validate_is_mapping("replacement", replacement)
# Replacement can be {}
if replacement:
first = next(iter(replacement))
if first.startswith('$'):
raise ValueError('replacement can not include $ operators')
def validate_ok_for_update(update):
"""Validate an update document."""
validate_is_mapping("update", update)
# Update can not be {}
if not update:
raise ValueError('update only works with $ operators')
first = next(iter(update))
if not first.startswith('$'):
raise ValueError('update only works with $ operators')
# journal is an alias for j,
# wtimeoutms is an alias for wtimeout,
VALIDATORS = {
@ -312,16 +354,16 @@ VALIDATORS = {
'w': validate_int_or_basestring,
'wtimeout': validate_integer,
'wtimeoutms': validate_integer,
'fsync': validate_boolean,
'j': validate_boolean,
'journal': validate_boolean,
'fsync': validate_boolean_or_string,
'j': validate_boolean_or_string,
'journal': validate_boolean_or_string,
'connecttimeoutms': validate_timeout_or_none,
'max_pool_size': validate_positive_integer_or_none,
'socketkeepalive': validate_boolean,
'socketkeepalive': validate_boolean_or_string,
'sockettimeoutms': validate_timeout_or_none,
'waitqueuetimeoutms': validate_timeout_or_none,
'waitqueuemultiple': validate_positive_integer_or_none,
'ssl': validate_boolean,
'ssl': validate_boolean_or_string,
'ssl_keyfile': validate_readable,
'ssl_certfile': validate_readable,
'ssl_cert_reqs': validate_cert_reqs,
@ -334,7 +376,7 @@ VALIDATORS = {
'authsource': validate_string,
'authmechanismproperties': validate_auth_mechanism_properties,
'document_class': validate_document_class,
'tz_aware': validate_boolean,
'tz_aware': validate_boolean_or_string,
'uuidrepresentation': validate_uuid_representation,
}

View File

@ -17,7 +17,7 @@
import copy
import socket
from collections import deque, Mapping
from collections import deque
from bson import RE_TYPE
from bson.code import Code
@ -26,11 +26,12 @@ from bson.py3compat import (iteritems,
string_type)
from bson.son import SON
from pymongo import helpers, message
from pymongo.read_preferences import ReadPreference
from pymongo.common import validate_boolean, validate_is_mapping
from pymongo.errors import (AutoReconnect,
InvalidOperation,
NotMasterError,
OperationFailure)
from pymongo.read_preferences import ReadPreference
_QUERY_OPTIONS = {
"tailable_cursor": 2,
@ -113,24 +114,19 @@ class Cursor(object):
if spec is None:
spec = {}
if not isinstance(spec, Mapping):
raise TypeError("filter must be a mapping type.")
validate_is_mapping("filter", spec)
if not isinstance(skip, int):
raise TypeError("skip must be an instance of int")
if not isinstance(limit, int):
raise TypeError("limit must be an instance of int")
if not isinstance(no_cursor_timeout, bool):
raise TypeError("no_cursor_timeout must be an instance of bool")
validate_boolean("no_cursor_timeout", no_cursor_timeout)
if cursor_type not in (NON_TAILABLE, TAILABLE,
TAILABLE_AWAIT, EXHAUST):
raise ValueError("not a valid value for cursor_type")
if not isinstance(allow_partial_results, bool):
raise TypeError("allow_partial_results "
"must be an instance of bool")
if not isinstance(oplog_replay, bool):
raise TypeError("oplog_replay must be an instance of bool")
if modifiers is not None and not isinstance(modifiers, Mapping):
raise TypeError("modifiers must be a mapping type.")
validate_boolean("allow_partial_results", allow_partial_results)
validate_boolean("oplog_replay", oplog_replay)
if modifiers is not None:
validate_is_mapping("modifiers", modifiers)
if projection is not None:
if not projection:
@ -666,8 +662,7 @@ class Cursor(object):
.. versionchanged:: 2.8
The :meth:`~count` method now supports :meth:`~hint`.
"""
if not isinstance(with_limit_and_skip, bool):
raise TypeError("with_limit_and_skip must be an instance of bool")
validate_boolean("with_limit_and_skip", with_limit_and_skip)
options = {"query": self.__spec}
if self.__max_time_ms is not None:
options["maxTimeMS"] = self.__max_time_ms

View File

@ -33,29 +33,6 @@ from pymongo.errors import (CursorNotFound,
from pymongo.message import query
def _check_ok_for_replace(replacement):
"""Validate a replacement document."""
if not isinstance(replacement, collections.Mapping):
raise TypeError('replacement must be a mapping type.')
# Replacement can be {}
if replacement:
first = next(iter(replacement))
if first.startswith('$'):
raise ValueError('replacement can not include $ operators')
def _check_ok_for_update(update):
"""Validate an update document."""
if not isinstance(update, collections.Mapping):
raise TypeError('update must be a mapping type.')
# Update can not be {}
if not update:
raise ValueError('update only works with $ operators')
first = next(iter(update))
if not first.startswith('$'):
raise ValueError('update only works with $ operators')
def _index_list(key_or_list, direction=None):
"""Helper to generate a list of (key, direction) pairs.

View File

@ -14,8 +14,7 @@
"""Option class definitions."""
import collections
from pymongo.common import validate_boolean, validate_is_mapping
class ReturnDocument(object):
"""An enum used with
@ -26,7 +25,7 @@ class ReturnDocument(object):
"""Return the original document before it was updated/replaced, or
``None`` if no document matches the query.
"""
After = True
After = True
"""Return the updated/replaced or inserted document."""
@ -36,10 +35,10 @@ class _WriteOp(object):
__slots__ = ("_filter", "_doc", "_upsert")
def __init__(self, filter=None, doc=None, upsert=None):
if filter is not None and not isinstance(filter, collections.Mapping):
raise TypeError("filter must be a mapping type.")
if upsert is not None and not isinstance(upsert, bool):
raise TypeError("upsert must be True or False")
if filter is not None:
validate_is_mapping("filter", filter)
if upsert is not None:
validate_boolean("upsert", upsert)
self._filter = filter
self._doc = doc
self._upsert = upsert

View File

@ -55,7 +55,9 @@ def _validate_tag_sets(tag_sets):
for tags in tag_sets:
if not isinstance(tags, Mapping):
raise ConfigurationError(
"Tag set %r invalid, must be a mapping." % (tags,))
"Tag set %r invalid, must be an instance of dict, "
"bson.son.SON or other type that inherits from "
"collection.Mapping" % (tags,))
return tag_sets