From fc1f257f78d8ea15e5fc86c6c1a3d5395f4fb74e Mon Sep 17 00:00:00 2001 From: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:21:08 -0700 Subject: [PATCH] Preserve an empty TLS table in envconfig dict conversion ClientConfigProfile.to_dict() left out "tls" when the TLS config had no fields set, and ClientConfigTLS.from_dict() returned None for an empty dictionary, so an empty [profile.x.tls] table was lost when a loaded profile went through a dictionary. Keep "tls" whenever the profile has a TLS config, and return None only for None. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP Signed-off-by: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com> --- changelog/fixed/persistent-pelican.md | 1 + temporalio/envconfig.py | 12 ++++-- tests/test_envconfig.py | 55 +++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 changelog/fixed/persistent-pelican.md diff --git a/changelog/fixed/persistent-pelican.md b/changelog/fixed/persistent-pelican.md new file mode 100644 index 0000000000..27db3dc930 --- /dev/null +++ b/changelog/fixed/persistent-pelican.md @@ -0,0 +1 @@ +Keep an empty TLS table when `ClientConfigProfile` and `ClientConfig` are converted to and from dictionaries, so a profile that enables TLS with default settings does not lose it. diff --git a/temporalio/envconfig.py b/temporalio/envconfig.py index ad413b9a9a..8eb0d5314c 100644 --- a/temporalio/envconfig.py +++ b/temporalio/envconfig.py @@ -147,8 +147,12 @@ def to_connect_tls_config(self) -> bool | temporalio.service.TLSConfig: @classmethod def from_dict(cls, d: ClientConfigTLSDict | None) -> Self | None: - """Create a ClientConfigTLS from a dictionary.""" - if not d: + """Create a ClientConfigTLS from a dictionary. + + ``None`` means there is no TLS table. An empty dictionary is a TLS table + with nothing set, so it gives a default ``ClientConfigTLS``. + """ + if d is None: return None return cls( disabled=d.get("disabled"), @@ -213,8 +217,8 @@ def to_dict(self) -> ClientConfigProfileDict: d["namespace"] = self.namespace if self.api_key is not None: d["api_key"] = self.api_key - if self.tls and (tls_dict := self.tls.to_dict()): - d["tls"] = tls_dict + if self.tls is not None: + d["tls"] = self.tls.to_dict() if self.grpc_meta: d["grpc_meta"] = self.grpc_meta return d diff --git a/tests/test_envconfig.py b/tests/test_envconfig.py index 0b44db7319..2842f098ce 100644 --- a/tests/test_envconfig.py +++ b/tests/test_envconfig.py @@ -2,6 +2,7 @@ import textwrap from pathlib import Path from types import SimpleNamespace +from typing import Any from unittest.mock import AsyncMock import pytest @@ -694,6 +695,60 @@ def test_client_config_to_from_dict(): assert empty_config == new_empty_config +def test_client_config_tls_empty_dict_is_not_missing(): + """Test that an empty TLS table is distinct from no TLS table in both directions.""" + empty_tls_profile = ClientConfigProfile(tls=ClientConfigTLS()) + assert empty_tls_profile.to_dict() == {"tls": {}} + assert ClientConfigProfile.from_dict({"tls": {}}) == empty_tls_profile + assert ClientConfigTLS.from_dict({}) == ClientConfigTLS() + assert ClientConfigTLS.from_dict(None) is None + + +@pytest.mark.parametrize( + ("toml_tls", "tls_dict", "connect_tls"), + [ + pytest.param("", None, None, id="no-tls-table"), + pytest.param("[profile.default.tls]", {}, TLSConfig(), id="empty-tls-table"), + pytest.param( + "[profile.default.tls]\ndisabled = false", + {"disabled": False}, + TLSConfig(), + id="disabled-false", + ), + pytest.param( + "[profile.default.tls]\ndisabled = true", + {"disabled": True}, + False, + id="disabled-true", + ), + ], +) +def test_client_config_dict_round_trip_keeps_tls( + toml_tls: str, + tls_dict: dict[str, bool] | None, + connect_tls: bool | TLSConfig | None, +): + """Test that dictionary round trips preserve TLS and the connect config.""" + config = ClientConfig.load( + config_source=f"[profile.default]\naddress = 'some-address'\n{toml_tls}" + ) + expected_profile: dict[str, Any] = {"address": "some-address"} + if tls_dict is not None: + expected_profile["tls"] = tls_dict + + config_dict = config.to_dict() + assert config_dict == {"default": expected_profile} + + new_config = ClientConfig.from_dict(config_dict) + assert new_config == config + connect_config = new_config.profiles["default"].to_client_connect_config() + assert connect_config == config.profiles["default"].to_client_connect_config() + if connect_tls is None: + assert "tls" not in connect_config + else: + assert connect_config.get("tls") == connect_tls + + def test_grpc_metadata_normalization_from_toml(): """Test that gRPC metadata keys get normalized from TOML.""" toml_config = textwrap.dedent(