Skip to content

Commit 77bad09

Browse files
authored
feat: Allow setting config options via env var (#346)
1 parent 520bc71 commit 77bad09

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

dataframely/config.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# SPDX-License-Identifier: BSD-3-Clause
33

44
import contextlib
5+
import os
56
import sys
67
from types import TracebackType
7-
from typing import TypedDict
8+
from typing import Any, TypedDict, cast, get_type_hints
89

910
if sys.version_info >= (3, 11):
1011
from typing import Unpack
@@ -17,22 +18,37 @@ class Options(TypedDict):
1718
max_sampling_iterations: int
1819

1920

20-
def default_options() -> Options:
21+
_ENV_PREFIX = "DATAFRAMELY_"
22+
23+
24+
def _builtin_defaults() -> Options:
2125
return {
2226
"max_sampling_iterations": 10_000,
2327
}
2428

2529

30+
def _init_options() -> Options:
31+
options: dict[str, Any] = dict(_builtin_defaults())
32+
for key, target_type in get_type_hints(Options).items():
33+
env_name = f"{_ENV_PREFIX}{key.upper()}"
34+
if env_name in os.environ:
35+
options[key] = target_type(os.environ[env_name])
36+
return cast(Options, options)
37+
38+
39+
_DEFAULT_OPTIONS = _init_options()
40+
41+
2642
class Config(contextlib.ContextDecorator):
2743
"""An object to track global configuration for operations in dataframely."""
2844

2945
#: The currently valid config options.
30-
options: Options = default_options()
46+
options: Options = _DEFAULT_OPTIONS.copy()
3147
#: Singleton stack to track where to go back after exiting a context.
3248
_stack: list[Options] = []
3349

3450
def __init__(self, **options: Unpack[Options]) -> None:
35-
self._local_options: Options = {**default_options(), **options}
51+
self._local_options: Options = {**_DEFAULT_OPTIONS, **options}
3652

3753
@staticmethod
3854
def set_max_sampling_iterations(iterations: int) -> None:
@@ -43,7 +59,7 @@ def set_max_sampling_iterations(iterations: int) -> None:
4359
@staticmethod
4460
def restore_defaults() -> None:
4561
"""Restore the defaults of the configuration."""
46-
Config.options = default_options()
62+
Config.options = _DEFAULT_OPTIONS.copy()
4763

4864
# ------------------------------------ CONTEXT ----------------------------------- #
4965

tests/test_config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Copyright (c) QuantCo 2025-2026
22
# SPDX-License-Identifier: BSD-3-Clause
33

4+
import importlib
5+
6+
import pytest
7+
48
import dataframely as dy
9+
from dataframely import config as _config
510

611

712
def test_config_global() -> None:
@@ -40,3 +45,23 @@ def test_config_global_local() -> None:
4045
assert dy.Config.options["max_sampling_iterations"] == 50
4146
finally:
4247
dy.Config.restore_defaults()
48+
49+
50+
def test_config_env_var_override(monkeypatch: pytest.MonkeyPatch) -> None:
51+
monkeypatch.setenv("DATAFRAMELY_MAX_SAMPLING_ITERATIONS", "123")
52+
try:
53+
importlib.reload(_config)
54+
assert _config.Config.options["max_sampling_iterations"] == 123
55+
finally:
56+
monkeypatch.delenv("DATAFRAMELY_MAX_SAMPLING_ITERATIONS")
57+
importlib.reload(_config)
58+
# Re-bind dy.Config to the reloaded module's class to keep state consistent.
59+
dy.Config = _config.Config # type: ignore
60+
dy.Config.restore_defaults()
61+
62+
63+
def test_config_env_var_not_reread_after_startup(
64+
monkeypatch: pytest.MonkeyPatch,
65+
) -> None:
66+
monkeypatch.setenv("DATAFRAMELY_MAX_SAMPLING_ITERATIONS", "777")
67+
assert dy.Config.options["max_sampling_iterations"] == 10_000

0 commit comments

Comments
 (0)