Repository: python-babel/babel

Issue:
TypeError: '<' not supported between instances of '_NormalizedString' and '_NormalizedString'
Hi there,
I was getting this message when running this command against the Blender's translation files

```
sphinx-intl update -p build/locale -l "vi" &
```

The listing of the error is here:

```
Not Changed: locale/vi/LC_MESSAGES/animation/actions.po
Traceback (most recent call last):
  File "/usr/local/bin/sphinx-intl", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib64/python3.6/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib64/python3.6/site-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib64/python3.6/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib64/python3.6/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/sphinx_intl/commands.py", line 256, in update
    basic.update(locale_dir, pot_dir, languages)
  File "/usr/local/lib/python3.6/site-packages/sphinx_intl/basic.py", line 53, in update
    cat = c.load_po(po_file)
  File "/usr/local/lib/python3.6/site-packages/sphinx_intl/catalog.py", line 17, in load_po
    cat = pofile.read_po(f)
  File "/usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 345, in read_po
    parser.parse(fileobj)
  File "/usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 276, in parse
    self._process_comment(line)
  File "/usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 235, in _process_comment
    self._finish_current_message()
  File "/usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 175, in _finish_current_message
    self._add_message()
  File "/usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 143, in _add_message
    self.translations.sort()
TypeError: '<' not supported between instances of '_NormalizedString' and '_NormalizedString'
```

Traced down to /usr/local/lib64/python3.6/site-packages/babel/messages/pofile.py", line 143
and found that the class '_NormalizedString' missed the implementations of '__repr__', '__gt__', '__eq__', '__lt__'. Hacked implementation of the above operators appeared to have solved the problem. Listing of the '_NormalizedString' class is listed below:

```
class _NormalizedString(object):

    def __init__(self, *args):
        self._strs = []
        for arg in args:
            self.append(arg)

    def __repr__(self):
        return os.linesep.join(self._strs)

    def __cmp__(self, other):
        if (other == None): return 1

        this_text = str(self)
        other_text = str(other)
        if (this_text == other_text): return 0
        if (this_text < other_text): return -1
        if (this_text > other_text): return 1

    def __gt__(self, other) -> bool:
       return self.__cmp__(other) > 0

    def __eq__(self, other) -> bool:
       return self.__cmp__(other) == 0

    def __lt__(self, other) -> bool:
       return self.__cmp__(other) < 0

    def append(self, s):
        self._strs.append(s.strip())

    def denormalize(self):
        return ''.join(map(unescape, self._strs))

    def __nonzero__(self):
        return bool(self._strs)
```

I have not yet understood the purpose of 'sort' operation in the pofile.py", line 143, as when listing out the translations list before and after the sort, most I see was one block of msgstr element. Anyhow just to let you know the bug and potential fix.

I think the package version is

babel.noarch                                        2.5.3-1

Best regards,
Hoang Tran
<hoangduytran1960@gmail.com>


Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=h1) Report
> Merging [#646](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=desc) into [master](https://codecov.io/gh/python-babel/babel/commit/4fa0c6e5a2d3ae4c10ca160dabe4e3d169dace81?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `88%`.

[![Impacted file tree graph](https://codecov.io/gh/python-babel/babel/pull/646/graphs/tree.svg?width=650&token=lEYaWCjCnu&height=150&src=pr)](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #646      +/-   ##
==========================================
- Coverage   90.55%   90.53%   -0.02%     
==========================================
  Files          24       24              
  Lines        4138     4161      +23     
==========================================
+ Hits         3747     3767      +20     
- Misses        391      394       +3
```


| [Impacted Files](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [babel/messages/pofile.py](https://codecov.io/gh/python-babel/babel/pull/646/diff?src=pr&el=tree#diff-YmFiZWwvbWVzc2FnZXMvcG9maWxlLnB5) | `95.51% <88%> (-0.69%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=footer). Last update [4fa0c6e...af0f0b6](https://codecov.io/gh/python-babel/babel/pull/646?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index fe37631..9eb3a5c 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -16,8 +16,7 @@ import re
 
 from babel.messages.catalog import Catalog, Message
 from babel.util import wraptext
-from babel._compat import text_type
-
+from babel._compat import text_type, cmp
 
 
 def unescape(string):
@@ -99,6 +98,36 @@ class _NormalizedString(object):
     def __nonzero__(self):
         return bool(self._strs)
 
+    __bool__ = __nonzero__
+
+    def __repr__(self):
+        return os.linesep.join(self._strs)
+
+    def __cmp__(self, other):
+        if not other:
+            return 1
+
+        return cmp(text_type(self), text_type(other))
+
+    def __gt__(self, other):
+        return self.__cmp__(other) > 0
+
+    def __lt__(self, other):
+        return self.__cmp__(other) < 0
+
+    def __ge__(self, other):
+        return self.__cmp__(other) >= 0
+
+    def __le__(self, other):
+        return self.__cmp__(other) <= 0
+
+    def __eq__(self, other):
+        return self.__cmp__(other) == 0
+
+    def __ne__(self, other):
+        return self.__cmp__(other) != 0
+
+
 
 class PoFileParser(object):
     """Support class to  read messages from a ``gettext`` PO (portable object) file
@@ -552,7 +581,16 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
 
         if not no_location:
             locs = []
-            for filename, lineno in sorted(message.locations):
+
+            # Attempt to sort the locations.  If we can't do that, for instance
+            # because there are mixed integers and Nones or whatnot (see issue #606)
+            # then give up, but also don't just crash.
+            try:
+                locations = sorted(message.locations)
+            except TypeError:  # e.g. "TypeError: unorderable types: NoneType() < int()"
+                locations = message.locations
+
+            for filename, lineno in locations:
                 if lineno and include_lineno:
                     locs.append(u'%s:%d' % (filename.replace(os.sep, '/'), lineno))
                 else:


Test patch:
diff --git a/tests/messages/test_normalized_string.py b/tests/messages/test_normalized_string.py
new file mode 100644
index 0000000..9c95672
--- /dev/null
+++ b/tests/messages/test_normalized_string.py
@@ -0,0 +1,17 @@
+from babel.messages.pofile import _NormalizedString
+
+
+def test_normalized_string():
+    ab1 = _NormalizedString('a', 'b ')
+    ab2 = _NormalizedString('a', ' b')
+    ac1 = _NormalizedString('a', 'c')
+    ac2 = _NormalizedString('  a', 'c  ')
+    z = _NormalizedString()
+    assert ab1 == ab2 and ac1 == ac2  # __eq__
+    assert ab1 < ac1  # __lt__
+    assert ac1 > ab2  # __gt__
+    assert ac1 >= ac2  # __ge__
+    assert ab1 <= ab2  # __le__
+    assert ab1 != ac1  # __ne__
+    assert not z  # __nonzero__ / __bool__
+    assert sorted([ab1, ab2, ac1])  # the sort order is not stable so we can't really check it, just that we can sort