Skip to content

Commit f3bcd94

Browse files
committed
feat(config): env var override
1 parent 21cbc31 commit f3bcd94

File tree

2 files changed

+99
-21
lines changed

2 files changed

+99
-21
lines changed

src/jmclient/configure.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import atexit
2-
import io
32
import logging
43
import os
54
import re
@@ -91,8 +90,10 @@ def jm_single() -> AttributeDict:
9190
'POLICY': ['absurd_fee_per_kb', 'taker_utxo_retries',
9291
'taker_utxo_age', 'taker_utxo_amtpercent']}
9392

94-
_DEFAULT_INTEREST_RATE = "0.015"
93+
_ENV_VAR_PREFIX = "JM_"
94+
_SECTIONS_WITH_SUBSECTIONS = {"MESSAGING"}
9595

96+
_DEFAULT_INTEREST_RATE = "0.015"
9697
_DEFAULT_BONDLESS_MAKERS_ALLOWANCE = "0.125"
9798

9899
defaultconfig = \
@@ -673,9 +674,8 @@ def _remove_unwanted_default_settings(config: ConfigParser) -> None:
673674
if section.startswith('MESSAGING:'):
674675
config.remove_section(section)
675676

676-
def load_program_config(config_path: str = "", bs: Optional[str] = None,
677-
plugin_services: List[JMPluginService] = []) -> None:
678-
global_singleton.config.read_file(io.StringIO(defaultconfig))
677+
678+
def set_paths(config_path: str = "") -> None:
679679
if not config_path:
680680
config_path = lookup_appdata_folder(global_singleton.APPNAME)
681681
# we set the global home directory, but keep the config_path variable
@@ -692,29 +692,70 @@ def load_program_config(config_path: str = "", bs: Optional[str] = None,
692692
if not os.path.exists(os.path.join(global_singleton.datadir, "cmtdata")):
693693
os.makedirs(os.path.join(global_singleton.datadir, "cmtdata"))
694694
global_singleton.config_location = os.path.join(
695-
global_singleton.datadir, global_singleton.config_location)
695+
global_singleton.datadir, global_singleton.config_location
696+
)
696697

697-
_remove_unwanted_default_settings(global_singleton.config)
698+
699+
def read_config_file() -> bool:
698700
try:
699-
loadedFiles = global_singleton.config.read(
700-
[global_singleton.config_location])
701+
loaded = global_singleton.config.read([global_singleton.config_location])
701702
except UnicodeDecodeError:
702-
jmprint("Error loading `joinmarket.cfg`, invalid file format.",
703-
"info")
703+
jmprint("Error loading `joinmarket.cfg`, invalid file format.", "info")
704704
sys.exit(EXIT_FAILURE)
705+
return len(loaded) == 1
706+
705707

708+
def write_config_file(config: str = defaultconfig) -> bool:
709+
with open(global_singleton.config_location, "w") as configfile:
710+
configfile.write(config)
711+
jmprint(
712+
"Created a new `joinmarket.cfg`. Please review and adopt the "
713+
"settings and restart joinmarket.",
714+
"info",
715+
)
716+
sys.exit(EXIT_FAILURE)
717+
718+
719+
def override(config: ConfigParser, bs: Optional[str] = None) -> None:
720+
_remove_unwanted_default_settings(config)
706721
# Hack required for bitcoin-rpc-no-history and probably others
707722
# (historicaly electrum); must be able to enforce a different blockchain
708723
# interface even in default/new load.
709724
if bs:
710-
global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
711-
# Create default config file if not found
712-
if len(loadedFiles) != 1:
713-
with open(global_singleton.config_location, "w") as configfile:
714-
configfile.write(defaultconfig)
715-
jmprint("Created a new `joinmarket.cfg`. Please review and adopt the "
716-
"settings and restart joinmarket.", "info")
717-
sys.exit(EXIT_FAILURE)
725+
config.set("BLOCKCHAIN", "blockchain_source", bs)
726+
727+
728+
def override_from_environment(config: ConfigParser) -> ConfigParser:
729+
for key, value in os.environ.items():
730+
if key.startswith(_ENV_VAR_PREFIX):
731+
key = key.removeprefix(_ENV_VAR_PREFIX)
732+
section, key = key.split("_", 1)
733+
if section in _SECTIONS_WITH_SUBSECTIONS:
734+
sub, key = key.split("_", 1)
735+
section = f"{section}:{sub.lower()}"
736+
key = key.lower()
737+
if not config.has_section(section):
738+
config.add_section(section)
739+
log.info(f"Overriding [{section}] {key}={value}")
740+
config.set(section, key, value)
741+
return config
742+
743+
744+
def load_program_config(
745+
config_path: str = "",
746+
bs: Optional[str] = None,
747+
plugin_services: List[JMPluginService] = [],
748+
from_environment: bool = False,
749+
) -> None:
750+
set_paths(config_path)
751+
global_singleton.config.read_string(defaultconfig)
752+
if from_environment:
753+
override_from_environment(global_singleton.config)
754+
else:
755+
override(global_singleton.config, bs)
756+
# Create default config file if not found
757+
if not read_config_file():
758+
write_config_file()
718759

719760
loglevel = global_singleton.config.get("LOGGING", "console_log_level")
720761
try:

test/jmclient/test_configure.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
'''test configure module.'''
22

3+
import os
4+
from configparser import ConfigParser
5+
36
import pytest
4-
from jmclient import load_test_config, jm_single
5-
from jmclient.configure import get_blockchain_interface_instance
7+
8+
from jmclient import jm_single, load_test_config
9+
from jmclient.configure import (
10+
get_blockchain_interface_instance,
11+
override_from_environment,
12+
)
613

714
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
815

@@ -35,3 +42,33 @@ def test_blockchain_sources():
3542
else:
3643
get_blockchain_interface_instance(jm_single().config)
3744
load_test_config()
45+
46+
47+
def test_override_from_environment():
48+
config = ConfigParser()
49+
config.add_section("BLOCKCHAIN")
50+
config.add_section("POLICY")
51+
52+
overrides = {
53+
"JM_BLOCKCHAIN_NETWORK": "testnet",
54+
"JM_POLICY_TX_FEES": "1000",
55+
"JM_MESSAGING_ONION_TYPE": "onion",
56+
"JM_DAEMON_DAEMON_PORT": "27184",
57+
}
58+
for key, value in overrides.items():
59+
os.environ[key] = value
60+
61+
try:
62+
# Override config from environment
63+
override_from_environment(config)
64+
65+
# Verify overrides were applied
66+
assert config.get("BLOCKCHAIN", "network") == "testnet"
67+
assert config.get("POLICY", "tx_fees") == "1000"
68+
assert config.get("MESSAGING:onion", "type") == "onion"
69+
assert config.get("DAEMON", "daemon_port") == "27184"
70+
finally:
71+
# Clean up environment variables
72+
for key in overrides.keys():
73+
if key in os.environ:
74+
del os.environ[key]

0 commit comments

Comments
 (0)