Repository: boto/botocore (python)

Issue:
Expose the exception type (and other helpful information) from configparser
**Is your feature request related to a problem? Please describe.**

I am a user of AWS Organizations, as well as having multiple AWS-using freelance clients, and my own personal AWS account. As a result I have over 160 profiles in my `~/.aws/config` file.

Today, after adding  a couple dozen more profiles for recently-provisioned accounts, I started getting the following error from the AWS CLI.

```plain
Unable to parse config file: /Users/{me}/.aws/config
```

However, there was no additional information to help me debug. Was it an issue with line endings? Spacing around `=` signs? A typo somewhere?

After a few hours of poking around, I tracked it down to this line in `botocore`: https://github.com/boto/botocore/blob/6451ae1fad57f4453af97649e7ed9192b0f623be/botocore/configloader.py#L147-L152

The issue was that I'd inadvertently duplicated a key (similar to #2311).

**Describe the solution you'd like**

Looking at [the read() method of configparser](https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read), and in-turn [configparser exceptions](https://docs.python.org/3/library/configparser.html#configparser.DuplicateSectionError), I'd like to see a little more information exposed from the underlying `configparser` class when an exception occurs, so that it doesn't take so long to track down the source of the problem.

With AWS continuing to get exponentially more complex over the years, I'm certainly not the only one responsible for managing hundreds of accounts.

Thank you.


Pull request description:
Decode file paths in config loader
This decodes the path in config parser before throwing errors.

Fixes aws/aws-cli#2395

cc @kyleknap @jamesls @stealthycoin @dstufft

Patch:
diff --git a/botocore/configloader.py b/botocore/configloader.py
index 24d07b1c4..b9a4d542a 100644
--- a/botocore/configloader.py
+++ b/botocore/configloader.py
@@ -14,8 +14,9 @@
 import os
 import shlex
 import copy
+import sys
 
-from six.moves import configparser
+from botocore.compat import six
 
 import botocore.exceptions
 
@@ -142,12 +143,13 @@ def raw_config_parse(config_filename, parse_subsections=True):
         path = os.path.expandvars(path)
         path = os.path.expanduser(path)
         if not os.path.isfile(path):
-            raise botocore.exceptions.ConfigNotFound(path=path)
-        cp = configparser.RawConfigParser()
+            raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
+        cp = six.moves.configparser.RawConfigParser()
         try:
-            cp.read(path)
-        except configparser.Error:
-            raise botocore.exceptions.ConfigParseError(path=path)
+            cp.read([path])
+        except six.moves.configparser.Error:
+            raise botocore.exceptions.ConfigParseError(
+                path=_unicode_path(path))
         else:
             for section in cp.sections():
                 config[section] = {}
@@ -161,11 +163,17 @@ def raw_config_parse(config_filename, parse_subsections=True):
                             config_value = _parse_nested(config_value)
                         except ValueError:
                             raise botocore.exceptions.ConfigParseError(
-                                path=path)
+                                path=_unicode_path(path))
                     config[section][option] = config_value
     return config
 
 
+def _unicode_path(path):
+    if isinstance(path, six.text_type):
+        return path
+    return path.decode(sys.getfilesystemencoding(), 'replace')
+
+
 def _parse_nested(config_value):
     # Given a value like this:
     # \n
diff --git "a/tests/unit/cfg/aws_config_unicode\342\234\223" "b/tests/unit/cfg/aws_config_unicode\342\234\223"
new file mode 100644
index 000000000..07adb69f7
--- /dev/null
+++ "b/tests/unit/cfg/aws_config_unicode\342\234\223"
@@ -0,0 +1,8 @@
+[default]
+aws_access_key_id = foo
+aws_secret_access_key = bar
+
+[profile "personal"]
+aws_access_key_id = fie
+aws_secret_access_key = baz
+aws_security_token = fiebaz


Test patch:
diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py
index e20aba72f..eb1d903dc 100644
--- a/tests/unit/test_configloader.py
+++ b/tests/unit/test_configloader.py
@@ -14,13 +14,19 @@
 # language governing permissions and limitations under the License.
 from tests import unittest, BaseEnvVar
 import os
+import mock
+
 import botocore.exceptions
 from botocore.configloader import raw_config_parse, load_config, \
     multi_file_load_config
+from botocore.compat import six
 
 
 def path(filename):
-    return os.path.join(os.path.dirname(__file__), 'cfg', filename)
+    directory = os.path.join(os.path.dirname(__file__), 'cfg')
+    if isinstance(filename, six.binary_type):
+        directory = six.b(directory)
+    return os.path.join(directory, filename)
 
 
 class TestConfigLoader(BaseEnvVar):
@@ -103,6 +109,20 @@ class TestConfigLoader(BaseEnvVar):
         self.assertEqual(third_config['aws_secret_access_key'], 'third_baz')
         self.assertEqual(third_config['aws_security_token'], 'third_fiebaz')
 
+    def test_unicode_bytes_path_not_found(self):
+        with self.assertRaises(botocore.exceptions.ConfigNotFound):
+            with mock.patch('sys.getfilesystemencoding') as encoding:
+                encoding.return_value = 'utf-8'
+                load_config(path(b'\xe2\x9c\x93'))
+
+    def test_unicode_bytes_path(self):
+        filename = path(b'aws_config_unicode\xe2\x9c\x93')
+        with mock.patch('sys.getfilesystemencoding') as encoding:
+            encoding.return_value = 'utf-8'
+            loaded_config = load_config(filename)
+        self.assertIn('default', loaded_config['profiles'])
+        self.assertIn('personal', loaded_config['profiles'])
+
 
 if __name__ == "__main__":
     unittest.main()