Repository: Pylons/pyramid

Issue:
pyramid.exceptions.ConfigurationConflictError: <exception str() failed>
**Describe the bug**
While building an app I caused an error who's traceback ending the the following line:
```
pyramid.exceptions.ConfigurationConflictError: <exception str() failed>
```
What caused the error in the first place was that I copied a python package containing my views to a new package called controllers and forgot to delete the original views package. I assume that the configurator failed while trying to commit the config.scan(). Since I couldn't find any information online about the above traceback message I assume that this is probably just some internal problem with Pyramid. 
**To Reproduce**
download from: https://github.com/benkawecki/pypi/tree/error
after setting up run 
``` 
pserve development.ini
```

**Expected behavior**
I expect there to be an error message. 

**Screenshots**
Screenshot of the error message:
<img width="489" alt="screen shot 2019-01-15 at 10 02 44 pm" src="https://user-images.githubusercontent.com/39999125/51224413-c57eb800-1913-11e9-9e0f-b25878a479f5.png">
Screenshot of installed packages:
<img width="488" alt="screen shot 2019-01-15 at 10 24 42 pm" src="https://user-images.githubusercontent.com/39999125/51224563-8b61e600-1914-11e9-9b04-42936f94d4bd.png">



**Additional context**
I'm looking to help out in open-source more this year so if this is an easy fix I would love to see if I can do it!



Discussion:
This project started up for me successfully using the following commands. If you recreate your virtualenv are you still seeing issues? My understanding of the `__pycache__` folders is that they shouldn't need to be deleted because they are unused if a corresponding `.py` file cannot be found, but you could also try deleting them. I tested this using macos 10.14.2, python 3.6.6. Until I can reproduce the issue it's hard to say what a fix would be in pyramid.

```
❯ $(pyenv prefix 3.6.6)/bin/python -m venv env
❯ env/bin/pip install -U pip setuptools
... snip ...
❯ env/bin/pip install -e .
... snip ...
❯ env/bin/pserve development.ini
hello
Starting server in PID 1405.
Serving on http://localhost:6543
Serving on http://localhost:6543
Serving on http://localhost:6543
```
I'm getting getting it on arch linux with python 3.7.2; did you maybe not checkout the error branch @mmerickel?

The error seems to stem, as you mentioned @benkawecki, from having the same view_configs.

The message `exception <str> failed` looks to be a `__str__` method that isn't able to make a string and I believe the method that's choking is at this line:

https://github.com/Pylons/pyramid/blob/7bd9da72c63124c7ca007f4fa2113633bba5c403/src/pyramid/exceptions.py#L95

When self._conflicts contains the following dictionary (specifically the HTTPNotFound object):

```
('view', None, '', 'about', 'd41d8cd98f00b204e9800998ecf8427e'):
    [
        <pyramid.config.actions.ActionInfo object at 0x7f02f0bd25c0>,
        <pyramid.config.actions.ActionInfo object at 0x7f02f0ae3128>
    ],
('view', None, '', 'home', 'd41d8cd98f00b204e9800998ecf8427e'):
    [
        <pyramid.config.actions.ActionInfo object at 0x7f02f0bd2898>,
        <pyramid.config.actions.ActionInfo object at 0x7f02f0ae3278>
    ],
('view', <class 'pyramid.httpexceptions.HTTPNotFound'>, '', None, 'd41d8cd98f00b204e9800998ecf8427e'):
    [
        <pyramid.config.actions.ActionInfo object at 0x7f02f0bd2b00>,
        <pyramid.config.actions.ActionInfo object at 0x7f02f0ae3470>
    ]
}
```

Anyway, just a cursory inspection, but maybe enough to get you started looking into or someone else can shed more insight :)
@mmerickel as @stloma said did you checkout the error branch? That was my bad for not specifying which branch in my initial post. I was able to recreate again though by just copying the controller folder in the master branch and renaming it to views. 
This issue is the `sorted` function choking on trying to sort the `None` value against the `HTTPNotFound` object that I detailed in my previous post.

The resolution is changing however you'd like to sort the conflicts (if they need to be sorted at all?)
Ok I can repro using the error branch. The bug is in sorting the discriminators on Python 3. It's kind of amazing this hasn't come up before. I think instead of sorting the discriminators the best we can do is group by them.

@stloma thanks for the help, I think we are in agreement to just remove the sorting.
@mmerickel 
The following seems to work for me:
```python
class ConfigurationConflictError(ConfigurationError):
    """ Raised when a configuration conflict is detected during action
    processing"""

    def __init__(self, conflicts):
        self._conflicts = conflicts

    def __str__(self):
        r = ["Conflicting configuration actions"]

        items = sorted(self._conflicts.items(), key=lambda x: (x[0][1] is None, x))
        for discriminator, infos in items:
            r.append("  For: %s" % (discriminator,))
            for info in infos:
                for line in str(info).rstrip().split(CR):
                    r.append("    " + line)
        return CR.join(r)
```

The output is below, please let me know if this is the expected behavior:
![image](https://user-images.githubusercontent.com/39999125/51327949-5652b200-1a40-11e9-9ada-5e7afc38fd54.png)

I also considered something like that until I realized that it only works for the view discriminators. This code is block is attempting to sort any arbitrary discriminator and they are simply not orderable. The sort just needs to be removed imo. 
Fair enough, I haven't really dug much into the discriminators. Would it be worth including a timestamp when an action in made (atleast a timestamp that isn't included in the hash)? this way the order is preserved.
Nah. It’s really just sorting between different conflicts.
@mmerickel 

Do you mind if I branch and make a PR removing the sorted function?
@benkawecki we will always welcome PR's :-) Please make sure if you haven't yet, to read and sign CONTRIBUTORS.rst. Thanks!
Sorry @mmerickel and @bertjwregeer, for the delay, working on the PR now. I've always used pytest over unit test so making an appropriate test is taking a bit.

Resolving patch:
diff --git a/src/pyramid/exceptions.py b/src/pyramid/exceptions.py
--- a/src/pyramid/exceptions.py
+++ b/src/pyramid/exceptions.py
@@ -3,8 +3,6 @@
 NotFound = HTTPNotFound  # bw compat
 Forbidden = HTTPForbidden  # bw compat
 
-CR = '\n'
-
 
 class BadCSRFOrigin(HTTPBadRequest):
     """
@@ -92,14 +90,13 @@ def __init__(self, conflicts):
 
     def __str__(self):
         r = ["Conflicting configuration actions"]
-        items = sorted(self._conflicts.items())
-        for discriminator, infos in items:
+        for discriminator, infos in self._conflicts.items():
             r.append("  For: %s" % (discriminator,))
             for info in infos:
-                for line in str(info).rstrip().split(CR):
+                for line in str(info).rstrip().split('\n'):
                     r.append("    " + line)
 
-        return CR.join(r)
+        return '\n'.join(r)
 
 
 class ConfigurationExecutionError(ConfigurationError):


Test patch:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
 import unittest
 
 
@@ -72,7 +73,9 @@ def _makeOne(self, conflicts):
         return ConfigurationConflictError(conflicts)
 
     def test_str(self):
-        conflicts = {'a': ('1', '2', '3'), 'b': ('4', '5', '6')}
+        conflicts = OrderedDict()
+        conflicts['a'] = ('1', '2', '3')
+        conflicts['b'] = ('4', '5', '6')
         exc = self._makeOne(conflicts)
         self.assertEqual(
             str(exc),
@@ -88,6 +91,25 @@ def test_str(self):
     6""",
         )
 
+    def test_non_sortable_discriminators_in_str(self):
+        conflicts = OrderedDict()
+        conflicts['a'] = ('1', '2', '3')
+        conflicts[None] = ('4', '5', '6')
+        exc = self._makeOne(conflicts)
+        self.assertEqual(
+            str(exc),
+            """\
+Conflicting configuration actions
+  For: a
+    1
+    2
+    3
+  For: None
+    4
+    5
+    6""",
+        )
+
 
 class TestConfigurationExecutionError(unittest.TestCase):
     def _makeOne(self, etype, evalue, info):