Blob Blame History Raw
# HG changeset patch
# User Emilio Cobos Álvarez <emilio@crisal.io>
# Date 1625585189 0
# Node ID 2418633d529c6df00a83e973295eed0e891990a6
# Parent  0b5bfe85b344b34894f72176099da78c3849b5cc
Bug 1719144 - Fix various imports for Python 3.10. r=firefox-build-system-reviewers,mhentges

These are enough for me to run bootstrap+configure+build.

Some touch third-party code (gyp), but per discussion in the earlier
versions of this revision that seems fine.

Differential Revision: https://phabricator.services.mozilla.com/D119080

diff --git a/python/mach/mach/config.py b/python/mach/mach/config.py
--- a/python/mach/mach/config.py
+++ b/python/mach/mach/config.py
@@ -17,6 +17,7 @@
 from __future__ import absolute_import, unicode_literals
 
 import collections
+import collections.abc
 import os
 import sys
 import six
@@ -144,7 +145,7 @@
     return _
 
 
-class ConfigSettings(collections.Mapping):
+class ConfigSettings(collections.abc.Mapping):
     """Interface for configuration settings.
 
     This is the main interface to the configuration.
@@ -190,7 +191,7 @@
     will result in exceptions being raised.
     """
 
-    class ConfigSection(collections.MutableMapping, object):
+    class ConfigSection(collections.abc.MutableMapping, object):
         """Represents an individual config section."""
         def __init__(self, config, name, settings):
             object.__setattr__(self, '_config', config)
@@ -312,8 +313,8 @@
         self._config.write(fh)
 
     @classmethod
-    def _format_metadata(cls, provider, section, option, type_cls, description,
-                         default=DefaultValue, extra=None):
+    def _format_metadata(cls, provider, section, option, type_cls, description,
+                         default=DefaultValue, extra=None):
         """Formats and returns the metadata for a setting.
 
         Each setting must have:
@@ -340,10 +340,7 @@
         if isinstance(type_cls, string_types):
             type_cls = TYPE_CLASSES[type_cls]
 
-        meta = {
-            'description': description,
-            'type_cls': type_cls,
-        }
+        meta = {'description': description, 'type_cls': type_cls}
 
         if default != DefaultValue:
             meta['default'] = default
diff --git a/python/mach/mach/decorators.py b/python/mach/mach/decorators.py
--- a/python/mach/mach/decorators.py
+++ b/python/mach/mach/decorators.py
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
 import argparse
 import collections
+import collections.abc
 
 from .base import MachError
 from .registrar import Registrar
 from mozbuild.base import MachCommandBase
 
 
 class _MachCommand(object):
     """Container for mach command metadata."""
@@ -140,7 +141,7 @@
               'Conditions argument must take a list ' + \
               'of functions. Found %s instead.'
 
-        if not isinstance(command.conditions, collections.Iterable):
+        if not isinstance(command.conditions, collections.abc.Iterable):
             msg = msg % (command.name, type(command.conditions))
             raise MachError(msg)
 
diff --git a/python/mach/mach/main.py b/python/mach/mach/main.py
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -11,17 +11,17 @@ import argparse
 import codecs
 import errno
 import imp
 import logging
 import os
 import sys
 import traceback
 import uuid
-from collections import Iterable
+from collections.abc import Iterable
 
 from six import string_types
 
 from .base import (
     CommandContext,
     MachError,
     MissingFileError,
     NoCommandError,
diff --git a/python/mozbuild/mozbuild/backend/configenvironment.py b/python/mozbuild/mozbuild/backend/configenvironment.py
--- a/python/mozbuild/mozbuild/backend/configenvironment.py
+++ b/python/mozbuild/mozbuild/backend/configenvironment.py
@@ -4,17 +4,18 @@
 
 from __future__ import absolute_import, print_function
 
 import os
 import six
 import sys
 import json
 
-from collections import Iterable, OrderedDict
+from collections.abc import Iterable
+from collections import OrderedDict
 from types import ModuleType
 
 import mozpack.path as mozpath
 
 from mozbuild.util import (
     FileAvoidWrite,
     memoized_property,
     ReadOnlyDict,
@@ -63,10 +64,7 @@
                     compile(source, path, 'exec', dont_inherit=1)
                 )
 
-        g = {
-            '__builtins__': __builtins__,
-            '__file__': path,
-        }
+        g = {"__builtins__": __builtins__, "__file__": path}
         l = {}
         try:
             exec(code_cache[path][1], g, l)
diff --git a/python/mozbuild/mozbuild/makeutil.py b/python/mozbuild/mozbuild/makeutil.py
--- a/python/mozbuild/mozbuild/makeutil.py
+++ b/python/mozbuild/mozbuild/makeutil.py
@@ -7,7 +7,7 @@
 import os
 import re
 import six
-from collections import Iterable
+from collections.abc import Iterable
 
 
 class Makefile(object):
diff --git a/python/mozbuild/mozbuild/util.py b/python/mozbuild/mozbuild/util.py
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -4,16 +4,17 @@
 
 # This file contains miscellaneous utility functions that don't belong anywhere
 # in particular.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import argparse
 import collections
+import collections.abc
 import ctypes
 import difflib
 import errno
 import functools
 import hashlib
 import io
 import itertools
 import os
@@ -782,7 +783,7 @@
         self._strings = StrictOrderingOnAppendList()
         self._children = {}
 
-    class StringListAdaptor(collections.Sequence):
+    class StringListAdaptor(collections.abc.Sequence):
         def __init__(self, hsl):
             self._hsl = hsl
 
diff --git a/taskcluster/taskgraph/util/schema.py b/taskcluster/taskgraph/util/schema.py
--- a/taskcluster/taskgraph/util/schema.py
+++ b/taskcluster/taskgraph/util/schema.py
@@ -6,7 +6,7 @@
 
 import re
 import pprint
-import collections
+import collections.abc
 import voluptuous
 
 from six import text_type, iteritems
@@ -160,7 +160,7 @@
                     'Unexpected type in YAML schema: {} @ {}'.format(
                         type(k).__name__, path))
 
-        if isinstance(sch, collections.Mapping):
+        if isinstance(sch, collections.abc.Mapping):
             for k, v in iteritems(sch):
                 child = "{}[{!r}]".format(path, k)
                 check_identifier(child, k)
diff --git a/third_party/python/gyp/pylib/gyp/common.py b/third_party/python/gyp/pylib/gyp/common.py
--- a/third_party/python/gyp/pylib/gyp/common.py
+++ b/third_party/python/gyp/pylib/gyp/common.py
@@ -1,15 +1,16 @@
 # Copyright (c) 2012 Google Inc. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
 from __future__ import with_statement
 
 import collections
+import collections.abc
 import errno
 import filecmp
 import os.path
 import re
 import tempfile
 import sys
 
 
@@ -489,17 +490,17 @@ def uniquer(seq, idfun=None):
         marker = idfun(item)
         if marker in seen: continue
         seen[marker] = 1
         result.append(item)
     return result
 
 
 # Based on http://code.activestate.com/recipes/576694/.
-class OrderedSet(collections.MutableSet):
+class OrderedSet(collections.abc.MutableSet):
   def __init__(self, iterable=None):
     self.end = end = []
     end += [None, end, end]         # sentinel node for doubly linked list
     self.map = {}                   # key --> [key, prev, next]
     if iterable is not None:
       self |= iterable
 
   def __len__(self):


# HG changeset patch
# User Emilio Cobos Álvarez <emilio@crisal.io>
# Date 1625657350 -7200
# Node ID 6b2a1a09cd4735386414483dd4050c25f9e58e86
# Parent  d2ab10b1b1619f8e2473837ed9043ad28c842aa5
Bug 1719144 - Keep some code python2 compatible since we still run it in some windows talos jobs.

MANUAL PUSH: Orange fix CLOSED TREE


diff --git a/testing/mozbase/manifestparser/manifestparser/filters.py b/testing/mozbase/manifestparser/manifestparser/filters.py
--- a/testing/mozbase/manifestparser/manifestparser/filters.py
+++ b/testing/mozbase/manifestparser/manifestparser/filters.py
@@ -12,7 +12,7 @@
 
 import itertools
 import os
-from collections import defaultdict, MutableSequence
+from collections.abc import MutableSequence
 
 import six
 from six import string_types
--- a/third_party/python/pyyaml/lib3/yaml/constructor.orig.py	2021-07-13 12:14:20.000000000 +0200
+++ b/third_party/python/pyyaml/lib3/yaml/constructor.py	2021-07-14 14:36:31.579176122 +0200
@@ -5,7 +5,7 @@
 from .error import *
 from .nodes import *
 
-import collections, datetime, base64, binascii, re, sys, types
+import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
@@ -123,7 +123,7 @@
         mapping = {}
         for key_node, value_node in node.value:
             key = self.construct_object(key_node, deep=deep)
-            if not isinstance(key, collections.Hashable):
+            if not isinstance(key, collections.abc.Hashable):
                 raise ConstructorError("while constructing a mapping", node.start_mark,
                         "found unhashable key", key_node.start_mark)
             value = self.construct_object(value_node, deep=deep)
--- a/third_party/python/requests/requests/cookies.orig.py	2021-08-02 21:40:27.000000000 +0200
+++ b/third_party/python/requests/requests/cookies.py	2021-08-03 21:46:38.917998970 +0200
@@ -10,6 +10,7 @@
 import time
 import calendar
 import collections
+import collections.abc
 from .compat import cookielib, urlparse, urlunparse, Morsel
 
 try:
@@ -161,7 +162,7 @@
     Use .get and .set and include domain and path args in order to be more specific."""
 
 
-class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
+class RequestsCookieJar(cookielib.CookieJar, collections.abc.MutableMapping):
     """Compatibility class; is a cookielib.CookieJar, but exposes a dict
     interface.
 
--- a/third_party/python/requests/requests/utils.orig.py	2021-08-02 21:40:27.000000000 +0200
+++ b/third_party/python/requests/requests/utils.py	2021-08-03 21:47:12.396466647 +0200
@@ -12,6 +12,7 @@
 import cgi
 import codecs
 import collections
+import collections.abc
 import io
 import os
 import platform
--- a/third_party/python/requests/requests/structures.orig.py	2021-08-02 21:40:27.000000000 +0200
+++ b/third_party/python/requests/requests/structures.py	2021-08-04 08:03:34.858332348 +0200
@@ -9,9 +9,10 @@
 """
 
 import collections
+import collections.abc
 
 
-class CaseInsensitiveDict(collections.MutableMapping):
+class CaseInsensitiveDict(collections.abc.MutableMapping):
     """
     A case-insensitive ``dict``-like object.
 
diff -up a/third_party/python/voluptuous/voluptuous-0.11.5.dist-info/RECORD.build-python-2 b/third_party/python/voluptuous/voluptuous-0.11.5.dist-info/RECORD
diff -up a/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/COPYING.build-python-2 b/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/COPYING
--- a/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/COPYING.build-python-2	2021-07-21 09:42:47.774128166 +0200
+++ b/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/COPYING	2021-07-21 09:42:47.774128166 +0200
@@ -0,0 +1,25 @@
+Copyright (c) 2010, Alec Thomas
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+ - Neither the name of SwapOff.org nor the names of its contributors may
+   be used to endorse or promote products derived from this software without
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff -up a/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/RECORD.build-python-2 b/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/RECORD
--- a/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/RECORD.build-python-2	2021-07-21 09:42:47.774128166 +0200
+++ b/third_party/python/voluptuous/voluptuous-0.12.1.dist-info/RECORD	2021-07-21 09:42:47.774128166 +0200
@@ -0,0 +1,11 @@
+voluptuous/__init__.py,sha256=tSYWPAIWee6YwcMK8hxmaiagG_swokUMeH8MluJLWZM,203
+voluptuous/error.py,sha256=fLRmJwKp0bqRGgBM34ztg9MTxhEOf465sbQcvJlEtAk,4026
+voluptuous/humanize.py,sha256=hZlhdN4aVeGDIXdtSTeyEbmku65SDPRuut3mOfuRQP0,1606
+voluptuous/schema_builder.py,sha256=xVJpf3uJMyS1CKwzDw3rEK39ebqDiF_A2Kbq4VnZ3Aw,43677
+voluptuous/util.py,sha256=RXLZ2b5y-A4az3teG6UpCx2UZcXpS11sIVCdORyKar8,3150
+voluptuous/validators.py,sha256=xZgyKH-EVqUHCHral5gzViXq4HfEjJEsGdQy7z6llc0,32798
+voluptuous-0.12.1.dist-info/COPYING,sha256=JHtJdren-k2J2Vh8qlCVVh60bcVFfyJ59ipitUUq3qk,1486
+voluptuous-0.12.1.dist-info/METADATA,sha256=OdEydy5NydPFFzAhP8qj_YqJsQPQvoIt5ZT1t8B14Ok,20120
+voluptuous-0.12.1.dist-info/WHEEL,sha256=S6zePDbUAjzMmpYOg2cHDxuYFWw7WiOXt6ogM6hIB5Q,92
+voluptuous-0.12.1.dist-info/top_level.txt,sha256=TTdVb7M-vndb67UqTmAxuVjpAUakrlAWJYqvo3w4Iqc,11
+voluptuous-0.12.1.dist-info/RECORD,,
diff -up a/third_party/python/voluptuous/voluptuous/error.py.build-python-2 b/third_party/python/voluptuous/voluptuous/error.py
--- a/third_party/python/voluptuous/voluptuous/error.py.build-python-2	2021-07-05 21:16:03.000000000 +0200
+++ b/third_party/python/voluptuous/voluptuous/error.py	2021-07-21 09:42:47.775128197 +0200
@@ -142,11 +142,11 @@ class BooleanInvalid(Invalid):
 
 
 class UrlInvalid(Invalid):
-    """The value is not a url."""
+    """The value is not a URL."""
 
 
 class EmailInvalid(Invalid):
-    """The value is not a email."""
+    """The value is not an email address."""
 
 
 class FileInvalid(Invalid):
diff -up a/third_party/python/voluptuous/voluptuous/__init__.py.build-python-2 b/third_party/python/voluptuous/voluptuous/__init__.py
--- a/third_party/python/voluptuous/voluptuous/__init__.py.build-python-2	2021-07-05 21:16:03.000000000 +0200
+++ b/third_party/python/voluptuous/voluptuous/__init__.py	2021-07-21 09:42:47.775128197 +0200
@@ -5,5 +5,5 @@ from voluptuous.validators import *
 from voluptuous.util import *
 from voluptuous.error import *
 
-__version__ = '0.11.5'
+__version__ = '0.12.1'
 __author__ = 'alecthomas'
diff -up a/third_party/python/voluptuous/voluptuous/schema_builder.py.build-python-2 b/third_party/python/voluptuous/voluptuous/schema_builder.py
--- a/third_party/python/voluptuous/voluptuous/schema_builder.py.build-python-2	2021-07-05 21:16:03.000000000 +0200
+++ b/third_party/python/voluptuous/voluptuous/schema_builder.py	2021-07-21 09:42:47.775128197 +0200
@@ -22,6 +22,11 @@ else:
     def iteritems(d):
         return d.iteritems()
 
+if sys.version_info >= (3, 3):
+    _Mapping = collections.abc.Mapping
+else:
+    _Mapping = collections.Mapping
+
 """Schema validation for Python data structures.
 
 Given eg. a nested data structure like this:
@@ -280,7 +285,7 @@ class Schema(object):
             return schema.__voluptuous_compile__(self)
         if isinstance(schema, Object):
             return self._compile_object(schema)
-        if isinstance(schema, collections.Mapping):
+        if isinstance(schema, _Mapping):
             return self._compile_dict(schema)
         elif isinstance(schema, list):
             return self._compile_list(schema)
@@ -610,11 +615,11 @@ class Schema(object):
             if not isinstance(data, seq_type):
                 raise er.SequenceTypeInvalid('expected a %s' % seq_type_name, path)
 
-            # Empty seq schema, allow any data.
+            # Empty seq schema, reject any data.
             if not schema:
                 if data:
                     raise er.MultipleInvalid([
-                        er.ValueInvalid('not a valid value', [value]) for value in data
+                        er.ValueInvalid('not a valid value', path if path else data)
                     ])
                 return data
 
@@ -735,7 +740,7 @@ class Schema(object):
 
         result = self.schema.copy()
 
-        # returns the key that may have been passed as arugment to Marker constructor
+        # returns the key that may have been passed as an argument to Marker constructor
         def key_literal(key):
             return (key.schema if isinstance(key, Marker) else key)
 
@@ -771,9 +776,10 @@ class Schema(object):
                 result[key] = value
 
         # recompile and send old object
+        result_cls = type(self)
         result_required = (required if required is not None else self.required)
         result_extra = (extra if extra is not None else self.extra)
-        return Schema(result, required=result_required, extra=result_extra)
+        return result_cls(result, required=result_required, extra=result_extra)
 
 
 def _compile_scalar(schema):
@@ -809,7 +815,7 @@ def _compile_scalar(schema):
         def validate_callable(path, data):
             try:
                 return schema(data)
-            except ValueError as e:
+            except ValueError:
                 raise er.ValueInvalid('not a valid value', path)
             except er.Invalid as e:
                 e.prepend(path)
@@ -1121,8 +1127,11 @@ class Inclusive(Optional):
     True
     """
 
-    def __init__(self, schema, group_of_inclusion, msg=None):
-        super(Inclusive, self).__init__(schema, msg=msg)
+    def __init__(self, schema, group_of_inclusion,
+                 msg=None, description=None, default=UNDEFINED):
+        super(Inclusive, self).__init__(schema, msg=msg,
+                                        default=default,
+                                        description=description)
         self.group_of_inclusion = group_of_inclusion
 
 
diff -up a/third_party/python/voluptuous/voluptuous/util.py.build-python-2 b/third_party/python/voluptuous/voluptuous/util.py
--- a/third_party/python/voluptuous/voluptuous/util.py.build-python-2	2021-07-05 21:16:03.000000000 +0200
+++ b/third_party/python/voluptuous/voluptuous/util.py	2021-07-21 09:42:47.776128229 +0200
@@ -7,6 +7,14 @@ from voluptuous import validators
 __author__ = 'tusharmakkar08'
 
 
+def _fix_str(v):
+    if sys.version_info[0] == 2 and isinstance(v, unicode):
+        s = v
+    else:
+        s = str(v)
+    return s
+
+
 def Lower(v):
     """Transform a string to lower case.
 
@@ -14,7 +22,7 @@ def Lower(v):
     >>> s('HI')
     'hi'
     """
-    return str(v).lower()
+    return _fix_str(v).lower()
 
 
 def Upper(v):
@@ -24,7 +32,7 @@ def Upper(v):
     >>> s('hi')
     'HI'
     """
-    return str(v).upper()
+    return _fix_str(v).upper()
 
 
 def Capitalize(v):
@@ -34,7 +42,7 @@ def Capitalize(v):
     >>> s('hello world')
     'Hello world'
     """
-    return str(v).capitalize()
+    return _fix_str(v).capitalize()
 
 
 def Title(v):
@@ -44,7 +52,7 @@ def Title(v):
     >>> s('hello world')
     'Hello World'
     """
-    return str(v).title()
+    return _fix_str(v).title()
 
 
 def Strip(v):
@@ -54,7 +62,7 @@ def Strip(v):
     >>> s('  hello world  ')
     'hello world'
     """
-    return str(v).strip()
+    return _fix_str(v).strip()
 
 
 class DefaultTo(object):
diff -up a/third_party/python/voluptuous/voluptuous/validators.py.build-python-2 b/third_party/python/voluptuous/voluptuous/validators.py
--- a/third_party/python/voluptuous/voluptuous/validators.py.build-python-2	2021-07-05 21:16:03.000000000 +0200
+++ b/third_party/python/voluptuous/voluptuous/validators.py	2021-07-21 09:42:47.776128229 +0200
@@ -192,15 +192,26 @@ class _WithSubValidators(object):
     def __init__(self, *validators, **kwargs):
         self.validators = validators
         self.msg = kwargs.pop('msg', None)
+        self.required = kwargs.pop('required', False)
+        self.discriminant = kwargs.pop('discriminant', None)
 
     def __voluptuous_compile__(self, schema):
-        self._compiled = [
-            schema._compile(v)
-            for v in self.validators
-        ]
+        self._compiled = []
+        old_required = schema.required
+        self.schema = schema
+        for v in self.validators:
+            schema.required = self.required
+            self._compiled.append(schema._compile(v))
+        schema.required = old_required
         return self._run
 
     def _run(self, path, value):
+        if self.discriminant is not None:
+            self._compiled = [
+                self.schema._compile(v)
+                for v in self.discriminant(value, self.validators)
+            ]
+
         return self._exec(self._compiled, value, path)
 
     def __call__(self, v):
@@ -218,7 +229,7 @@ class Any(_WithSubValidators):
     """Use the first validated value.
 
     :param msg: Message to deliver to user if validation fails.
-    :param kwargs: All other keyword arguments are passed to the sub-Schema constructors.
+    :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
     :returns: Return value of the first validator that passes.
 
     >>> validate = Schema(Any('true', 'false',
@@ -262,13 +273,57 @@ class Any(_WithSubValidators):
 Or = Any
 
 
+class Union(_WithSubValidators):
+    """Use the first validated value among those selected by discriminant.
+
+    :param msg: Message to deliver to user if validation fails.
+    :param discriminant(value, validators): Returns the filtered list of validators based on the value.
+    :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
+    :returns: Return value of the first validator that passes.
+
+    >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'},
+    ...                         discriminant=lambda val, alt: filter(
+    ...                         lambda v : v['type'] == val['type'] , alt)))
+    >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'}
+    True
+    >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"):
+    ...   validate({'type':'b', 'b_val':'5'})
+
+    ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked
+
+    Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']"
+    """
+
+    def _exec(self, funcs, v, path=None):
+        error = None
+        for func in funcs:
+            try:
+                if path is None:
+                    return func(v)
+                else:
+                    return func(path, v)
+            except Invalid as e:
+                if error is None or len(e.path) > len(error.path):
+                    error = e
+        else:
+            if error:
+                raise error if self.msg is None else AnyInvalid(
+                    self.msg, path=path)
+            raise AnyInvalid(self.msg or 'no valid value found',
+                             path=path)
+
+
+# Convenience alias
+Switch = Union
+
+
 class All(_WithSubValidators):
     """Value must pass all validators.
 
     The output of each validator is passed as input to the next.
 
     :param msg: Message to deliver to user if validation fails.
-    :param kwargs: All other keyword arguments are passed to the sub-Schema constructors.
+    :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
 
     >>> validate = Schema(All('10', Coerce(int)))
     >>> validate('10')
@@ -303,7 +358,7 @@ class Match(object):
     >>> with raises(MultipleInvalid, 'expected string or buffer'):
     ...   validate(123)
 
-    Pattern may also be a _compiled regular expression:
+    Pattern may also be a compiled regular expression:
 
     >>> validate = Schema(Match(re.compile(r'0x[A-F0-9]+', re.I)))
     >>> validate('0x123ef4')
@@ -361,38 +416,38 @@ def _url_validation(v):
     return parsed
 
 
-@message('expected an Email', cls=EmailInvalid)
+@message('expected an email address', cls=EmailInvalid)
 def Email(v):
-    """Verify that the value is an Email or not.
+    """Verify that the value is an email address or not.
 
     >>> s = Schema(Email())
-    >>> with raises(MultipleInvalid, 'expected an Email'):
+    >>> with raises(MultipleInvalid, 'expected an email address'):
     ...   s("a.com")
-    >>> with raises(MultipleInvalid, 'expected an Email'):
+    >>> with raises(MultipleInvalid, 'expected an email address'):
     ...   s("a@.com")
-    >>> with raises(MultipleInvalid, 'expected an Email'):
+    >>> with raises(MultipleInvalid, 'expected an email address'):
     ...   s("a@.com")
     >>> s('t@x.com')
     't@x.com'
     """
     try:
         if not v or "@" not in v:
-            raise EmailInvalid("Invalid Email")
+            raise EmailInvalid("Invalid email address")
         user_part, domain_part = v.rsplit('@', 1)
 
         if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)):
-            raise EmailInvalid("Invalid Email")
+            raise EmailInvalid("Invalid email address")
         return v
     except:
         raise ValueError
 
 
-@message('expected a Fully qualified domain name URL', cls=UrlInvalid)
+@message('expected a fully qualified domain name URL', cls=UrlInvalid)
 def FqdnUrl(v):
-    """Verify that the value is a Fully qualified domain name URL.
+    """Verify that the value is a fully qualified domain name URL.
 
     >>> s = Schema(FqdnUrl())
-    >>> with raises(MultipleInvalid, 'expected a Fully qualified domain name URL'):
+    >>> with raises(MultipleInvalid, 'expected a fully qualified domain name URL'):
     ...   s("http://localhost/")
     >>> s('http://w3.org')
     'http://w3.org'
@@ -423,14 +478,14 @@ def Url(v):
         raise ValueError
 
 
-@message('not a file', cls=FileInvalid)
+@message('Not a file', cls=FileInvalid)
 @truth
 def IsFile(v):
     """Verify the file exists.
 
     >>> os.path.basename(IsFile()(__file__)).startswith('validators.py')
     True
-    >>> with raises(FileInvalid, 'not a file'):
+    >>> with raises(FileInvalid, 'Not a file'):
     ...   IsFile()("random_filename_goes_here.py")
     >>> with raises(FileInvalid, 'Not a file'):
     ...   IsFile()(None)
@@ -445,7 +500,7 @@ def IsFile(v):
         raise FileInvalid('Not a file')
 
 
-@message('not a directory', cls=DirInvalid)
+@message('Not a directory', cls=DirInvalid)
 @truth
 def IsDir(v):
     """Verify the directory exists.
@@ -487,11 +542,11 @@ def PathExists(v):
         raise PathInvalid("Not a Path")
 
 
-def Maybe(validator):
+def Maybe(validator, msg=None):
     """Validate that the object matches given validator or is None.
 
-    :raises Invalid: if the value does not match the given validator and is not
-        None
+    :raises Invalid: If the value does not match the given validator and is not
+        None.
 
     >>> s = Schema(Maybe(int))
     >>> s(10)
@@ -500,7 +555,7 @@ def Maybe(validator):
     ...  s("string")
 
     """
-    return Any(None, validator)
+    return Any(validator, None, msg=msg)
 
 
 class Range(object):
@@ -533,23 +588,30 @@ class Range(object):
         self.msg = msg
 
     def __call__(self, v):
-        if self.min_included:
-            if self.min is not None and not v >= self.min:
-                raise RangeInvalid(
-                    self.msg or 'value must be at least %s' % self.min)
-        else:
-            if self.min is not None and not v > self.min:
-                raise RangeInvalid(
-                    self.msg or 'value must be higher than %s' % self.min)
-        if self.max_included:
-            if self.max is not None and not v <= self.max:
-                raise RangeInvalid(
-                    self.msg or 'value must be at most %s' % self.max)
-        else:
-            if self.max is not None and not v < self.max:
-                raise RangeInvalid(
-                    self.msg or 'value must be lower than %s' % self.max)
-        return v
+        try:
+            if self.min_included:
+                if self.min is not None and not v >= self.min:
+                    raise RangeInvalid(
+                        self.msg or 'value must be at least %s' % self.min)
+            else:
+                if self.min is not None and not v > self.min:
+                    raise RangeInvalid(
+                        self.msg or 'value must be higher than %s' % self.min)
+            if self.max_included:
+                if self.max is not None and not v <= self.max:
+                    raise RangeInvalid(
+                        self.msg or 'value must be at most %s' % self.max)
+            else:
+                if self.max is not None and not v < self.max:
+                    raise RangeInvalid(
+                        self.msg or 'value must be lower than %s' % self.max)
+
+            return v
+
+        # Objects that lack a partial ordering, e.g. None or strings will raise TypeError
+        except TypeError:
+            raise RangeInvalid(
+                self.msg or 'invalid value or type (must have a partial ordering)')
 
     def __repr__(self):
         return ('Range(min=%r, max=%r, min_included=%r,'
@@ -579,11 +641,17 @@ class Clamp(object):
         self.msg = msg
 
     def __call__(self, v):
-        if self.min is not None and v < self.min:
-            v = self.min
-        if self.max is not None and v > self.max:
-            v = self.max
-        return v
+        try:
+            if self.min is not None and v < self.min:
+                v = self.min
+            if self.max is not None and v > self.max:
+                v = self.max
+            return v
+
+        # Objects that lack a partial ordering, e.g. None or strings will raise TypeError
+        except TypeError:
+            raise RangeInvalid(
+                self.msg or 'invalid value or type (must have a partial ordering)')
 
     def __repr__(self):
         return 'Clamp(min=%s, max=%s)' % (self.min, self.max)
@@ -598,13 +666,19 @@ class Length(object):
         self.msg = msg
 
     def __call__(self, v):
-        if self.min is not None and len(v) < self.min:
-            raise LengthInvalid(
-                self.msg or 'length of value must be at least %s' % self.min)
-        if self.max is not None and len(v) > self.max:
-            raise LengthInvalid(
-                self.msg or 'length of value must be at most %s' % self.max)
-        return v
+        try:
+            if self.min is not None and len(v) < self.min:
+                raise LengthInvalid(
+                    self.msg or 'length of value must be at least %s' % self.min)
+            if self.max is not None and len(v) > self.max:
+                raise LengthInvalid(
+                    self.msg or 'length of value must be at most %s' % self.max)
+            return v
+
+        # Objects that havbe no length e.g. None or strings will raise TypeError
+        except TypeError:
+            raise RangeInvalid(
+                self.msg or 'invalid value or type')
 
     def __repr__(self):
         return 'Length(min=%s, max=%s)' % (self.min, self.max)
@@ -663,7 +737,8 @@ class In(object):
         except TypeError:
             check = True
         if check:
-            raise InInvalid(self.msg or 'value is not allowed')
+            raise InInvalid(self.msg or 
+                            'value must be one of {}'.format(sorted(self.container)))
         return v
 
     def __repr__(self):
@@ -683,7 +758,8 @@ class NotIn(object):
         except TypeError:
             check = True
         if check:
-            raise NotInInvalid(self.msg or 'value is not allowed')
+            raise NotInInvalid(self.msg or 
+                               'value must not be one of {}'.format(sorted(self.container)))
         return v
 
     def __repr__(self):
@@ -722,7 +798,7 @@ class ExactSequence(object):
     the validators.
 
     :param msg: Message to deliver to user if validation fails.
-    :param kwargs: All other keyword arguments are passed to the sub-Schema
+    :param kwargs: All other keyword arguments are passed to the sub-schema
         constructors.
 
     >>> from voluptuous import Schema, ExactSequence
@@ -887,7 +963,7 @@ class Unordered(object):
 class Number(object):
     """
     Verify the number of digits that are present in the number(Precision),
-    and the decimal places(Scale)
+    and the decimal places(Scale).
 
     :raises Invalid: If the value does not match the provided Precision and Scale.
 
@@ -951,13 +1027,13 @@ class SomeOf(_WithSubValidators):
     The output of each validator is passed as input to the next.
 
     :param min_valid: Minimum number of valid schemas.
-    :param validators: a list of schemas or validators to match input against
+    :param validators: List of schemas or validators to match input against.
     :param max_valid: Maximum number of valid schemas.
     :param msg: Message to deliver to user if validation fails.
-    :param kwargs: All other keyword arguments are passed to the sub-Schema constructors.
+    :param kwargs: All other keyword arguments are passed to the sub-schema constructors.
 
-    :raises NotEnoughValid: if the minimum number of validations isn't met
-    :raises TooManyValid: if the more validations than the given amount is met
+    :raises NotEnoughValid: If the minimum number of validations isn't met.
+    :raises TooManyValid: If the maximum number of validations is exceeded.
 
     >>> validate = Schema(SomeOf(min_valid=2, validators=[Range(1, 5), Any(float, int), 6.6]))
     >>> validate(6.6)

    Add to Phrasebook
        No word lists for English (USA) → Italian...
        Create a new word list...
    Copy