Repository: keleshev/schema

Issue:
Formatting error when supplying raw dict as schema to `Or`
I'm not sure if this is valid Schema usage, but I wanted to drop my reproduction here just in case.

I have a schema that looks like this:

```python
  import schema as s

  test1 = s.Or(str, {
      s.Optional("gpu"): str,
      s.Optional("cpu"): str
  },
               error="Got '{}'")

  test1.validate({"cpu": "face", "gpu": "cake"})
```

Trying to run that code throws this error:

```python
self = Schema(<class 'dict'>), data = {'cpu': 'face', 'gpu': 'cake'}

    def validate(self, data):
      Schema = self.__class__
      s = self._schema
>     e = self._error.format(data) if self._error else None
E     KeyError: "'cpu'"

env/lib/python3.6/site-packages/schema.py:372: KeyError
```

When I trace, it looks like what's happening is that `self._error` on the `Or` schema is getting set to the FILLED schema, on this line:

https://github.com/keleshev/schema/blob/master/schema.py#L345

ie, when I insert `print(self._error)` there, I see:

```python
Got '{}'
Got '{'cpu': 'face', 'gpu': 'cake'}'
```

Which creates this case:

```python
>>> "Got '{'cpu': 'face', 'gpu': 'cake'}'".format({'cpu': 'face', 'gpu': 'cake'})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: "'cpu'"
```

The issue's solved if I wrap the dictionary in `s.Schema`... but it might be worth having `Or`, `And` etc do the wrapping for us if it sees a raw dict to prevent this issue.

Hope this helps!

Discussion:
ioerror: @skorokithakis Would it be possible to merge this PR? 
skorokithakis: Certainly, sorry, not sure why I didn't see this. Does it work well for you, @ioerror? Have you tried it?

Patch:
diff --git a/schema.py b/schema.py
index ef5dede..a1ada98 100644
--- a/schema.py
+++ b/schema.py
@@ -220,7 +220,7 @@ class Regex(object):
             if self._pattern.search(data):
                 return data
             else:
-                raise SchemaError("%r does not match %r" % (self, data), e)
+                raise SchemaError("%r does not match %r" % (self, data), e.format(data) if e else None)
         except TypeError:
             raise SchemaError("%r is not string nor buffer" % data, e)
 
@@ -344,7 +344,7 @@ class Schema(object):
     def validate(self, data):
         Schema = self.__class__
         s = self._schema
-        e = self._error.format(data) if self._error else None
+        e = self._error
         i = self._ignore_extra_keys
 
         if isinstance(s, Literal):
@@ -397,7 +397,7 @@ class Schema(object):
                                 except SchemaError as x:
                                     k = "Key '%s' error:" % nkey
                                     message = self._prepend_schema_name(k)
-                                    raise SchemaError([message] + x.autos, [e] + x.errors)
+                                    raise SchemaError([message] + x.autos, [e.format(data) if e else None] + x.errors)
                                 else:
                                     new[nkey] = nvalue
                                     coverage.add(skey)
@@ -408,13 +408,13 @@ class Schema(object):
                 s_missing_keys = ", ".join(repr(k) for k in sorted(missing_keys, key=repr))
                 message = "Missing key%s: %s" % (_plural_s(missing_keys), s_missing_keys)
                 message = self._prepend_schema_name(message)
-                raise SchemaMissingKeyError(message, e)
+                raise SchemaMissingKeyError(message, e.format(data) if e else None)
             if not self._ignore_extra_keys and (len(new) != len(data)):
                 wrong_keys = set(data.keys()) - set(new.keys())
                 s_wrong_keys = ", ".join(repr(k) for k in sorted(wrong_keys, key=repr))
                 message = "Wrong key%s %s in %r" % (_plural_s(wrong_keys), s_wrong_keys, data)
                 message = self._prepend_schema_name(message)
-                raise SchemaWrongKeyError(message, e)
+                raise SchemaWrongKeyError(message, e.format(data) if e else None)
 
             # Apply default-having optionals that haven't been used:
             defaults = set(k for k in s if type(k) is Optional and hasattr(k, "default")) - coverage
@@ -428,36 +428,36 @@ class Schema(object):
             else:
                 message = "%r should be instance of %r" % (data, s.__name__)
                 message = self._prepend_schema_name(message)
-                raise SchemaUnexpectedTypeError(message, e)
+                raise SchemaUnexpectedTypeError(message, e.format(data) if e else None)
         if flavor == VALIDATOR:
             try:
                 return s.validate(data)
             except SchemaError as x:
-                raise SchemaError([None] + x.autos, [e] + x.errors)
+                raise SchemaError([None] + x.autos, [e.format(data) if e else None] + x.errors)
             except BaseException as x:
                 message = "%r.validate(%r) raised %r" % (s, data, x)
                 message = self._prepend_schema_name(message)
-                raise SchemaError(message, e)
+                raise SchemaError(message, e.format(data) if e else None)
         if flavor == CALLABLE:
             f = _callable_str(s)
             try:
                 if s(data):
                     return data
             except SchemaError as x:
-                raise SchemaError([None] + x.autos, [e] + x.errors)
+                raise SchemaError([None] + x.autos, [e.format(data) if e else None] + x.errors)
             except BaseException as x:
                 message = "%s(%r) raised %r" % (f, data, x)
                 message = self._prepend_schema_name(message)
-                raise SchemaError(message, e)
+                raise SchemaError(message, e.format(data) if e else None)
             message = "%s(%r) should evaluate to True" % (f, data)
             message = self._prepend_schema_name(message)
-            raise SchemaError(message, e)
+            raise SchemaError(message, e.format(data) if e else None)
         if s == data:
             return data
         else:
             message = "%r does not match %r" % (s, data)
             message = self._prepend_schema_name(message)
-            raise SchemaError(message, e)
+            raise SchemaError(message, e.format(data) if e else None)
 
     def json_schema(self, schema_id, use_refs=False):
         """Generate a draft-07 JSON schema dict representing the Schema.


Test patch:
diff --git a/test_schema.py b/test_schema.py
index fb7a59f..d8af5e0 100644
--- a/test_schema.py
+++ b/test_schema.py
@@ -1600,3 +1600,18 @@ def test_prepend_schema_name():
         Schema(int, name="custom_schemaname").validate("a")
     except SchemaUnexpectedTypeError as e:
         assert str(e) == "'custom_schemaname' 'a' should be instance of 'int'"
+
+
+def test_dict_literal_error_string():
+    # this is a simplified regression test of the bug in github issue #240
+    assert Schema(Or({"a": 1}, error="error: {}")).is_valid(dict(a=1))
+
+
+def test_callable_error():
+    # this tests for the behavior desired in github pull request #238
+    e = None
+    try:
+        Schema(lambda d: False, error="{}").validate("This is the error message")
+    except SchemaError as ex:
+        e = ex
+    assert e.errors == ["This is the error message"]