From e541d4228e7b406af966c57148ca5c557f32f5ae Mon Sep 17 00:00:00 2001 From: Steve Yoo Date: Tue, 2 Aug 2022 17:39:32 -0400 Subject: [PATCH] Include global options in command docs We get a lot of requests from users to include global options in a command's help page. This PR fulfils them by pre-generating .rst files for global options and synopsis and writing them to commands' help docs. An alternative that was considered attempted to plumb the global arg table from the CLIDriver to any help command that would need it. However, we abandoned the approach because it was too invasive to the existing interfaces and there was no easy way to apply the changes to customizations. --- .../next-release/enhancement-docs-19006.json | 5 ++ awscli/bcdoc/docevents.py | 3 + awscli/bcdoc/restdoc.py | 5 ++ awscli/clidocs.py | 83 ++++++++++++------ awscli/customizations/commands.py | 10 +-- awscli/examples/global_options.rst | 72 ++++++++++++++++ awscli/examples/global_synopsis.rst | 14 +++ scripts/make-global-opts-documentation | 38 ++++++++ tests/functional/docs/test_help_output.py | 17 ++-- tests/functional/test_globals.py | 23 +++++ tests/unit/customizations/test_commands.py | 83 ++++++++++-------- tests/unit/test_clidocs.py | 86 +++++++++---------- 12 files changed, 322 insertions(+), 117 deletions(-) create mode 100644 .changes/next-release/enhancement-docs-19006.json create mode 100644 awscli/examples/global_options.rst create mode 100644 awscli/examples/global_synopsis.rst create mode 100755 scripts/make-global-opts-documentation create mode 100644 tests/functional/test_globals.py diff --git a/.changes/next-release/enhancement-docs-19006.json b/.changes/next-release/enhancement-docs-19006.json new file mode 100644 index 000000000000..eb38c0152d51 --- /dev/null +++ b/.changes/next-release/enhancement-docs-19006.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "docs", + "description": "Improve AWS CLI docs to include global options available to service commands." +} diff --git a/awscli/bcdoc/docevents.py b/awscli/bcdoc/docevents.py index f5c6c84938f7..54bce6ebd9d9 100644 --- a/awscli/bcdoc/docevents.py +++ b/awscli/bcdoc/docevents.py @@ -23,6 +23,7 @@ 'doc-option': '.%s.%s', 'doc-option-example': '.%s.%s', 'doc-options-end': '.%s', + 'doc-global-option': '.%s', 'doc-examples': '.%s', 'doc-output': '.%s', 'doc-subitems-start': '.%s', @@ -74,6 +75,8 @@ def generate_events(session, help_command): arg_name=arg_name, help_command=help_command) session.emit('doc-options-end.%s' % help_command.event_class, help_command=help_command) + session.emit('doc-global-option.%s' % help_command.event_class, + help_command=help_command) session.emit('doc-subitems-start.%s' % help_command.event_class, help_command=help_command) if help_command.command_table: diff --git a/awscli/bcdoc/restdoc.py b/awscli/bcdoc/restdoc.py index cb557c038ae2..cdb6be14dcbc 100644 --- a/awscli/bcdoc/restdoc.py +++ b/awscli/bcdoc/restdoc.py @@ -102,6 +102,11 @@ def remove_last_doc_string(self): start, end = self._last_doc_string del self._writes[start:end] + def write_from_file(self, filename): + with open(filename, 'r') as f: + for line in f.readlines(): + self.writeln(line.strip()) + class DocumentStructure(ReSTDocument): def __init__(self, name, section_names=None, target='man', context=None): diff --git a/awscli/clidocs.py b/awscli/clidocs.py index 903eed39050c..ad5164e1ae3e 100644 --- a/awscli/clidocs.py +++ b/awscli/clidocs.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. import logging import os +import re from botocore import xform_name from botocore.model import StringShape from botocore.utils import is_json_value_header @@ -26,6 +27,11 @@ ) LOG = logging.getLogger(__name__) +EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'examples') +GLOBAL_OPTIONS_FILE = os.path.join(EXAMPLES_DIR, 'global_options.rst') +GLOBAL_OPTIONS_SYNOPSIS_FILE = os.path.join(EXAMPLES_DIR, + 'global_synopsis.rst') class CLIDocumentEventHandler(object): @@ -146,6 +152,8 @@ def doc_synopsis_option(self, arg_name, help_command, **kwargs): def doc_synopsis_end(self, help_command, **kwargs): doc = help_command.doc + # Append synopsis for global options. + doc.write_from_file(GLOBAL_OPTIONS_SYNOPSIS_FILE) doc.style.end_codeblock() # Reset the documented arg groups for other sections # that may document args (the detailed docs following @@ -183,6 +191,11 @@ def doc_option(self, arg_name, help_command, **kwargs): doc.style.dedent() doc.style.new_paragraph() + def doc_global_option(self, help_command, **kwargs): + doc = help_command.doc + doc.style.h2('Global Options') + doc.write_from_file(GLOBAL_OPTIONS_FILE) + def doc_relateditems_start(self, help_command, **kwargs): if help_command.related_items: doc = help_command.doc @@ -297,20 +310,10 @@ def doc_synopsis_end(self, help_command, **kwargs): doc.style.new_paragraph() def doc_options_start(self, help_command, **kwargs): - doc = help_command.doc - doc.style.h2('Options') + pass def doc_option(self, arg_name, help_command, **kwargs): - doc = help_command.doc - argument = help_command.arg_table[arg_name] - doc.writeln('``%s`` (%s)' % (argument.cli_name, - argument.cli_type_name)) - doc.include_doc_string(argument.documentation) - if argument.choices: - doc.style.start_ul() - for choice in argument.choices: - doc.style.li(choice) - doc.style.end_ul() + pass def doc_subitems_start(self, help_command, **kwargs): doc = help_command.doc @@ -348,6 +351,9 @@ def doc_option_example(self, arg_name, help_command, **kwargs): def doc_options_end(self, help_command, **kwargs): pass + def doc_global_option(self, help_command, **kwargs): + pass + def doc_description(self, help_command, **kwargs): doc = help_command.doc service_model = help_command.obj @@ -384,17 +390,8 @@ def doc_description(self, help_command, **kwargs): doc.style.h2('Description') doc.include_doc_string(operation_model.documentation) self._add_webapi_crosslink(help_command) - self._add_top_level_args_reference(help_command) self._add_note_for_document_types_if_used(help_command) - def _add_top_level_args_reference(self, help_command): - help_command.doc.writeln('') - help_command.doc.write("See ") - help_command.doc.style.internal_link( - title="'aws help'", - page='/reference/index' - ) - help_command.doc.writeln(' for descriptions of global parameters.') def _add_webapi_crosslink(self, help_command): doc = help_command.doc @@ -592,9 +589,6 @@ def doc_output(self, help_command, event_name, **kwargs): for member_name, member_shape in output_shape.members.items(): self._doc_member(doc, member_name, member_shape, stack=[]) - def doc_options_end(self, help_command, **kwargs): - self._add_top_level_args_reference(help_command) - class TopicListerDocumentEventHandler(CLIDocumentEventHandler): DESCRIPTION = ( @@ -729,3 +723,44 @@ def _line_has_tag(self, line): def doc_subitems_start(self, help_command, **kwargs): pass + + +class GlobalOptionsDocumenter: + """Documenter used to pre-generate global options docs.""" + + def __init__(self, help_command): + self._help_command = help_command + + def _remove_multilines(self, s): + return re.sub(r'\n+', '\n', s) + + def doc_global_options(self): + help_command = self._help_command + for arg in help_command.arg_table: + argument = help_command.arg_table.get(arg) + help_command.doc.writeln( + f"``{argument.cli_name}`` ({argument.cli_type_name})") + help_command.doc.style.indent() + help_command.doc.style.new_paragraph() + help_command.doc.include_doc_string(argument.documentation) + if argument.choices: + help_command.doc.style.start_ul() + for choice in argument.choices: + help_command.doc.style.li(choice) + help_command.doc.style.end_ul() + help_command.doc.style.dedent() + help_command.doc.style.new_paragraph() + global_options = help_command.doc.getvalue().decode('utf-8') + return self._remove_multilines(global_options) + + def doc_global_synopsis(self): + help_command = self._help_command + for arg in help_command.arg_table: + argument = help_command.arg_table.get(arg) + if argument.cli_type_name == 'boolean': + arg_synopsis = f"[{argument.cli_name}]" + else: + arg_synopsis = f"[{argument.cli_name} ]" + help_command.doc.writeln(arg_synopsis) + global_synopsis = help_command.doc.getvalue().decode('utf-8') + return self._remove_multilines(global_synopsis) diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py index 7c016751dbb7..45ac54e565ff 100644 --- a/awscli/customizations/commands.py +++ b/awscli/customizations/commands.py @@ -370,7 +370,6 @@ def doc_description(self, help_command, **kwargs): self.doc.style.h2('Description') self.doc.write(help_command.description) self.doc.style.new_paragraph() - self._add_top_level_args_reference(help_command) def doc_synopsis_start(self, help_command, **kwargs): if not help_command.synopsis: @@ -411,12 +410,16 @@ def doc_synopsis_option(self, arg_name, help_command, **kwargs): pass def doc_synopsis_end(self, help_command, **kwargs): - if not help_command.synopsis: + if not help_command.synopsis and not help_command.command_table: super(BasicDocHandler, self).doc_synopsis_end( help_command=help_command, **kwargs) else: self.doc.style.end_codeblock() + def doc_global_option(self, help_command, **kwargs): + if not help_command.command_table: + super().doc_global_option(help_command, **kwargs) + def doc_examples(self, help_command, **kwargs): if help_command.examples: self.doc.style.h2('Examples') @@ -438,6 +441,3 @@ def doc_subitems_end(self, help_command, **kwargs): def doc_output(self, help_command, event_name, **kwargs): pass - - def doc_options_end(self, help_command, **kwargs): - self._add_top_level_args_reference(help_command) diff --git a/awscli/examples/global_options.rst b/awscli/examples/global_options.rst new file mode 100644 index 000000000000..088e1be7aeea --- /dev/null +++ b/awscli/examples/global_options.rst @@ -0,0 +1,72 @@ +``--debug`` (boolean) + + Turn on debug logging. + +``--endpoint-url`` (string) + + Override command's default URL with the given URL. + +``--no-verify-ssl`` (boolean) + + By default, the AWS CLI uses SSL when communicating with AWS services. For each SSL connection, the AWS CLI will verify SSL certificates. This option overrides the default behavior of verifying SSL certificates. + +``--no-paginate`` (boolean) + + Disable automatic pagination. + +``--output`` (string) + + The formatting style for command output. + + + * json + + * text + + * table + + +``--query`` (string) + + A JMESPath query to use in filtering the response data. + +``--profile`` (string) + + Use a specific profile from your credential file. + +``--region`` (string) + + The region to use. Overrides config/env settings. + +``--version`` (string) + + Display the version of this tool. + +``--color`` (string) + + Turn on/off color output. + + + * on + + * off + + * auto + + +``--no-sign-request`` (boolean) + + Do not sign requests. Credentials will not be loaded if this argument is provided. + +``--ca-bundle`` (string) + + The CA certificate bundle to use when verifying SSL certificates. Overrides config/env settings. + +``--cli-read-timeout`` (int) + + The maximum socket read time in seconds. If the value is set to 0, the socket read will be blocking and not timeout. The default value is 60 seconds. + +``--cli-connect-timeout`` (int) + + The maximum socket connect time in seconds. If the value is set to 0, the socket connect will be blocking and not timeout. The default value is 60 seconds. + diff --git a/awscli/examples/global_synopsis.rst b/awscli/examples/global_synopsis.rst new file mode 100644 index 000000000000..c5baaa9583ba --- /dev/null +++ b/awscli/examples/global_synopsis.rst @@ -0,0 +1,14 @@ +[--debug] +[--endpoint-url ] +[--no-verify-ssl] +[--no-paginate] +[--output ] +[--query ] +[--profile ] +[--region ] +[--version ] +[--color ] +[--no-sign-request] +[--ca-bundle ] +[--cli-read-timeout ] +[--cli-connect-timeout ] diff --git a/scripts/make-global-opts-documentation b/scripts/make-global-opts-documentation new file mode 100755 index 000000000000..3f8f345df62c --- /dev/null +++ b/scripts/make-global-opts-documentation @@ -0,0 +1,38 @@ +#!/usr/bin/env python +"""Generate a global options section. + +The purpose of this script is to pre-generate +global options for the help docs. +The output of this script is loaded and applied to +every subcommand's help docs. + +""" + +import os + +from awscli.clidriver import create_clidriver +from awscli.clidocs import ( + EXAMPLES_DIR, GLOBAL_OPTIONS_FILE, + GLOBAL_OPTIONS_SYNOPSIS_FILE, GlobalOptionsDocumenter +) + + +def main(): + if not os.path.isdir(EXAMPLES_DIR): + os.makedirs(EXAMPLES_DIR) + driver = create_clidriver() + options_help_command = driver.create_help_command() + synopsis_help_command = driver.create_help_command() + options_documenter = GlobalOptionsDocumenter(options_help_command) + synopsis_documenter = GlobalOptionsDocumenter(synopsis_help_command) + + with open(GLOBAL_OPTIONS_FILE, 'w') as f: + for line in options_documenter.doc_global_options(): + f.write(line) + with open(GLOBAL_OPTIONS_SYNOPSIS_FILE, 'w') as f: + for line in synopsis_documenter.doc_global_synopsis(): + f.write(line) + + +if __name__ == "__main__": + main() diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index b835806fc695..3acffd72c51e 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -46,9 +46,9 @@ def test_output(self): self.assert_contains('``--no-paginate``') # Arg with choices self.assert_contains('``--color``') - self.assert_contains('* on') - self.assert_contains('* off') - self.assert_contains('* auto') + self.assert_contains('* on') + self.assert_contains('* off') + self.assert_contains('* auto') # Then we should see the services. self.assert_contains('* ec2') self.assert_contains('* s3api') @@ -80,22 +80,23 @@ def test_operation_help_output(self): self.assert_contains('Launches the specified number of instances') self.assert_contains('``--count`` (string)') - def test_waiter_does_not_have_duplicate_global_params_link(self): - self.driver.main(['ec2', 'wait', 'help']) - self.assert_contains_with_count( - 'for descriptions of global parameters', 1) - def test_custom_service_help_output(self): self.driver.main(['s3', 'help']) self.assert_contains('.. _cli:aws s3:') self.assert_contains('high-level S3 commands') self.assert_contains('* cp') + def test_waiter_does_not_have_global_args(self): + self.driver.main(['ec2', 'wait', 'help']) + self.assert_not_contains('--debug') + self.assert_not_contains('Global Options') + def test_custom_operation_help_output(self): self.driver.main(['s3', 'ls', 'help']) self.assert_contains('.. _cli:aws s3 ls:') self.assert_contains('List S3 objects') self.assert_contains('--summarize') + self.assert_contains('--debug') def test_topic_list_help_output(self): self.driver.main(['help', 'topics']) diff --git a/tests/functional/test_globals.py b/tests/functional/test_globals.py new file mode 100644 index 000000000000..754826fb09f8 --- /dev/null +++ b/tests/functional/test_globals.py @@ -0,0 +1,23 @@ +import os +import unittest + +from awscli.testutils import create_clidriver +from awscli.clidocs import ( + GLOBAL_OPTIONS_FILE, GLOBAL_OPTIONS_SYNOPSIS_FILE, + GlobalOptionsDocumenter +) + + +class TestGlobalOptionsDocumenter(unittest.TestCase): + def setUp(self): + self.driver = create_clidriver() + self.help_command = self.driver.create_help_command() + self.globals = GlobalOptionsDocumenter(self.help_command) + + def test_doc_global_options_match_saved_content(self): + with open(GLOBAL_OPTIONS_FILE, 'r') as f: + self.assertEqual(self.globals.doc_global_options(), f.read()) + + def test_doc_global_synopsis_match_saved_content(self): + with open(GLOBAL_OPTIONS_SYNOPSIS_FILE, 'r') as f: + self.assertEqual(self.globals.doc_global_synopsis(), f.read()) diff --git a/tests/unit/customizations/test_commands.py b/tests/unit/customizations/test_commands.py index 869c50fd2aa9..b13eef06296d 100644 --- a/tests/unit/customizations/test_commands.py +++ b/tests/unit/customizations/test_commands.py @@ -15,7 +15,6 @@ from awscli.clidriver import CLIDriver from awscli.customizations.commands import BasicHelp, BasicCommand from awscli.customizations.commands import BasicDocHandler -from awscli.bcdoc.restdoc import ReSTDocument from botocore.hooks import HierarchicalEmitter from tests.unit.test_clidriver import FakeSession, FakeCommand @@ -144,45 +143,57 @@ def create_help_command(self): self.session, self.obj, self.command_table, self.arg_table ) - def test_includes_global_args_ref_in_man_description(self): - help_command = self.create_help_command() + def create_arg_table(self): + return CLIDriver().create_help_command().arg_table + + def generate_global_option_docs(self, help_command): operation_handler = BasicDocHandler(help_command) - operation_handler.doc_description(help_command=help_command) - rendered = help_command.doc.getvalue() - rendered = rendered.decode('utf-8') - # The links aren't generated in the "man" mode. - self.assertIn( - "See 'aws help' for descriptions of global parameters", rendered - ) + operation_handler.doc_global_option(help_command=help_command) + return help_command.doc.getvalue().decode('utf-8') - def test_includes_global_args_ref_in_html_description(self): - help_command = self.create_help_command() - help_command.doc.target = 'html' + def generate_global_synopsis_docs(self, help_command): operation_handler = BasicDocHandler(help_command) - operation_handler.doc_description(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - self.assertIn( - "See :doc:`'aws help' ` for descriptions of " - "global parameters", rendered - ) + operation_handler.doc_synopsis_end(help_command=help_command) + return help_command.doc.getvalue().decode('utf-8') + + def assert_global_args_documented(self, arg_table, content): + for arg in arg_table: + self.assertIn(arg_table.get(arg).cli_name, content) + + def assert_global_args_not_documented(self, arg_table, content): + for arg in arg_table: + self.assertNotIn(arg_table.get(arg).cli_name, content) - def test_includes_global_args_ref_in_man_options(self): + def test_includes_global_options_when_command_table_empty(self): help_command = self.create_help_command() - operation_handler = BasicDocHandler(help_command) - operation_handler.doc_options_end(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - # The links aren't generated in the "man" mode. - self.assertIn( - "See 'aws help' for descriptions of global parameters", rendered - ) + arg_table = self.create_arg_table() + help_command.arg_table = arg_table + rendered = self.generate_global_option_docs(help_command) + self.assert_global_args_documented(arg_table, rendered) - def test_includes_global_args_ref_in_html_options(self): + def test_excludes_global_options_when_command_table_not_empty(self): help_command = self.create_help_command() - help_command.doc.target = 'html' - operation_handler = BasicDocHandler(help_command) - operation_handler.doc_options_end(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - self.assertIn( - "See :doc:`'aws help' ` for descriptions of " - "global parameters", rendered - ) + arg_table = self.create_arg_table() + help_command.arg_table = arg_table + fake_command = FakeCommand(FakeSession()) + fake_command.NAME = 'command' + help_command.command_table = {'command': fake_command} + rendered = self.generate_global_option_docs(help_command) + self.assert_global_args_not_documented(arg_table, rendered) + + def test_includes_global_synopsis_when_command_table_empty(self): + help_command = self.create_help_command() + arg_table = self.create_arg_table() + help_command.arg_table = arg_table + rendered = self.generate_global_synopsis_docs(help_command) + self.assert_global_args_documented(arg_table, rendered) + + def test_excludes_global_synopsis_when_command_table_not_empty(self): + help_command = self.create_help_command() + arg_table = self.create_arg_table() + help_command.arg_table = arg_table + fake_command = FakeCommand(FakeSession()) + fake_command.NAME = 'command' + help_command.command_table = {'command': fake_command} + rendered = self.generate_global_synopsis_docs(help_command) + self.assert_global_args_not_documented(arg_table, rendered) diff --git a/tests/unit/test_clidocs.py b/tests/unit/test_clidocs.py index 3294f402e340..010698a734b0 100644 --- a/tests/unit/test_clidocs.py +++ b/tests/unit/test_clidocs.py @@ -18,10 +18,10 @@ from awscli.testutils import mock, unittest, FileCreator from awscli.clidocs import OperationDocumentEventHandler, \ CLIDocumentEventHandler, TopicListerDocumentEventHandler, \ - TopicDocumentEventHandler + TopicDocumentEventHandler, GlobalOptionsDocumenter from awscli.bcdoc.restdoc import ReSTDocument from awscli.help import ServiceHelpCommand, TopicListerCommand, \ - TopicHelpCommand + TopicHelpCommand, HelpCommand from awscli.arguments import CustomArgument @@ -365,48 +365,6 @@ def test_includes_webapi_crosslink_in_html(self): '`_', rendered) - def test_includes_global_args_ref_in_man_description(self): - help_command = self.create_help_command() - operation_handler = OperationDocumentEventHandler(help_command) - operation_handler.doc_description(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - # The links aren't generated in the "man" mode. - self.assertIn( - "See 'aws help' for descriptions of global parameters", rendered - ) - - def test_includes_global_args_ref_in_html_description(self): - help_command = self.create_help_command() - help_command.doc.target = 'html' - operation_handler = OperationDocumentEventHandler(help_command) - operation_handler.doc_description(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - self.assertIn( - "See :doc:`'aws help' ` for descriptions of " - "global parameters", rendered - ) - - def test_includes_global_args_ref_in_man_options(self): - help_command = self.create_help_command() - operation_handler = OperationDocumentEventHandler(help_command) - operation_handler.doc_options_end(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - # The links aren't generated in the "man" mode. - self.assertIn( - "See 'aws help' for descriptions of global parameters", rendered - ) - - def test_includes_global_args_ref_in_html_options(self): - help_command = self.create_help_command() - help_command.doc.target = 'html' - operation_handler = OperationDocumentEventHandler(help_command) - operation_handler.doc_options_end(help_command=help_command) - rendered = help_command.doc.getvalue().decode('utf-8') - self.assertIn( - "See :doc:`'aws help' ` for descriptions of " - "global parameters", rendered - ) - def test_includes_streaming_blob_options(self): help_command = self.create_help_command() blob_shape = Shape('blob_shape', {'type': 'blob'}) @@ -640,3 +598,43 @@ def test_description_tags_in_body(self): self.doc_handler.doc_description(self.cmd) contents = self.cmd.doc.getvalue().decode('utf-8') self.assertIn(ref_body, contents) + + +class TestGlobalOptionsDocumenter(unittest.TestCase): + def create_help_command(self): + types = ['blob', 'integer', 'boolean', 'string'] + arg_table = {} + for t in types: + name = f'{t}_type' + help_text = f'This arg type is {t}' + choices = ['A', 'B', 'C'] if t == 'string' else [] + arg_table[name] = CustomArgument(name=name, + cli_type_name=t, + help_text=help_text, + choices=choices) + help_command = mock.Mock(spec=HelpCommand) + help_command.arg_table = arg_table + help_command.doc = ReSTDocument() + return help_command + + def create_documenter(self): + return GlobalOptionsDocumenter(self.create_help_command()) + + def test_doc_global_options(self): + documenter = self.create_documenter() + options = documenter.doc_global_options() + self.assertIn('``--string_type`` (string)', options) + self.assertIn('``--integer_type`` (integer)', options) + self.assertIn('``--boolean_type`` (boolean)', options) + self.assertIn('``--blob_type`` (blob)', options) + self.assertIn('* A', options) + self.assertIn('* B', options) + self.assertIn('* C', options) + + def test_doc_global_synopsis(self): + documenter = self.create_documenter() + synopsis = documenter.doc_global_synopsis() + self.assertIn('[--string_type ]', synopsis) + self.assertIn('[--integer_type ]', synopsis) + self.assertIn('[--boolean_type]', synopsis) + self.assertIn('[--blob_type ]', synopsis)