From 0a32b27bd0c1e98fe1e224061f5853c949bd77ec Mon Sep 17 00:00:00 2001 From: Guy Smoilovsky Date: Wed, 15 Apr 2026 23:58:47 +0300 Subject: [PATCH 1/3] Parse client config env vars explicitly Stop treating any non-empty string as a truthy boolean and ensure HTTP timeout is loaded as a numeric value. Add small self-contained tests so common values like 'false' and '12.5' keep behaving the way users expect. --- dagshub/common/config.py | 14 +++++++-- tests/common/test_config.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/common/test_config.py diff --git a/dagshub/common/config.py b/dagshub/common/config.py index e82b0063..03c21b7b 100644 --- a/dagshub/common/config.py +++ b/dagshub/common/config.py @@ -21,6 +21,7 @@ HTTP_TIMEOUT_KEY = "DAGSHUB_HTTP_TIMEOUT" DAGSHUB_QUIET_KEY = "DAGSHUB_QUIET" DISABLE_TRACEPARENT_KEY = "DAGSHUB_DISABLE_TRACEPARENT" +TRUE_VALUES = {"1", "true", "yes", "on"} def set_host(new_host: str): @@ -32,6 +33,13 @@ def set_host(new_host: str): hostname, host, parsed_host = _hostname, _host, _parsed_host +def _get_boolean_env(key: str, default: bool = False) -> bool: + value = os.environ.get(key) + if value is None: + return default + return value.strip().lower() in TRUE_VALUES + + hostname = "" host = "" parsed_host = "" @@ -44,12 +52,12 @@ def set_host(new_host: str): password = os.environ.get(DAGSHUB_PASSWORD_KEY) custom_user_agent_suffix = f" dagshub-client-python/{__version__}" requests_headers = {"user-agent": USER_AGENT + custom_user_agent_suffix} -http_timeout = os.environ.get(HTTP_TIMEOUT_KEY, 30) +http_timeout = float(os.environ.get(HTTP_TIMEOUT_KEY, 30)) REPO_INFO_URL = "api/v1/repos/{owner}/{reponame}" -quiet = bool(os.environ.get(DAGSHUB_QUIET_KEY, False)) +quiet = _get_boolean_env(DAGSHUB_QUIET_KEY) -disable_traceparent = bool(os.environ.get(DISABLE_TRACEPARENT_KEY, False)) +disable_traceparent = _get_boolean_env(DISABLE_TRACEPARENT_KEY) # DVC config templates CONFIG_GITIGNORE = "/config.local\n/tmp\n/cache" diff --git a/tests/common/test_config.py b/tests/common/test_config.py new file mode 100644 index 00000000..9bccd972 --- /dev/null +++ b/tests/common/test_config.py @@ -0,0 +1,58 @@ +import importlib.util +import os +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +CONFIG_PATH = Path(__file__).resolve().parents[2] / "dagshub" / "common" / "config.py" + + +def load_config_module(): + fake_dagshub = types.ModuleType("dagshub") + fake_dagshub.__version__ = "test-version" + + fake_appdirs = types.ModuleType("appdirs") + fake_appdirs.user_cache_dir = lambda app_name: f"/tmp/{app_name}" + + fake_httpx = types.ModuleType("httpx") + fake_httpx_client = types.ModuleType("httpx._client") + fake_httpx_client.USER_AGENT = "test-agent" + + spec = importlib.util.spec_from_file_location("test_dagshub_common_config", CONFIG_PATH) + module = importlib.util.module_from_spec(spec) + with patch.dict( + sys.modules, + { + "dagshub": fake_dagshub, + "appdirs": fake_appdirs, + "httpx": fake_httpx, + "httpx._client": fake_httpx_client, + }, + ): + spec.loader.exec_module(module) + return module + + +class ConfigTestCase(unittest.TestCase): + def test_boolean_env_flags_are_parsed_from_string_values(self): + with patch.dict( + os.environ, + { + "DAGSHUB_QUIET": "false", + "DAGSHUB_DISABLE_TRACEPARENT": "1", + }, + clear=False, + ): + config = load_config_module() + + self.assertFalse(config.quiet) + self.assertTrue(config.disable_traceparent) + + def test_http_timeout_is_loaded_as_a_number(self): + with patch.dict(os.environ, {"DAGSHUB_HTTP_TIMEOUT": "12.5"}, clear=False): + config = load_config_module() + + self.assertEqual(config.http_timeout, 12.5) + self.assertIsInstance(config.http_timeout, float) From 851e877234ca141487ef2f46e41e8ff494250330 Mon Sep 17 00:00:00 2001 From: Guy Smoilovsky Date: Thu, 16 Apr 2026 15:11:58 +0300 Subject: [PATCH 2/3] Keep HTTP timeout parsing aligned with integer seconds Continue parsing the timeout env var into a numeric type so overrides are not left as strings, but narrow it back to int seconds to preserve the simpler historical contract while still fixing the boolean parsing bug. --- dagshub/common/config.py | 2 +- tests/common/test_config.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dagshub/common/config.py b/dagshub/common/config.py index 03c21b7b..18608e42 100644 --- a/dagshub/common/config.py +++ b/dagshub/common/config.py @@ -52,7 +52,7 @@ def _get_boolean_env(key: str, default: bool = False) -> bool: password = os.environ.get(DAGSHUB_PASSWORD_KEY) custom_user_agent_suffix = f" dagshub-client-python/{__version__}" requests_headers = {"user-agent": USER_AGENT + custom_user_agent_suffix} -http_timeout = float(os.environ.get(HTTP_TIMEOUT_KEY, 30)) +http_timeout = int(os.environ.get(HTTP_TIMEOUT_KEY, 30)) REPO_INFO_URL = "api/v1/repos/{owner}/{reponame}" quiet = _get_boolean_env(DAGSHUB_QUIET_KEY) diff --git a/tests/common/test_config.py b/tests/common/test_config.py index 9bccd972..a2629fdb 100644 --- a/tests/common/test_config.py +++ b/tests/common/test_config.py @@ -50,9 +50,9 @@ def test_boolean_env_flags_are_parsed_from_string_values(self): self.assertFalse(config.quiet) self.assertTrue(config.disable_traceparent) - def test_http_timeout_is_loaded_as_a_number(self): - with patch.dict(os.environ, {"DAGSHUB_HTTP_TIMEOUT": "12.5"}, clear=False): + def test_http_timeout_is_loaded_as_an_integer(self): + with patch.dict(os.environ, {"DAGSHUB_HTTP_TIMEOUT": "12"}, clear=False): config = load_config_module() - self.assertEqual(config.http_timeout, 12.5) - self.assertIsInstance(config.http_timeout, float) + self.assertEqual(config.http_timeout, 12) + self.assertIsInstance(config.http_timeout, int) From c53faca3571508d6c3b76c1e7626425a99dd4bfb Mon Sep 17 00:00:00 2001 From: Guy Smoilovsky Date: Thu, 16 Apr 2026 15:13:09 +0300 Subject: [PATCH 3/3] Drop the HTTP timeout cast and keep the boolean fix The boolean env parsing bug was clearly broken, but casting the timeout introduced extra behavior without enough payoff. Keep the risk-free boolean cleanup and stop changing the timeout value type until there is a stronger reason to tighten that contract. --- dagshub/common/config.py | 2 +- tests/common/test_config.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/dagshub/common/config.py b/dagshub/common/config.py index 18608e42..8e5e8949 100644 --- a/dagshub/common/config.py +++ b/dagshub/common/config.py @@ -52,7 +52,7 @@ def _get_boolean_env(key: str, default: bool = False) -> bool: password = os.environ.get(DAGSHUB_PASSWORD_KEY) custom_user_agent_suffix = f" dagshub-client-python/{__version__}" requests_headers = {"user-agent": USER_AGENT + custom_user_agent_suffix} -http_timeout = int(os.environ.get(HTTP_TIMEOUT_KEY, 30)) +http_timeout = os.environ.get(HTTP_TIMEOUT_KEY, 30) REPO_INFO_URL = "api/v1/repos/{owner}/{reponame}" quiet = _get_boolean_env(DAGSHUB_QUIET_KEY) diff --git a/tests/common/test_config.py b/tests/common/test_config.py index a2629fdb..aaf9e602 100644 --- a/tests/common/test_config.py +++ b/tests/common/test_config.py @@ -49,10 +49,3 @@ def test_boolean_env_flags_are_parsed_from_string_values(self): self.assertFalse(config.quiet) self.assertTrue(config.disable_traceparent) - - def test_http_timeout_is_loaded_as_an_integer(self): - with patch.dict(os.environ, {"DAGSHUB_HTTP_TIMEOUT": "12"}, clear=False): - config = load_config_module() - - self.assertEqual(config.http_timeout, 12) - self.assertIsInstance(config.http_timeout, int)