Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YAML param files support #5482

Draft
wants to merge 1 commit into
base: v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-file-64839.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "file",
"description": "Add support for YAML input files with ``yaml"
}
67 changes: 52 additions & 15 deletions awscli/paramfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import json
import logging
import os
import copy

import ruamel
from ruamel.yaml import YAML

from awscli.compat import six

from awscli.compat import compat_open
Expand Down Expand Up @@ -79,22 +83,55 @@ def get_paramfile(path, cases):
return data


def get_file(prefix, path, mode):
file_path = os.path.expandvars(os.path.expanduser(path[len(prefix):]))
try:
with compat_open(file_path, mode) as f:
return f.read()
except UnicodeDecodeError:
raise ResourceLoadingError(
'Unable to load paramfile (%s), text contents could '
'not be decoded. If this is a binary file, please use the '
'fileb:// prefix instead of the file:// prefix.' % file_path)
except (OSError, IOError) as e:
raise ResourceLoadingError('Unable to load paramfile %s: %s' % (
path, e))
class FileLoader:

def __call__(self, prefix, path, mode):
return getattr(self, '_get_%s' % mode)(prefix, path)

def _get_r(self, prefix, path):
return self._get_file(prefix, path, 'r')

def _get_rb(self, prefix, path):
return self._get_file(prefix, path, 'rb')

def _get_yaml(self, prefix, path):
file_path = os.path.expandvars(os.path.expanduser(path[len(prefix):]))
yaml = YAML(typ='safe')
yaml.constructor.add_constructor(
'tag:yaml.org,2002:binary', self._load_binary
)
try:
with compat_open(file_path, 'r') as f:
return json.dumps(yaml.load(f))
except ruamel.yaml.scanner.ScannerError:
raise ResourceLoadingError(
'Unable to load paramfile (%s), it is not a valid YAML '
'file' % file_path)
except (OSError, IOError) as e:
raise ResourceLoadingError('Unable to load paramfile %s: %s' % (
path, e))

def _get_file(self, prefix, path, mode):
file_path = os.path.expandvars(os.path.expanduser(path[len(prefix):]))
try:
with compat_open(file_path, mode) as f:
return f.read()
except UnicodeDecodeError:
raise ResourceLoadingError(
'Unable to load paramfile (%s), text contents could '
'not be decoded. If this is a binary file, please use the '
'fileb:// prefix instead of the file:// prefix.' % file_path)
except (OSError, IOError) as e:
raise ResourceLoadingError('Unable to load paramfile %s: %s' % (
path, e))

def _load_binary(self, loader, node):
# stringify the binary value
return node.value


LOCAL_PREFIX_MAP = {
'file://': (get_file, {'mode': 'r'}),
'fileb://': (get_file, {'mode': 'rb'}),
'file://': (FileLoader(), {'mode': 'r'}),
'fileb://': (FileLoader(), {'mode': 'rb'}),
'yaml://': (FileLoader(), {'mode': 'yaml'}),
}
16 changes: 16 additions & 0 deletions tests/functional/test_paramfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ def test_does_use_fileb_prefix(self):
expected_param=b'file content'
)

def test_does_use_yaml_prefix(self):
path = self.files.create_file('foobar.yaml', '- key: value')
param = 'yaml://%s' % path
self.assert_param_expansion_is_correct(
provided_param=param,
expected_param='[{"key": "value"}]'
)

def test_does_use_yaml_binary_data(self):
path = self.files.create_file('foobar.yaml', '{key: !!binary "4pyT"}')
param = 'yaml://%s' % path
self.assert_param_expansion_is_correct(
provided_param=param,
expected_param='{"key": "4pyT"}'
)


class TestCLIUseEncodingFromEnv(BaseTestCLIFollowParamFile):

Expand Down
20 changes: 20 additions & 0 deletions tests/unit/test_paramfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ def test_binary_file(self):
self.assertEqual(data, b'This is a test')
self.assertIsInstance(data, six.binary_type)

def test_yaml_file(self):
contents = '- key: value'
filename = self.files.create_file('foo', contents)
prefixed_filename = 'yaml://' + filename
data = self.get_paramfile(prefixed_filename)
self.assertEqual(data, '[{"key": "value"}]')
self.assertIsInstance(data, six.string_types)

def test_invalid_yaml_file(self):
contents = '- - "key -value'
filename = self.files.create_file('foo', contents)
prefixed_filename = 'yaml://' + filename
with self.assertRaises(ResourceLoadingError) as e:
self.get_paramfile(prefixed_filename)


@skip_if_windows('Binary content error only occurs '
'on non-Windows platforms.')
def test_cannot_load_text_file(self):
Expand All @@ -61,6 +77,10 @@ def test_file_does_not_exist_raises_error(self):
with self.assertRaises(ResourceLoadingError):
self.get_paramfile('file://file/does/not/existsasdf.txt')

def test_yaml_file_does_not_exist_raises_error(self):
with self.assertRaises(ResourceLoadingError):
self.get_paramfile('yaml://file/does/not/existsasdf.yaml')

def test_no_match_uris_returns_none(self):
self.assertIsNone(self.get_paramfile('foobar://somewhere.bar'))

Expand Down