osc-lib 3.2.0__py3-none-any.whl → 4.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osc_lib/api/api.py +62 -27
- osc_lib/api/auth.py +36 -21
- osc_lib/api/utils.py +10 -5
- osc_lib/cli/client_config.py +53 -32
- osc_lib/cli/format_columns.py +19 -17
- osc_lib/cli/identity.py +14 -3
- osc_lib/cli/pagination.py +83 -0
- osc_lib/cli/parseractions.py +98 -20
- osc_lib/clientmanager.py +45 -24
- osc_lib/command/command.py +19 -8
- osc_lib/command/timing.py +11 -1
- osc_lib/exceptions.py +12 -2
- osc_lib/logs.py +17 -8
- osc_lib/py.typed +0 -0
- osc_lib/shell.py +64 -44
- osc_lib/tests/test_shell.py +4 -4
- osc_lib/tests/utils/__init__.py +0 -19
- osc_lib/tests/utils/test_tags.py +22 -7
- osc_lib/tests/utils/test_utils.py +1 -11
- osc_lib/utils/__init__.py +179 -108
- osc_lib/utils/columns.py +15 -4
- osc_lib/utils/tags.py +38 -21
- {osc_lib-3.2.0.dist-info → osc_lib-4.0.1.dist-info}/METADATA +11 -13
- osc_lib-4.0.1.dist-info/RECORD +53 -0
- {osc_lib-3.2.0.dist-info → osc_lib-4.0.1.dist-info}/WHEEL +1 -1
- osc_lib-4.0.1.dist-info/pbr.json +1 -0
- osc_lib-3.2.0.dist-info/RECORD +0 -51
- osc_lib-3.2.0.dist-info/pbr.json +0 -1
- {osc_lib-3.2.0.dist-info → osc_lib-4.0.1.dist-info}/AUTHORS +0 -0
- {osc_lib-3.2.0.dist-info → osc_lib-4.0.1.dist-info}/LICENSE +0 -0
- {osc_lib-3.2.0.dist-info → osc_lib-4.0.1.dist-info}/top_level.txt +0 -0
osc_lib/shell.py
CHANGED
@@ -20,12 +20,15 @@ import getpass
|
|
20
20
|
import logging
|
21
21
|
import sys
|
22
22
|
import traceback
|
23
|
+
import typing as ty
|
23
24
|
|
25
|
+
from cliff import _argparse
|
24
26
|
from cliff import app
|
25
27
|
from cliff import command
|
26
28
|
from cliff import commandmanager
|
27
29
|
from cliff import complete
|
28
30
|
from cliff import help
|
31
|
+
from cliff import interactive
|
29
32
|
from oslo_utils import importutils
|
30
33
|
from oslo_utils import strutils
|
31
34
|
|
@@ -45,7 +48,7 @@ DEFAULT_DOMAIN = 'default'
|
|
45
48
|
DEFAULT_INTERFACE = 'public'
|
46
49
|
|
47
50
|
|
48
|
-
def prompt_for_password(prompt=None):
|
51
|
+
def prompt_for_password(prompt: ty.Optional[str] = None) -> str:
|
49
52
|
"""Prompt user for a password
|
50
53
|
|
51
54
|
Prompt for a password if stdin is a tty.
|
@@ -76,26 +79,30 @@ def prompt_for_password(prompt=None):
|
|
76
79
|
class OpenStackShell(app.App):
|
77
80
|
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
|
78
81
|
|
82
|
+
client_manager: clientmanager.ClientManager
|
83
|
+
|
79
84
|
log = logging.getLogger(__name__)
|
80
|
-
timing_data = []
|
85
|
+
timing_data: ty.List[ty.Any] = []
|
81
86
|
|
82
87
|
def __init__(
|
83
88
|
self,
|
84
|
-
description=None,
|
85
|
-
version=None,
|
86
|
-
command_manager=None,
|
87
|
-
stdin=None,
|
88
|
-
stdout=None,
|
89
|
-
stderr=None,
|
90
|
-
interactive_app_factory
|
91
|
-
|
92
|
-
|
89
|
+
description: ty.Optional[str] = None,
|
90
|
+
version: ty.Optional[str] = None,
|
91
|
+
command_manager: ty.Optional[commandmanager.CommandManager] = None,
|
92
|
+
stdin: ty.Optional[ty.TextIO] = None,
|
93
|
+
stdout: ty.Optional[ty.TextIO] = None,
|
94
|
+
stderr: ty.Optional[ty.TextIO] = None,
|
95
|
+
interactive_app_factory: ty.Optional[
|
96
|
+
ty.Type['interactive.InteractiveApp']
|
97
|
+
] = None,
|
98
|
+
deferred_help: bool = False,
|
99
|
+
) -> None:
|
93
100
|
# Patch command.Command to add a default auth_required = True
|
94
|
-
command.Command
|
101
|
+
setattr(command.Command, 'auth_required', True)
|
95
102
|
|
96
103
|
# Some commands do not need authentication
|
97
|
-
help.HelpCommand
|
98
|
-
complete.CompleteCommand
|
104
|
+
setattr(help.HelpCommand, 'auth_required', False)
|
105
|
+
setattr(complete.CompleteCommand, 'auth_required', False)
|
99
106
|
|
100
107
|
# Slight change to the meaning of --debug
|
101
108
|
self.DEFAULT_DEBUG_VALUE = None
|
@@ -120,17 +127,16 @@ class OpenStackShell(app.App):
|
|
120
127
|
# Set in subclasses
|
121
128
|
self.api_version = None
|
122
129
|
|
123
|
-
self.
|
124
|
-
self.command_options = None
|
130
|
+
self.command_options: ty.List[str] = []
|
125
131
|
|
126
132
|
self.do_profile = False
|
127
133
|
|
128
|
-
def configure_logging(self):
|
134
|
+
def configure_logging(self) -> None:
|
129
135
|
"""Configure logging for the app."""
|
130
136
|
self.log_configurator = logs.LogConfigurator(self.options)
|
131
137
|
self.dump_stack_trace = self.log_configurator.dump_trace
|
132
138
|
|
133
|
-
def run(self, argv):
|
139
|
+
def run(self, argv: ty.List[str]) -> int:
|
134
140
|
ret_val = 1
|
135
141
|
self.command_options = argv
|
136
142
|
try:
|
@@ -149,12 +155,12 @@ class OpenStackShell(app.App):
|
|
149
155
|
finally:
|
150
156
|
self.log.debug("END return value: %s", ret_val)
|
151
157
|
|
152
|
-
def init_profile(self):
|
158
|
+
def init_profile(self) -> None:
|
153
159
|
self.do_profile = osprofiler_profiler and self.options.profile
|
154
160
|
if self.do_profile:
|
155
161
|
osprofiler_profiler.init(self.options.profile)
|
156
162
|
|
157
|
-
def close_profile(self):
|
163
|
+
def close_profile(self) -> None:
|
158
164
|
if self.do_profile:
|
159
165
|
profiler = osprofiler_profiler.get()
|
160
166
|
trace_id = profiler.get_base_id()
|
@@ -167,14 +173,14 @@ class OpenStackShell(app.App):
|
|
167
173
|
# that (PROFILE = 60 for instance), but not sure we need it here.
|
168
174
|
self.log.warning(f"Trace ID: {trace_id}")
|
169
175
|
self.log.warning(
|
170
|
-
"Short trace ID
|
176
|
+
f"Short trace ID for OpenTracing-based drivers: {short_id}"
|
171
177
|
)
|
172
178
|
self.log.warning(
|
173
179
|
"Display trace data with command:\n"
|
174
180
|
f"osprofiler trace show --html {trace_id} "
|
175
181
|
)
|
176
182
|
|
177
|
-
def run_subcommand(self, argv):
|
183
|
+
def run_subcommand(self, argv: ty.List[str]) -> int:
|
178
184
|
self.init_profile()
|
179
185
|
try:
|
180
186
|
ret_value = super().run_subcommand(argv)
|
@@ -182,7 +188,7 @@ class OpenStackShell(app.App):
|
|
182
188
|
self.close_profile()
|
183
189
|
return ret_value
|
184
190
|
|
185
|
-
def interact(self):
|
191
|
+
def interact(self) -> None:
|
186
192
|
self.init_profile()
|
187
193
|
try:
|
188
194
|
ret_value = super().interact()
|
@@ -190,8 +196,17 @@ class OpenStackShell(app.App):
|
|
190
196
|
self.close_profile()
|
191
197
|
return ret_value
|
192
198
|
|
193
|
-
def build_option_parser(
|
194
|
-
|
199
|
+
def build_option_parser(
|
200
|
+
self,
|
201
|
+
description: ty.Optional[str],
|
202
|
+
version: ty.Optional[str],
|
203
|
+
argparse_kwargs: ty.Optional[ty.Dict[str, ty.Any]] = None,
|
204
|
+
) -> _argparse.ArgumentParser:
|
205
|
+
parser = super().build_option_parser(
|
206
|
+
description,
|
207
|
+
version,
|
208
|
+
argparse_kwargs,
|
209
|
+
)
|
195
210
|
|
196
211
|
# service token auth argument
|
197
212
|
parser.add_argument(
|
@@ -248,9 +263,7 @@ class OpenStackShell(app.App):
|
|
248
263
|
metavar='<auth-domain>',
|
249
264
|
dest='default_domain',
|
250
265
|
default=utils.env('OS_DEFAULT_DOMAIN', default=DEFAULT_DOMAIN),
|
251
|
-
help=_(
|
252
|
-
'Default domain ID, default=%s. ' '(Env: OS_DEFAULT_DOMAIN)'
|
253
|
-
)
|
266
|
+
help=_('Default domain ID, default=%s. (Env: OS_DEFAULT_DOMAIN)')
|
254
267
|
% DEFAULT_DOMAIN,
|
255
268
|
)
|
256
269
|
parser.add_argument(
|
@@ -347,7 +360,6 @@ class OpenStackShell(app.App):
|
|
347
360
|
)
|
348
361
|
|
349
362
|
return parser
|
350
|
-
# return clientmanager.build_plugin_option_parser(parser)
|
351
363
|
|
352
364
|
"""
|
353
365
|
Break up initialize_app() so that overriding it in a subclass does not
|
@@ -363,7 +375,7 @@ class OpenStackShell(app.App):
|
|
363
375
|
|
364
376
|
"""
|
365
377
|
|
366
|
-
def _final_defaults(self):
|
378
|
+
def _final_defaults(self) -> None:
|
367
379
|
# Set the default plugin to None
|
368
380
|
# NOTE(dtroyer): This is here to set up for setting it to a default
|
369
381
|
# in the calling CLI
|
@@ -390,21 +402,21 @@ class OpenStackShell(app.App):
|
|
390
402
|
# Save default domain
|
391
403
|
self.default_domain = self.options.default_domain
|
392
404
|
|
393
|
-
def _load_plugins(self):
|
405
|
+
def _load_plugins(self) -> None:
|
394
406
|
"""Load plugins via stevedore
|
395
407
|
|
396
408
|
osc-lib has no opinion on what plugins should be loaded
|
397
409
|
"""
|
398
410
|
pass
|
399
411
|
|
400
|
-
def _load_commands(self):
|
412
|
+
def _load_commands(self) -> None:
|
401
413
|
"""Load commands via cliff/stevedore
|
402
414
|
|
403
415
|
osc-lib has no opinion on what commands should be loaded
|
404
416
|
"""
|
405
417
|
pass
|
406
418
|
|
407
|
-
def initialize_app(self, argv):
|
419
|
+
def initialize_app(self, argv: ty.List[str]) -> None:
|
408
420
|
"""Global app init bits:
|
409
421
|
|
410
422
|
* set up API versions
|
@@ -416,7 +428,9 @@ class OpenStackShell(app.App):
|
|
416
428
|
super().initialize_app(argv)
|
417
429
|
self.log.info(
|
418
430
|
"START with options: %s",
|
419
|
-
strutils.mask_password(" ".join(self.command_options))
|
431
|
+
strutils.mask_password(" ".join(self.command_options))
|
432
|
+
if self.command_options
|
433
|
+
else "",
|
420
434
|
)
|
421
435
|
self.log.debug("options: %s", strutils.mask_password(self.options))
|
422
436
|
|
@@ -473,23 +487,23 @@ class OpenStackShell(app.App):
|
|
473
487
|
pw_func=prompt_for_password,
|
474
488
|
)
|
475
489
|
|
476
|
-
def prepare_to_run_command(self, cmd):
|
490
|
+
def prepare_to_run_command(self, cmd: 'command.Command') -> None:
|
477
491
|
"""Set up auth and API versions"""
|
478
492
|
self.log.debug(
|
479
493
|
'command: %s -> %s.%s (auth=%s)',
|
480
494
|
getattr(cmd, 'cmd_name', '<none>'),
|
481
495
|
cmd.__class__.__module__,
|
482
496
|
cmd.__class__.__name__,
|
483
|
-
cmd
|
497
|
+
getattr(cmd, 'auth_required', None),
|
484
498
|
)
|
485
499
|
|
486
500
|
# NOTE(dtroyer): If auth is not required for a command, skip
|
487
501
|
# get_one()'s validation to avoid loading plugins
|
488
|
-
validate = cmd
|
502
|
+
validate = getattr(cmd, 'auth_required', False)
|
489
503
|
|
490
504
|
# NOTE(dtroyer): Save the auth required state of the _current_ command
|
491
505
|
# in the ClientManager
|
492
|
-
self.client_manager._auth_required =
|
506
|
+
self.client_manager._auth_required = validate
|
493
507
|
|
494
508
|
# Validate auth options
|
495
509
|
self.cloud = self.cloud_config.get_one(
|
@@ -503,26 +517,31 @@ class OpenStackShell(app.App):
|
|
503
517
|
# Push the updated args into ClientManager
|
504
518
|
self.client_manager._cli_options = self.cloud
|
505
519
|
|
506
|
-
if
|
520
|
+
if validate:
|
507
521
|
self.client_manager.setup_auth()
|
508
522
|
if hasattr(cmd, 'required_scope') and cmd.required_scope:
|
509
523
|
# let the command decide whether we need a scoped token
|
510
524
|
self.client_manager.validate_scope()
|
511
525
|
# Trigger the Identity client to initialize
|
512
|
-
self.client_manager.session.auth.auth_ref = (
|
526
|
+
self.client_manager.session.auth.auth_ref = ( # type: ignore
|
513
527
|
self.client_manager.auth_ref
|
514
528
|
)
|
515
529
|
return
|
516
530
|
|
517
|
-
def clean_up(
|
531
|
+
def clean_up(
|
532
|
+
self,
|
533
|
+
cmd: 'command.Command',
|
534
|
+
result: int,
|
535
|
+
err: ty.Optional[BaseException],
|
536
|
+
) -> None:
|
518
537
|
self.log.debug('clean_up %s: %s', cmd.__class__.__name__, err or '')
|
519
538
|
|
520
539
|
# Close SDK connection if available to have proper cleanup there
|
521
|
-
if
|
540
|
+
if getattr(self.client_manager, "sdk_connection", None) is not None:
|
522
541
|
self.client_manager.sdk_connection.close()
|
523
542
|
|
524
543
|
# Close session if available
|
525
|
-
if
|
544
|
+
if getattr(self.client_manager.session, "session", None) is not None:
|
526
545
|
self.client_manager.session.session.close()
|
527
546
|
|
528
547
|
# Process collected timing data
|
@@ -541,6 +560,7 @@ class OpenStackShell(app.App):
|
|
541
560
|
# Check the formatter used in the actual command
|
542
561
|
if (
|
543
562
|
hasattr(cmd, 'formatter')
|
563
|
+
and hasattr(cmd, '_formatter_plugins')
|
544
564
|
and cmd.formatter != cmd._formatter_plugins['table'].obj
|
545
565
|
):
|
546
566
|
format = 'csv'
|
@@ -550,7 +570,7 @@ class OpenStackShell(app.App):
|
|
550
570
|
tcmd.run(targs)
|
551
571
|
|
552
572
|
|
553
|
-
def main(argv=None):
|
573
|
+
def main(argv: ty.Optional[ty.List[str]] = None) -> int:
|
554
574
|
if argv is None:
|
555
575
|
argv = sys.argv[1:]
|
556
576
|
return OpenStackShell().run(argv)
|
osc_lib/tests/test_shell.py
CHANGED
@@ -288,19 +288,19 @@ class TestShellCli(utils.TestShell):
|
|
288
288
|
|
289
289
|
# Default
|
290
290
|
utils.fake_execute(_shell, "module list")
|
291
|
-
self.
|
292
|
-
self.
|
291
|
+
self.assertIsNone(_shell.options.cert)
|
292
|
+
self.assertIsNone(_shell.options.key)
|
293
293
|
self.assertIsNone(_shell.client_manager.cert)
|
294
294
|
|
295
295
|
# --os-cert
|
296
296
|
utils.fake_execute(_shell, "--os-cert mycert module list")
|
297
297
|
self.assertEqual('mycert', _shell.options.cert)
|
298
|
-
self.
|
298
|
+
self.assertIsNone(_shell.options.key)
|
299
299
|
self.assertEqual('mycert', _shell.client_manager.cert)
|
300
300
|
|
301
301
|
# --os-key
|
302
302
|
utils.fake_execute(_shell, "--os-key mickey module list")
|
303
|
-
self.
|
303
|
+
self.assertIsNone(_shell.options.cert)
|
304
304
|
self.assertEqual('mickey', _shell.options.key)
|
305
305
|
self.assertIsNone(_shell.client_manager.cert)
|
306
306
|
|
osc_lib/tests/utils/__init__.py
CHANGED
@@ -14,7 +14,6 @@
|
|
14
14
|
# under the License.
|
15
15
|
#
|
16
16
|
|
17
|
-
import contextlib
|
18
17
|
import copy
|
19
18
|
import json as jsonutils
|
20
19
|
import os
|
@@ -111,24 +110,6 @@ class TestCase(testtools.TestCase):
|
|
111
110
|
msg = f'method {m} should not have been called'
|
112
111
|
self.fail(msg)
|
113
112
|
|
114
|
-
@contextlib.contextmanager
|
115
|
-
def subTest(self, *args, **kwargs):
|
116
|
-
"""This is a wrapper to unittest's subTest method.
|
117
|
-
|
118
|
-
This wrapper suppresses 2 issues:
|
119
|
-
* lack of support in older Python versions
|
120
|
-
* bug in testtools that breaks support for all versions
|
121
|
-
"""
|
122
|
-
try:
|
123
|
-
with super().subTest(*args, **kwargs):
|
124
|
-
yield
|
125
|
-
except TypeError:
|
126
|
-
raise
|
127
|
-
except AttributeError:
|
128
|
-
# TODO(elhararb): remove this except clause when subTest is
|
129
|
-
# enabled in testtools
|
130
|
-
yield
|
131
|
-
|
132
113
|
|
133
114
|
class TestCommand(TestCase):
|
134
115
|
"""Test cliff command classes"""
|
osc_lib/tests/utils/test_tags.py
CHANGED
@@ -13,6 +13,7 @@
|
|
13
13
|
# under the License.
|
14
14
|
|
15
15
|
import argparse
|
16
|
+
import functools
|
16
17
|
import sys
|
17
18
|
from unittest import mock
|
18
19
|
|
@@ -27,7 +28,9 @@ def help_enhancer(_h):
|
|
27
28
|
|
28
29
|
class TestTags(test_utils.TestCase):
|
29
30
|
def test_add_tag_filtering_option_to_parser(self):
|
30
|
-
parser = argparse.ArgumentParser(
|
31
|
+
parser = argparse.ArgumentParser(
|
32
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
33
|
+
)
|
31
34
|
tags.add_tag_filtering_option_to_parser(parser, 'test')
|
32
35
|
|
33
36
|
parsed_args = parser.parse_args(
|
@@ -60,7 +63,9 @@ class TestTags(test_utils.TestCase):
|
|
60
63
|
self.assertCountEqual(expected, actual)
|
61
64
|
|
62
65
|
def test_get_tag_filtering_args(self):
|
63
|
-
parser = argparse.ArgumentParser(
|
66
|
+
parser = argparse.ArgumentParser(
|
67
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
68
|
+
)
|
64
69
|
tags.add_tag_filtering_option_to_parser(parser, 'test')
|
65
70
|
|
66
71
|
parsed_args = parser.parse_args(
|
@@ -86,7 +91,9 @@ class TestTags(test_utils.TestCase):
|
|
86
91
|
self.assertEqual(expected, args)
|
87
92
|
|
88
93
|
def test_add_tag_option_to_parser_for_create(self):
|
89
|
-
parser = argparse.ArgumentParser(
|
94
|
+
parser = argparse.ArgumentParser(
|
95
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
96
|
+
)
|
90
97
|
tags.add_tag_option_to_parser_for_create(parser, 'test')
|
91
98
|
|
92
99
|
# Test that --tag and --no-tag are mutually exclusive
|
@@ -105,7 +112,9 @@ class TestTags(test_utils.TestCase):
|
|
105
112
|
self.assertCountEqual(expected, actual)
|
106
113
|
|
107
114
|
def test_add_tag_option_to_parser_for_set(self):
|
108
|
-
parser = argparse.ArgumentParser(
|
115
|
+
parser = argparse.ArgumentParser(
|
116
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
117
|
+
)
|
109
118
|
tags.add_tag_option_to_parser_for_set(parser, 'test')
|
110
119
|
|
111
120
|
parsed_args = parser.parse_args(['--tag', 'tag1'])
|
@@ -119,7 +128,9 @@ class TestTags(test_utils.TestCase):
|
|
119
128
|
self.assertCountEqual(expected, actual)
|
120
129
|
|
121
130
|
def test_add_tag_option_to_parser_for_unset(self):
|
122
|
-
parser = argparse.ArgumentParser(
|
131
|
+
parser = argparse.ArgumentParser(
|
132
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
133
|
+
)
|
123
134
|
tags.add_tag_option_to_parser_for_unset(parser, 'test')
|
124
135
|
|
125
136
|
# Test that --tag and --all-tag are mutually exclusive
|
@@ -209,11 +220,15 @@ class TestTagHelps(test_utils.TestCase):
|
|
209
220
|
options_name = 'options'
|
210
221
|
else:
|
211
222
|
options_name = 'optional arguments'
|
212
|
-
parser = argparse.ArgumentParser(
|
223
|
+
parser = argparse.ArgumentParser(
|
224
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
225
|
+
)
|
213
226
|
meth(parser, 'test')
|
214
227
|
self.assertEqual(exp_normal % options_name, parser.format_help())
|
215
228
|
|
216
|
-
parser = argparse.ArgumentParser(
|
229
|
+
parser = argparse.ArgumentParser(
|
230
|
+
formatter_class=functools.partial(argparse.HelpFormatter, width=78)
|
231
|
+
)
|
217
232
|
meth(parser, 'test', enhance_help=help_enhancer)
|
218
233
|
self.assertEqual(exp_enhanced % options_name, parser.format_help())
|
219
234
|
|
@@ -460,11 +460,6 @@ class TestUtils(test_utils.TestCase):
|
|
460
460
|
self.assertEqual('fake-id', res_id)
|
461
461
|
return res_attr
|
462
462
|
|
463
|
-
def test_get_item_properties_with_format_func(self):
|
464
|
-
formatters = {'attr': utils.format_list}
|
465
|
-
res_attr = self._test_get_item_properties_with_formatter(formatters)
|
466
|
-
self.assertEqual(utils.format_list(['a', 'b']), res_attr)
|
467
|
-
|
468
463
|
def test_get_item_properties_with_formattable_column(self):
|
469
464
|
formatters = {'attr': format_columns.ListColumn}
|
470
465
|
res_attr = self._test_get_item_properties_with_formatter(formatters)
|
@@ -479,11 +474,6 @@ class TestUtils(test_utils.TestCase):
|
|
479
474
|
self.assertEqual('fake-id', res_id)
|
480
475
|
return res_attr
|
481
476
|
|
482
|
-
def test_get_dict_properties_with_format_func(self):
|
483
|
-
formatters = {'attr': utils.format_list}
|
484
|
-
res_attr = self._test_get_dict_properties_with_formatter(formatters)
|
485
|
-
self.assertEqual(utils.format_list(['a', 'b']), res_attr)
|
486
|
-
|
487
477
|
def test_get_dict_properties_with_formattable_column(self):
|
488
478
|
formatters = {'attr': format_columns.ListColumn}
|
489
479
|
res_attr = self._test_get_dict_properties_with_formatter(formatters)
|
@@ -711,7 +701,7 @@ class TestFindResource(test_utils.TestCase):
|
|
711
701
|
self.name,
|
712
702
|
)
|
713
703
|
self.assertEqual(
|
714
|
-
"More than one resource exists
|
704
|
+
"More than one resource exists with the name or ID 'legos'.",
|
715
705
|
str(result),
|
716
706
|
)
|
717
707
|
self.manager.get.assert_called_with(self.name)
|