Repository: posativ/isso
Issue: configparser.InterpolationSyntaxError: for SMTP password

Hi,

How to allow Isso to parse pwds correctly?
`configparser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%!...@c...!Cjq'`

Changed files:
isso/config.py, isso/tests/test_config.py, isso/utils/parse.py, isso/tests/test_utils_hash.py

diff --git a/isso/config.py b/isso/config.py
index 8e4c8ca..ef36580 100644
--- a/isso/config.py
+++ b/isso/config.py
@@ -78,10 +78,22 @@ class Section(object):
 class IssoParser(ConfigParser):
     """Parse INI-style configuration with some modifications for Isso.
 
-        * parse human-readable timedelta such as "15m" as "15 minutes"
-        * handle indented lines as "lists"
+        * Parse human-readable timedelta such as "15m" as "15 minutes"
+        * Handle indented lines as "lists"
+        * Disable string interpolation ('%s') for values
     """
 
+    def __init__(self, *args, **kwargs):
+        """Supply modified defaults to configparser.ConfigParser
+        https://docs.python.org/3.9/library/configparser.html#configparser.ConfigParser
+        allow_no_value=True: Allow empty config keys
+        interpolation=None:  Do not attempt to use python string interpolation on
+                             values. This is especially important for parsing
+                             passwords that might contain '%'
+        """
+        return super(IssoParser, self).__init__(
+            allow_no_value=True, interpolation=None, *args, **kwargs)
+
     def getint(self, section, key):
         try:
             delta = timedelta(self.get(section, key))
@@ -107,7 +119,7 @@ class IssoParser(ConfigParser):
 
 def new(options=None):
 
-    cp = IssoParser(allow_no_value=True)
+    cp = IssoParser()
 
     if options:
         cp.read_dict(options)
diff --git a/isso/tests/test_config.py b/isso/tests/test_config.py
index 2fd1b4d..8a15d14 100644
--- a/isso/tests/test_config.py
+++ b/isso/tests/test_config.py
@@ -10,8 +10,8 @@ class TestConfig(unittest.TestCase):
 
     def test_parser(self):
 
-        parser = config.IssoParser(allow_no_value=True)
-        parser.read_file(io.StringIO(u"""
+        parser = config.IssoParser()
+        parser.read_file(io.StringIO("""
             [foo]
             bar = 1h
             baz = 12
@@ -19,10 +19,18 @@ class TestConfig(unittest.TestCase):
             bla =
                 spam
                 ham
-            asd = fgh"""))
+            asd = fgh
+            password = %s%%foo"""))
 
         self.assertEqual(parser.getint("foo", "bar"), 3600)
         self.assertEqual(parser.getint("foo", "baz"), 12)
         self.assertEqual(parser.getlist("foo", "spam"), ['a', 'b', 'cdef'])
         self.assertEqual(list(parser.getiter("foo", "bla")), ['spam', 'ham'])
         self.assertEqual(list(parser.getiter("foo", "asd")), ['fgh'])
+
+        # Strings containing '%' should not be python-interpolated
+        self.assertEqual(parser.get("foo", "password"), '%s%%foo')
+
+        # Section.get() should function the same way as plain IssoParser
+        foosection = parser.section("foo")
+        self.assertEqual(foosection.get("password"), '%s%%foo')
diff --git a/isso/tests/test_utils_hash.py b/isso/tests/test_utils_hash.py
index 122c897..feafe69 100644
--- a/isso/tests/test_utils_hash.py
+++ b/isso/tests/test_utils_hash.py
@@ -21,7 +21,7 @@ class TestHasher(unittest.TestCase):
 
         self.assertRaises(TypeError, h.hash, "...")
         self.assertEqual(h.hash(b"..."), b"...")
-        self.assertIsInstance(h.uhash(u"..."), (str, ))
+        self.assertIsInstance(h.uhash("..."), (str, ))
 
     def test_uhash(self):
         h = Hash(b"", func=None)
diff --git a/isso/utils/parse.py b/isso/utils/parse.py
index 83ce819..74759ef 100644
--- a/isso/utils/parse.py
+++ b/isso/utils/parse.py
@@ -9,7 +9,7 @@ from urllib.parse import unquote
 import html5lib
 
 
-def thread(data, default=u"Untitled.", id=None):
+def thread(data, default="Untitled.", id=None):
     """
     Extract <h1> title from web page. The title is *probably* the text node,
     which is the nearest H1 node in context to an element with the `isso-thread` id.