Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/botocore/configloader.py: 24%
99 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
2# Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"). You
5# may not use this file except in compliance with the License. A copy of
6# the License is located at
7#
8# http://aws.amazon.com/apache2.0/
9#
10# or in the "license" file accompanying this file. This file is
11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12# ANY KIND, either express or implied. See the License for the specific
13# language governing permissions and limitations under the License.
14import configparser
15import copy
16import os
17import shlex
18import sys
20import botocore.exceptions
23def multi_file_load_config(*filenames):
24 """Load and combine multiple INI configs with profiles.
26 This function will take a list of filesnames and return
27 a single dictionary that represents the merging of the loaded
28 config files.
30 If any of the provided filenames does not exist, then that file
31 is ignored. It is therefore ok to provide a list of filenames,
32 some of which may not exist.
34 Configuration files are **not** deep merged, only the top level
35 keys are merged. The filenames should be passed in order of
36 precedence. The first config file has precedence over the
37 second config file, which has precedence over the third config file,
38 etc. The only exception to this is that the "profiles" key is
39 merged to combine profiles from multiple config files into a
40 single profiles mapping. However, if a profile is defined in
41 multiple config files, then the config file with the highest
42 precedence is used. Profile values themselves are not merged.
43 For example::
45 FileA FileB FileC
46 [foo] [foo] [bar]
47 a=1 a=2 a=3
48 b=2
50 [bar] [baz] [profile a]
51 a=2 a=3 region=e
53 [profile a] [profile b] [profile c]
54 region=c region=d region=f
56 The final result of ``multi_file_load_config(FileA, FileB, FileC)``
57 would be::
59 {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3},
60 "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}},
61 {"c": {"region": "f"}}}
63 Note that the "foo" key comes from A, even though it's defined in both
64 FileA and FileB. Because "foo" was defined in FileA first, then the values
65 for "foo" from FileA are used and the values for "foo" from FileB are
66 ignored. Also note where the profiles originate from. Profile "a"
67 comes FileA, profile "b" comes from FileB, and profile "c" comes
68 from FileC.
70 """
71 configs = []
72 profiles = []
73 for filename in filenames:
74 try:
75 loaded = load_config(filename)
76 except botocore.exceptions.ConfigNotFound:
77 continue
78 profiles.append(loaded.pop('profiles'))
79 configs.append(loaded)
80 merged_config = _merge_list_of_dicts(configs)
81 merged_profiles = _merge_list_of_dicts(profiles)
82 merged_config['profiles'] = merged_profiles
83 return merged_config
86def _merge_list_of_dicts(list_of_dicts):
87 merged_dicts = {}
88 for single_dict in list_of_dicts:
89 for key, value in single_dict.items():
90 if key not in merged_dicts:
91 merged_dicts[key] = value
92 return merged_dicts
95def load_config(config_filename):
96 """Parse a INI config with profiles.
98 This will parse an INI config file and map top level profiles
99 into a top level "profile" key.
101 If you want to parse an INI file and map all section names to
102 top level keys, use ``raw_config_parse`` instead.
104 """
105 parsed = raw_config_parse(config_filename)
106 return build_profile_map(parsed)
109def raw_config_parse(config_filename, parse_subsections=True):
110 """Returns the parsed INI config contents.
112 Each section name is a top level key.
114 :param config_filename: The name of the INI file to parse
116 :param parse_subsections: If True, parse indented blocks as
117 subsections that represent their own configuration dictionary.
118 For example, if the config file had the contents::
120 s3 =
121 signature_version = s3v4
122 addressing_style = path
124 The resulting ``raw_config_parse`` would be::
126 {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}}
128 If False, do not try to parse subsections and return the indented
129 block as its literal value::
131 {'s3': '\nsignature_version = s3v4\naddressing_style = path'}
133 :returns: A dict with keys for each profile found in the config
134 file and the value of each key being a dict containing name
135 value pairs found in that profile.
137 :raises: ConfigNotFound, ConfigParseError
138 """
139 config = {}
140 path = config_filename
141 if path is not None:
142 path = os.path.expandvars(path)
143 path = os.path.expanduser(path)
144 if not os.path.isfile(path):
145 raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
146 cp = configparser.RawConfigParser()
147 try:
148 cp.read([path])
149 except (configparser.Error, UnicodeDecodeError) as e:
150 raise botocore.exceptions.ConfigParseError(
151 path=_unicode_path(path), error=e
152 ) from None
153 else:
154 for section in cp.sections():
155 config[section] = {}
156 for option in cp.options(section):
157 config_value = cp.get(section, option)
158 if parse_subsections and config_value.startswith('\n'):
159 # Then we need to parse the inner contents as
160 # hierarchical. We support a single level
161 # of nesting for now.
162 try:
163 config_value = _parse_nested(config_value)
164 except ValueError as e:
165 raise botocore.exceptions.ConfigParseError(
166 path=_unicode_path(path), error=e
167 ) from None
168 config[section][option] = config_value
169 return config
172def _unicode_path(path):
173 if isinstance(path, str):
174 return path
175 # According to the documentation getfilesystemencoding can return None
176 # on unix in which case the default encoding is used instead.
177 filesystem_encoding = sys.getfilesystemencoding()
178 if filesystem_encoding is None:
179 filesystem_encoding = sys.getdefaultencoding()
180 return path.decode(filesystem_encoding, 'replace')
183def _parse_nested(config_value):
184 # Given a value like this:
185 # \n
186 # foo = bar
187 # bar = baz
188 # We need to parse this into
189 # {'foo': 'bar', 'bar': 'baz}
190 parsed = {}
191 for line in config_value.splitlines():
192 line = line.strip()
193 if not line:
194 continue
195 # The caller will catch ValueError
196 # and raise an appropriate error
197 # if this fails.
198 key, value = line.split('=', 1)
199 parsed[key.strip()] = value.strip()
200 return parsed
203def _parse_section(key, values):
204 result = {}
205 try:
206 parts = shlex.split(key)
207 except ValueError:
208 return result
209 if len(parts) == 2:
210 result[parts[1]] = values
211 return result
214def build_profile_map(parsed_ini_config):
215 """Convert the parsed INI config into a profile map.
217 The config file format requires that every profile except the
218 default to be prepended with "profile", e.g.::
220 [profile test]
221 aws_... = foo
222 aws_... = bar
224 [profile bar]
225 aws_... = foo
226 aws_... = bar
228 # This is *not* a profile
229 [preview]
230 otherstuff = 1
232 # Neither is this
233 [foobar]
234 morestuff = 2
236 The build_profile_map will take a parsed INI config file where each top
237 level key represents a section name, and convert into a format where all
238 the profiles are under a single top level "profiles" key, and each key in
239 the sub dictionary is a profile name. For example, the above config file
240 would be converted from::
242 {"profile test": {"aws_...": "foo", "aws...": "bar"},
243 "profile bar": {"aws...": "foo", "aws...": "bar"},
244 "preview": {"otherstuff": ...},
245 "foobar": {"morestuff": ...},
246 }
248 into::
250 {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"},
251 "bar": {"aws...": "foo", "aws...": "bar"},
252 "preview": {"otherstuff": ...},
253 "foobar": {"morestuff": ...},
254 }
256 If there are no profiles in the provided parsed INI contents, then
257 an empty dict will be the value associated with the ``profiles`` key.
259 .. note::
261 This will not mutate the passed in parsed_ini_config. Instead it will
262 make a deepcopy and return that value.
264 """
265 parsed_config = copy.deepcopy(parsed_ini_config)
266 profiles = {}
267 sso_sessions = {}
268 services = {}
269 final_config = {}
270 for key, values in parsed_config.items():
271 if key.startswith("profile"):
272 profiles.update(_parse_section(key, values))
273 elif key.startswith("sso-session"):
274 sso_sessions.update(_parse_section(key, values))
275 elif key.startswith("services"):
276 services.update(_parse_section(key, values))
277 elif key == 'default':
278 # default section is special and is considered a profile
279 # name but we don't require you use 'profile "default"'
280 # as a section.
281 profiles[key] = values
282 else:
283 final_config[key] = values
284 final_config['profiles'] = profiles
285 final_config['sso_sessions'] = sso_sessions
286 final_config['services'] = services
287 return final_config