Repository: scrapy/scrapy

Issue:
String value for order of Scrapy component
If Scrapy component order is defined as a string, it leads to undefined behaviour on Python 2 and to the following errors on Python 3:
```
File "/usr/local/lib/python3.5/site-packages/scrapy/middleware.py", line 58, in from_crawler
 return cls.from_settings(crawler.settings, crawler)
File "/usr/local/lib/python3.5/site-packages/scrapy/middleware.py", line 29, in from_settings
 mwlist = cls._get_mwlist_from_settings(settings)
File "/usr/local/lib/python3.5/site-packages/scrapy/core/spidermw.py", line 21, in _get_mwlist_from_settings
 return build_component_list(settings.getwithbase('SPIDER_MIDDLEWARES'))
File "/usr/local/lib/python3.5/site-packages/scrapy/utils/conf.py", line 47, in build_component_list
 return [k for k, v in sorted(six.iteritems(compdict), key=itemgetter(1))]
builtins.TypeError: unorderable types: str() < int()
```

My guess that 1) order of a Scrapy component should be stated as of integer type (or `None`) and there should be a check somewhere, 2) or the sorting logic should be fixed.


Discussion:
Right, it's probably not stated explicitly nor checked, but the examples given in the docs use integers as far as I can see.
In which case did you see this bug @vshlapakov ?
@redapple Yeah, that's a not too frequent case to worry about, but I found it in our own internal project (it's linked with this issue on above). It's fixed there already, but someone can do the same mistake.
How about something like 
```
for name, value in six.iteritems(compdict):
    if value is not None and not isinstance(value, int):
        try:
            compdict[name] = int(value.strip())
        except:
            raise ValueError('Invalid value {} for component {}, please ' \
                             'provide an integer instead'.format(value, name))
```

somewhere in [`build_component_list`](https://github.com/scrapy/scrapy/blob/master/scrapy/utils/conf.py#L13)?

I could open a PR with some mention in the docs and some tests if you like this approach.
@elacuesta Makes sense to me, that would be nice, thanks. In my case I encountered with a string with an integer inside as order, so it should be covered by the similar changes. Btw there's no need to strip value before converting to int, but that's nitpicking :)

Resolving patch:
diff --git a/scrapy/utils/conf.py b/scrapy/utils/conf.py
--- a/scrapy/utils/conf.py
+++ b/scrapy/utils/conf.py
@@ -1,5 +1,6 @@
 import os
 import sys
+import numbers
 from operator import itemgetter
 
 import six
@@ -34,6 +35,13 @@ def _map_keys(compdict):
             _check_components(compdict)
             return {convert(k): v for k, v in six.iteritems(compdict)}
 
+    def _validate_values(compdict):
+        """Fail if a value in the components dict is not a real number or None."""
+        for name, value in six.iteritems(compdict):
+            if value is not None and not isinstance(value, numbers.Real):
+                raise ValueError('Invalid value {} for component {}, please provide ' \
+                                 'a real number or None instead'.format(value, name))
+
     # BEGIN Backwards compatibility for old (base, custom) call signature
     if isinstance(custom, (list, tuple)):
         _check_components(custom)
@@ -43,6 +51,7 @@ def _map_keys(compdict):
         compdict.update(custom)
     # END Backwards compatibility
 
+    _validate_values(compdict)
     compdict = without_none_values(_map_keys(compdict))
     return [k for k, v in sorted(six.iteritems(compdict), key=itemgetter(1))]
 


Test patch:
diff --git a/tests/test_utils_conf.py b/tests/test_utils_conf.py
--- a/tests/test_utils_conf.py
+++ b/tests/test_utils_conf.py
@@ -62,6 +62,27 @@ def test_duplicate_components_in_basesettings(self):
         self.assertRaises(ValueError, build_component_list, duplicate_bs,
                           convert=lambda x: x.lower())
 
+    def test_valid_numbers(self):
+        # work well with None and numeric values
+        d = {'a': 10, 'b': None, 'c': 15, 'd': 5.0}
+        self.assertEqual(build_component_list(d, convert=lambda x: x),
+                         ['d', 'a', 'c'])
+        d = {'a': 33333333333333333333, 'b': 11111111111111111111, 'c': 22222222222222222222}
+        self.assertEqual(build_component_list(d, convert=lambda x: x),
+                         ['b', 'c', 'a'])
+        # raise exception for invalid values
+        d = {'one': '5'}
+        self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
+        d = {'one': '1.0'}
+        self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
+        d = {'one': [1, 2, 3]}
+        self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
+        d = {'one': {'a': 'a', 'b': 2}}
+        self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
+        d = {'one': 'lorem ipsum',}
+        self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
+
+
 
 class UtilsConfTestCase(unittest.TestCase):