Repository: Pylons/pyramid

Issue:
Helpful exceptions on "a named thing was not found"
[This was originally mentioned in #1601](https://github.com/Pylons/pyramid/issues/1601), but @mmerickel wanted it to be a separate issue, so here goes:

Whenever one uses an invalid kwarg name in `view_config`, or provides an invalid route name for a view, one gets an exception that just states that the given name was wrong. As this is an exceptional condition at startup time, and more so usually meaning that the whole application startup failed, it really would not matter if we used some extra cycles to make the error messages more helpful.

Especially with lots of 3rd-party stuff `include`d in the configurations, it might be really hard to remember how _exactly_ to write the name of a certain predicate, and usually this means more than 5 minutes of googling again. Thus I propose that the exception message would list close matches to these values, so that using `@view_config(method='POST')` would state `Unknown predicate values: {'method': 'POST'}; did you mean request_method`, for example I modified the [raise for invalid predicate names](https://github.com/Pylons/pyramid/blob/master/pyramid/config/util.py#L157) to

```
try:
    from difflib import get_close_matches
    closest = []
    names = [ name for name, _ in ordered ]
    for name in kw:
        closest.extend(get_close_matches(name, names, 3))

    raise ConfigurationError(
        'Unknown predicate values: %r (did you mean %s)'
         % (kw, ','.join(closest)))

except ImportError:
    pass

raise ConfigurationError('Unknown predicate values: %r' % (kw,))
```

Which produces the aforementioned message. The runtime overhead of this is 0 (excepting for a few more bytecodes loaded on startup) for successful configuration and minimal overhead in mostly fatal errors that would make the server not start anyway. The `difflib.get_close_matches` function itself has been in the Python stdlib since 2.1... refactored in a utility function, this close matching could be used for all the cases where the configurator could at configuration time list the allowed values for something.



Discussion:
I think I can handle this

Resolving patch:
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -36,7 +36,7 @@ class not_(object):
 
        config.add_view(
            'mypackage.views.my_view',
-           route_name='ok', 
+           route_name='ok',
            request_method=not_('POST')
            )
 
@@ -69,7 +69,7 @@ def _notted_text(self, val):
         # if the underlying predicate doesnt return a value, it's not really
         # a predicate, it's just something pretending to be a predicate,
         # so dont update the hash
-        if val: 
+        if val:
             val = '!' + val
         return val
 
@@ -90,7 +90,7 @@ def __call__(self, context, request):
 # over = before
 
 class PredicateList(object):
-    
+
     def __init__(self):
         self.sorter = TopologicalSorter()
         self.last_added = None
@@ -152,7 +152,16 @@ def make(self, config, **kw):
                 weights.append(1 << n + 1)
                 preds.append(pred)
         if kw:
-            raise ConfigurationError('Unknown predicate values: %r' % (kw,))
+            from difflib import get_close_matches
+            closest = []
+            names = [ name for name, _ in ordered ]
+            for name in kw:
+                closest.extend(get_close_matches(name, names, 3))
+
+            raise ConfigurationError(
+                'Unknown predicate values: %r (did you mean %s)'
+                % (kw, ','.join(closest))
+            )
         # A "order" is computed for the predicate list.  An order is
         # a scoring.
         #


Test patch:
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -365,6 +365,16 @@ def test_unknown_predicate(self):
         from pyramid.exceptions import ConfigurationError
         self.assertRaises(ConfigurationError, self._callFUT, unknown=1)
 
+    def test_predicate_close_matches(self):
+        from pyramid.exceptions import ConfigurationError
+        with  self.assertRaises(ConfigurationError) as context:
+            self._callFUT(method='GET')
+        expected_msg = (
+            "Unknown predicate values: {'method': 'GET'} "
+            "(did you mean request_method)"
+        )
+        self.assertEqual(context.exception.args[0], expected_msg)
+
     def test_notted(self):
         from pyramid.config import not_
         from pyramid.testing import DummyRequest