Repository: evennia/evennia

Issue:
[BUG] utils.latinify - AttributeError: 'str'/'int' object has no attribute 'decode'
#### Describe the bug
The `latinify` utility function seems to error out no matter what input is provided.

#### To Reproduce
```
class LatinifyTest(EvenniaTest):
    """
    utils._UNICODE_MAP may need some additional entries to resolve these tests--
    
        LEFT DOUBLE QUOTATION MARK: "
        RIGHT DOUBLE QUOTATION MARK: "
    
    """
    def setUp(self):
        super().setUp()
        
        self.example_str = 'It says, “plugh.”'
        self.example_ustr = u'It says, “plugh.”'
        
        self.expected_output = 'It says, "plugh."'
    
    def test_plain_string(self):
        result = latinify(self.example_str)
        self.assertEqual(result, self.expected_output)
        
    def test_unicode_string(self):
        result = latinify(self.example_ustr)
        self.assertEqual(result, self.expected_output)
        
    def test_encoded_string(self):
        result = latinify(self.example_str.encode('utf8'))
        self.assertEqual(result, self.expected_output)
        
    def test_byte_string(self):
        result = latinify(to_bytes(self.example_str))
        self.assertEqual(result, self.expected_output)
```

```
======================================================================
ERROR: test_byte_string (typeclasses.tests.LatinifyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/workspace/demogame/typeclasses/tests.py", line 43, in test_byte_string
    result = latinify(to_bytes(self.example_str))
  File "/home/ubuntu/workspace/evennia/evennia/utils/utils.py", line 781, in latinify
    ch = unich.decode('ascii')
AttributeError: 'int' object has no attribute 'decode'

======================================================================
ERROR: test_encoded_string (typeclasses.tests.LatinifyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/workspace/demogame/typeclasses/tests.py", line 39, in test_encoded_string
    result = latinify(self.example_str.encode('utf8'))
  File "/home/ubuntu/workspace/evennia/evennia/utils/utils.py", line 781, in latinify
    ch = unich.decode('ascii')
AttributeError: 'int' object has no attribute 'decode'

======================================================================
ERROR: test_plain_string (typeclasses.tests.LatinifyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/workspace/demogame/typeclasses/tests.py", line 31, in test_plain_string
    result = latinify(self.example_str)
  File "/home/ubuntu/workspace/evennia/evennia/utils/utils.py", line 781, in latinify
    ch = unich.decode('ascii')
AttributeError: 'str' object has no attribute 'decode'

======================================================================
ERROR: test_unicode_string (typeclasses.tests.LatinifyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/workspace/demogame/typeclasses/tests.py", line 35, in test_unicode_string
    result = latinify(self.example_ustr)
  File "/home/ubuntu/workspace/evennia/evennia/utils/utils.py", line 781, in latinify
    ch = unich.decode('ascii')
AttributeError: 'str' object has no attribute 'decode'

----------------------------------------------------------------------
Ran 4 tests in 2.104s

FAILED (errors=4)
```

#### Environment, Evennia version, OS etc
```
Evennia 0.9.0 (rev c6aa943) (rev c6aa943)
    OS: posix
    Python: 3.7.4
    Twisted: 19.7.0
    Django: 2.2.4
```


Discussion:
Will be opening up a PR for this momentarily which resolves the issue and adds the unit tests provided in the description. Note: current solution is only tested on Python 3 (3.7.4) so far, I should be able to manage testing it on Python 2 if that is a requirement.

Resolving patch:
diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py
--- a/evennia/utils/utils.py
+++ b/evennia/utils/utils.py
@@ -761,7 +761,10 @@ def dbref_to_obj(inp, objclass, raise_errors=True):
     "EN DASH": "-",
     "HORIZONTAL BAR": "-",
     "HORIZONTAL ELLIPSIS": "...",
+    "LEFT SINGLE QUOTATION MARK": "'",
     "RIGHT SINGLE QUOTATION MARK": "'",
+    "LEFT DOUBLE QUOTATION MARK": '"',
+    "RIGHT DOUBLE QUOTATION MARK": '"',
 }
 
 
@@ -788,10 +791,13 @@ def latinify(string, default="?", pure_ascii=False):
 
     from unicodedata import name
 
+    if isinstance(string, bytes):
+        string = string.decode("utf8")
+
     converted = []
     for unich in iter(string):
         try:
-            ch = unich.decode("ascii")
+            ch = unich.encode("utf8").decode("ascii")
         except UnicodeDecodeError:
             # deduce a latin letter equivalent from the Unicode data
             # point name; e.g., since `name(u'á') == 'LATIN SMALL


Test patch:
diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py
--- a/evennia/utils/tests/test_utils.py
+++ b/evennia/utils/tests/test_utils.py
@@ -223,3 +223,20 @@ def test_mod_import_from_path_invalid(self):
         test_path = self._t_dir_file("invalid_filename.py")
         loaded_mod = utils.mod_import_from_path(test_path)
         self.assertIsNone(loaded_mod)
+
+
+class LatinifyTest(TestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.example_str = 'It naïvely says, “plugh.”'
+        self.expected_output = 'It naively says, "plugh."'
+
+    def test_plain_string(self):
+        result = utils.latinify(self.example_str)
+        self.assertEqual(result, self.expected_output)
+
+    def test_byte_string(self):
+        byte_str = utils.to_bytes(self.example_str)
+        result = utils.latinify(byte_str)
+        self.assertEqual(result, self.expected_output)