From c5969d1227e04f0e9e18d5ba1ded8799b0685c17 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 3 Aug 2026 18:08:09 -0700 Subject: [PATCH 1/3] Fix #436: add --description/-d to publish tabcmd Classic accepts --description/-d when publishing to set the workbook or data source description. tabcmd 2 was missing the flag, so users had to publish and then run a separate update to set the description. Add --description/-d to set_publish_args and set it on the WorkbookItem or DatasourceItem before calling server.workbooks.publish or server.datasources.publish. TSC's _generate_xml already includes description in the publish request body when the attribute is set. Fixes #436. --- .../datasources_and_workbooks/publish_command.py | 4 ++++ tabcmd/execution/global_options.py | 1 + tabcmd/locales/en/tabcmd_messages_en.properties | 1 + tests/parsers/test_parser_publish.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 1dd2bfc1..854c20e4 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -177,6 +177,8 @@ def publish_workbook_file(args, logger, server, project_id, str_filename, publis raise AttributeError("Cannot specify both a user and group for thumbnails.") new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) + if getattr(args, "description", None): + new_workbook.description = args.description new_workbook = server.workbooks.publish( new_workbook, str_filename, @@ -193,6 +195,8 @@ def publish_workbook_file(args, logger, server, project_id, str_filename, publis def publish_datasource_file(args, logger, server, project_id, str_filename, publish_mode, credentials): new_datasource = TSC.DatasourceItem(project_id, name=args.name) new_datasource.use_remote_query_agent = args.use_tableau_bridge + if getattr(args, "description", None): + new_datasource.description = args.description new_datasource = server.datasources.publish( new_datasource, str_filename, publish_mode, connection_credentials=credentials ) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 1a24133c..d3db7c0c 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -252,6 +252,7 @@ def set_destination_filename_arg(parser): def set_publish_args(parser): parser.add_argument("-n", "--name", help=_("publish.options.name")) + parser.add_argument("-d", "--description", help=_("publish.options.description")) creds = parser.add_mutually_exclusive_group() creds.add_argument("--oauth-username", help=_("publish.options.oauth-username")) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 7eda4bff..50b2f168 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -78,6 +78,7 @@ publish.errors.server_resource_not_found=The resource you specified does not exi publish.options.append=Append extract file to existing data source publish.options.db-password=Database password for all data sources publish.options.db-username=Database username for all data sources +publish.options.description=Description for the workbook or data source, sent with the publish request publish.options.encrypt_extracts=Encrypt extracts in the workbook or datasource being published to the server. publish.options.name=Workbook or data source name on the server. If omitted, the workbook or data source will be named after the file name, without the twb(x), tds(x), or tde extension. Publishing a .tde file will create a data source publish.options.oauth-username=Use the credentials saved on the server keychain associated with USERNAME to publish diff --git a/tests/parsers/test_parser_publish.py b/tests/parsers/test_parser_publish.py index b26f911d..728eb8e7 100644 --- a/tests/parsers/test_parser_publish.py +++ b/tests/parsers/test_parser_publish.py @@ -105,3 +105,18 @@ def test_publish_parser_deprecated_options(self): def test_publish_parser_use_bridge_option(self): mock_args = [commandname, "filename.twbx", "--use-tableau-bridge"] args = self.parser_under_test.parse_args(mock_args) + + def test_publish_parser_description_long_flag(self): + mock_args = [commandname, "filename.twbx", "--description", "My workbook description"] + args = self.parser_under_test.parse_args(mock_args) + assert args.description == "My workbook description", args + + def test_publish_parser_description_short_flag(self): + mock_args = [commandname, "filename.twbx", "-d", "Short desc"] + args = self.parser_under_test.parse_args(mock_args) + assert args.description == "Short desc", args + + def test_publish_parser_description_optional(self): + mock_args = [commandname, "filename.twbx"] + args = self.parser_under_test.parse_args(mock_args) + assert args.description is None, args From 64df6efc8c61cc7324cb03678c30cd3dda4e14ae Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Sat, 19 Sep 2026 19:29:05 -0700 Subject: [PATCH 2/3] fix: preserve explicit empty --description in publish Addresses Copilot review feedback on PR #448: replace the truthy \`if getattr(args, "description", None):\` check in both publish_workbook_file and publish_datasource_file with an \`is not None\` check so that \`--description ""\` clears an existing description on replace instead of being silently dropped. --- tabcmd/commands/datasources_and_workbooks/publish_command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 854c20e4..7e5f3989 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -177,7 +177,7 @@ def publish_workbook_file(args, logger, server, project_id, str_filename, publis raise AttributeError("Cannot specify both a user and group for thumbnails.") new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) - if getattr(args, "description", None): + if getattr(args, "description", None) is not None: new_workbook.description = args.description new_workbook = server.workbooks.publish( new_workbook, @@ -195,7 +195,7 @@ def publish_workbook_file(args, logger, server, project_id, str_filename, publis def publish_datasource_file(args, logger, server, project_id, str_filename, publish_mode, credentials): new_datasource = TSC.DatasourceItem(project_id, name=args.name) new_datasource.use_remote_query_agent = args.use_tableau_bridge - if getattr(args, "description", None): + if getattr(args, "description", None) is not None: new_datasource.description = args.description new_datasource = server.datasources.publish( new_datasource, str_filename, publish_mode, connection_credentials=credentials From 8d57408e4a6d24c42de94cb78ea9b7b521dd77dd Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 20 Oct 2026 16:48:11 -0700 Subject: [PATCH 3/3] Add command-level test coverage for publish --description Copilot flagged that the existing parser tests for --description/-d only verify that argparse stores the value; they don't exercise the assignment onto the WorkbookItem/DatasourceItem that actually gets published. A regression that dropped the description assignment (publish_command.py lines 181/199) would have passed the existing suite since those tests only assert that workbooks.publish was called. Add two command-level tests asserting the published item's description matches the requested value, one for the workbook path (via run_command) and one for the datasource path (calling publish_datasource_file directly, since run_command's extension-based dispatch can't be relied on here due to the class-level os.path patch). Verified both fail if the description assignment is removed and pass with it restored. Co-Authored-By: Claude Code --- tests/commands/test_publish_command.py | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/commands/test_publish_command.py b/tests/commands/test_publish_command.py index 368fd997..5c02a3a2 100644 --- a/tests/commands/test_publish_command.py +++ b/tests/commands/test_publish_command.py @@ -66,6 +66,61 @@ def test_publish_with_creds(self, mock_path, mock_glob, mock_session): PublishCommand.run_command(mock_args) mock_session.internal_server.workbooks.publish.assert_called() + # regression test: the --description value must actually end up on the WorkbookItem + # that gets published, not just be parsed into args (see PR #448 review feedback) + def test_publish_with_description_sets_workbook_description(self, mock_path, mock_glob, mock_session): + set_up_mock_server(mock_session) + mock_path = set_up_mock_path(mock_path) + + mock_args.overwrite = False + mock_args.filename = "existing_file.twbx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + mock_args.db_username = None + mock_args.oauth_username = None + mock_args.append = False + mock_args.replace = False + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + mock_args.description = "My workbook description" + + PublishCommand.run_command(mock_args) + + published_workbook = mock_session.internal_server.workbooks.publish.call_args[0][0] + self.assertEqual(published_workbook.description, "My workbook description") + + # same regression, but for the datasource publish path (publish_command.py line ~198). + # calls publish_datasource_file directly: run_command dispatches by file extension via + # the class-patched os.path, which these tests also stub out, so it can't be relied on + # to route a .tdsx through the datasource branch here. + def test_publish_with_description_sets_datasource_description(self, mock_path, mock_glob, mock_session): + set_up_mock_server(mock_session) + mock_path = set_up_mock_path(mock_path) + + mock_server = mock_session.internal_server + mock_server.datasources = MagicMock() + mock_server.datasources.publish = MagicMock(return_value=mock_data.create_fake_item()) + + mock_args.name = "" + mock_args.use_tableau_bridge = False + mock_args.description = "My datasource description" + + PublishCommand.publish_datasource_file( + args=mock_args, + logger=mock_logger, + server=mock_server, + project_id="fake-project-id", + str_filename="existing_file.tdsx", + publish_mode=TSC.Server.PublishMode.CreateNew, + credentials=None, + ) + + published_datasource = mock_server.datasources.publish.call_args[0][0] + self.assertEqual(published_datasource.description, "My datasource description") + def test_get_files_to_publish_twbx(self, mock_path, mock_glob, mock_session): set_up_mock_server(mock_session) mock_path = set_up_mock_path(mock_path)