From c2c586a6f92be439f6d4cd29e3e5df429737461f Mon Sep 17 00:00:00 2001 From: joshyam-k Date: Wed, 12 Nov 2025 11:56:03 -0800 Subject: [PATCH 1/4] fix sorting bug --- rsconnect/main.py | 3 +++ rsconnect/metadata.py | 13 +++++++++++-- tests/test_main.py | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 9c6b9c099..4a31ac38b 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -666,6 +666,9 @@ def add( set_verbosity(verbose) output_params(ctx, locals().items()) + if not name: + ctx.fail('--name is required') + validation.validate_connection_options( ctx=ctx, url=server, diff --git a/rsconnect/metadata.py b/rsconnect/metadata.py index 6e731b562..7b2b0579b 100644 --- a/rsconnect/metadata.py +++ b/rsconnect/metadata.py @@ -15,7 +15,16 @@ from io import BufferedWriter from os.path import abspath, basename, dirname, exists, join from threading import Lock -from typing import TYPE_CHECKING, Callable, Dict, Generic, Mapping, Optional, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Generic, + Mapping, + Optional, + TypeVar, + Union, +) from urllib.parse import urlparse # Even though TypedDict is available in Python 3.8, because it's used with NotRequired, @@ -316,7 +325,7 @@ def get_all_servers(self): :return: the sorted list of known servers. """ - return self._get_sorted_values(lambda s: s["name"]) + return self._get_sorted_values(lambda s: s["name"] or "") def set( self, diff --git a/tests/test_main.py b/tests/test_main.py index 01b6ecbc5..def7751a0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1117,6 +1117,30 @@ def test_add_shinyapps_missing_options(self): if original_server_value: os.environ["CONNECT_SERVER"] = original_server_value + def test_add_missing_name(self): + """Test that the add command requires --name parameter.""" + original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) + original_server_value = os.environ.pop("CONNECT_SERVER", None) + try: + runner = CliRunner() + result = runner.invoke( + cli, + [ + "add", + "--server", + "https://connect.example.com", + "--api-key", + "test-key", + ], + ) + assert result.exit_code != 0, result.output + assert "--name is required" in result.output + finally: + if original_api_key_value: + os.environ["CONNECT_API_KEY"] = original_api_key_value + if original_server_value: + os.environ["CONNECT_SERVER"] = original_server_value + def test_env_management_callback(self): ctx = click.Context(cli) From e379b83f42cb4a771c176d2a5e52e3d8a98eb5c7 Mon Sep 17 00:00:00 2001 From: joshyam-k Date: Wed, 12 Nov 2025 12:05:13 -0800 Subject: [PATCH 2/4] lint --- rsconnect/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 4a31ac38b..168d9d97b 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -667,7 +667,7 @@ def add( output_params(ctx, locals().items()) if not name: - ctx.fail('--name is required') + ctx.fail("--name is required") validation.validate_connection_options( ctx=ctx, From 8604fdc900fea1d15a2a26914e936697406c44f8 Mon Sep 17 00:00:00 2001 From: joshyam-k Date: Thu, 20 Nov 2025 10:03:07 -0800 Subject: [PATCH 3/4] make name optional again and update tests --- rsconnect/main.py | 3 --- rsconnect/metadata.py | 2 +- tests/test_main.py | 25 ------------------------- tests/test_metadata.py | 28 ++++++++++++++++------------ 4 files changed, 17 insertions(+), 41 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 168d9d97b..9c6b9c099 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -666,9 +666,6 @@ def add( set_verbosity(verbose) output_params(ctx, locals().items()) - if not name: - ctx.fail("--name is required") - validation.validate_connection_options( ctx=ctx, url=server, diff --git a/rsconnect/metadata.py b/rsconnect/metadata.py index 7b2b0579b..7ea6e180c 100644 --- a/rsconnect/metadata.py +++ b/rsconnect/metadata.py @@ -325,7 +325,7 @@ def get_all_servers(self): :return: the sorted list of known servers. """ - return self._get_sorted_values(lambda s: s["name"] or "") + return self._get_sorted_values(lambda s: s.get("name") or "") def set( self, diff --git a/tests/test_main.py b/tests/test_main.py index def7751a0..a4692a746 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,7 +4,6 @@ from os.path import join from unittest import TestCase, mock - import click import httpretty import pytest @@ -1117,30 +1116,6 @@ def test_add_shinyapps_missing_options(self): if original_server_value: os.environ["CONNECT_SERVER"] = original_server_value - def test_add_missing_name(self): - """Test that the add command requires --name parameter.""" - original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) - original_server_value = os.environ.pop("CONNECT_SERVER", None) - try: - runner = CliRunner() - result = runner.invoke( - cli, - [ - "add", - "--server", - "https://connect.example.com", - "--api-key", - "test-key", - ], - ) - assert result.exit_code != 0, result.output - assert "--name is required" in result.output - finally: - if original_api_key_value: - os.environ["CONNECT_API_KEY"] = original_api_key_value - if original_server_value: - os.environ["CONNECT_SERVER"] = original_server_value - def test_env_management_callback(self): ctx = click.Context(cli) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index f64882cf0..c144626f1 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -32,7 +32,8 @@ def setUp(self): secret="c29tZVNlY3JldAo=", ) self.server_store.set("qux", "https://example.snowflakecomputing.app", snowflake_connection_name="dev") - self.assertEqual(len(self.server_store.get_all_servers()), 4, "Unexpected servers after setup") + self.server_store.set("None", "http://connect.test", "notAnApiKey") + self.assertEqual(len(self.server_store.get_all_servers()), 5, "Unexpected servers after setup") def tearDown(self): # clean up our temp test directory created with tempfile.mkdtemp() @@ -93,21 +94,23 @@ def test_remove_by_url(self): def test_remove_not_found(self): self.assertFalse(self.server_store.remove_by_name("frazzle")) - self.assertEqual(len(self.server_store.get_all_servers()), 4) + self.assertEqual(len(self.server_store.get_all_servers()), 5) self.assertFalse(self.server_store.remove_by_url("http://frazzle")) - self.assertEqual(len(self.server_store.get_all_servers()), 4) + self.assertEqual(len(self.server_store.get_all_servers()), 5) def test_list(self): servers = self.server_store.get_all_servers() - self.assertEqual(len(servers), 4) - self.assertEqual(servers[0]["name"], "bar") - self.assertEqual(servers[0]["url"], "http://connect.remote") - self.assertEqual(servers[1]["name"], "baz") - self.assertEqual(servers[1]["url"], "https://shinyapps.io") - self.assertEqual(servers[2]["name"], "foo") - self.assertEqual(servers[2]["url"], "http://connect.local") - self.assertEqual(servers[3]["name"], "qux") - self.assertEqual(servers[3]["url"], "https://example.snowflakecomputing.app") + self.assertEqual(len(servers), 5) + self.assertEqual(servers[0]["name"], "None") + self.assertEqual(servers[0]["url"], "http://connect.test") + self.assertEqual(servers[1]["name"], "bar") + self.assertEqual(servers[1]["url"], "http://connect.remote") + self.assertEqual(servers[2]["name"], "baz") + self.assertEqual(servers[2]["url"], "https://shinyapps.io") + self.assertEqual(servers[3]["name"], "foo") + self.assertEqual(servers[3]["url"], "http://connect.local") + self.assertEqual(servers[4]["name"], "qux") + self.assertEqual(servers[4]["url"], "https://example.snowflakecomputing.app") def check_resolve_call(self, name, server, api_key, insecure, ca_cert, should_be_from_store): server_data = self.server_store.resolve(name, server) @@ -133,6 +136,7 @@ def test_resolve_by_default(self): self.server_store.remove_by_url("http://connect.remote") self.server_store.remove_by_url("https://shinyapps.io") self.server_store.remove_by_name("qux") + self.server_store.remove_by_name("None") self.check_resolve_call(None, None, None, None, None, True) def test_resolve_from_args(self): From e4e4846b0e3080cbde016997587b2bad54df2bd9 Mon Sep 17 00:00:00 2001 From: joshyam-k Date: Thu, 20 Nov 2025 10:10:20 -0800 Subject: [PATCH 4/4] update changelog --- docs/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 01dd6a7c8..1b0937f20 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ?? +### Fixed + +- `rsconnect list` now properly functions when a stored server has no nickname. + ## [1.28.0] - 2025-11-06 ### Added