# HG changeset patch # User shelve@localhost # Date 1789435821 14400 # Mon Sep 14 21:30:21 2026 -0400 # Node ID 9607627b35c91558fd27676ad9beb13ce96d06a7 # Parent ed1e6e000097f452c311dba461509df69816f08a set config.ini vars from environment. Not sure it's a good idea for production, but for testing can be useful. Needs admin_guide.txt and upgrading.txt docs if merged diff --git a/CHANGES.txt b/CHANGES.txt --- a/CHANGES.txt +++ b/CHANGES.txt @@ -76,6 +76,8 @@ - added 'checkconfig' command to roundup-admin to find settings in the main config.ini that are typed incorrectly or in the wrong section. (John Rouillard) +- add support for setting config.ini settings from environment + variables. (John Rouillard) 2026-07-13 2.6.0 diff --git a/roundup/configuration.py b/roundup/configuration.py --- a/roundup/configuration.py +++ b/roundup/configuration.py @@ -18,6 +18,7 @@ import sys import time import traceback +from collections import defaultdict from textwrap import dedent, fill, wrap import roundup.date @@ -2198,6 +2199,48 @@ """Return the service name for config file heading""" return "" + def get_config_settings_from_env(self, cfg_source=None): + """Get settings from environment if we are called for main config + + Environment vars look like: + + ROUNDUP_section_var_name=value + + and define: + + [section] + var_name = value + + Note that ROUNDUP_MAIN_ is required for the variables + in the MAIN section even though the MAIN prefix is not + used when reading/accessing them. + + Env vars can also be used for setting detector and + extension ini files. The format is: + + ROUNDUP_DETECTOR_section_var_name + + ROUNDUP_EXTENSION_section_var_name + """ + + settings = defaultdict(dict) + + if cfg_source: + # format is: /some/path/tracker_home/config.ini or + # /some/path/tracker_home/{detectors,extensions}/config.ini + ini_file_source = cfg_source.split("/")[-2].upper() + "_" + if ini_file_source not in ("DETECTORS_", "EXTENSIONS_"): + ini_file_source = "" # default is main config. + + prefix = "ROUNDUP_" + ini_file_source + for evar, eval in os.environ.items(): + if not evar.startswith(prefix): + continue + section, var = evar[len(prefix):].lower().split('_', 1) + settings[section][var] = eval + + return settings + # file operations def load_ini(self, config_path, defaults=None): @@ -2220,13 +2263,25 @@ config_path = os.path.join(config_path, self.INI_FILE) else: home_dir = os.path.dirname(config_path) - # parse the file + + # merge defaults config_defaults = {"HOME": home_dir} if defaults: config_defaults.update(defaults) + + # parse values from environment + env_config_settings: dict(srt, dict(srt,str)) = \ + self.get_config_settings_from_env(config_path) + + # parse the file config = configparser.ConfigParser(config_defaults) config.read([config_path]) # .ini file loaded ok. + + # override settings with env vars for main config + if env_config_settings: + config.read_dict(env_config_settings, "env") + self.HOME = home_dir self.filepath = config_path self._adjust_options(config) @@ -2244,7 +2299,7 @@ for option in self.items(): option.load_ini(config, validate_keys=self.validate_keys) - # As CoreConfig lods the SETTINGS from config.ini, the config + # As CoreConfig loads the SETTINGS from config.ini, the config # file version of the setting is removed. Report any options # left in the parsed config file. They are possible typos. # config_defaults and self.ini_DEFAULTS are added to every diff --git a/test/test_config.py b/test/test_config.py --- a/test/test_config.py +++ b/test/test_config.py @@ -988,6 +988,53 @@ self.assertIn("Unable to load redis module", cm.exception.__str__()) + def testLoadConfigSetFromEnv(self): + """Test setting main, detectors and extensions configs from env vars""" + + # set one env var for each config file. + # Use different values so we can verify the right variable was + # used. MAIN uses a value that raises an exception to check + # exception code in configuration.py. + os.environ['ROUNDUP_MAIN_INSTANT_REGISTRATION'] = "foo" + os.environ['ROUNDUP_DETECTORS_INSTANT_REGISTRATION'] = "True" + os.environ['ROUNDUP_EXTENSIONS_INSTANT_REGISTRATION'] = "false" + + # check environment setting of main config values. It has an + # invalid value for the config type. So it will raise + # exception. + with self.assertRaises(configuration.OptionValueError) as cm: + configuration.CoreConfig().load(self.dirname) + self.assertEqual(cm.exception.args[0].name, "INSTANT_REGISTRATION") + # Note for section main, section is not part of name. It's just KEY. + # For other sections the name is SECTION_KEY + self.assertEqual(cm.exception.args[0].section, "main") + self.assertEqual(cm.exception.args[1], "foo") + + # Test setting detector config. UserConfig's don't have a schema. + # so it can't raise an exception. Check that name/sections are + # properly defined. + cfg = configuration.UserConfig( + os.path.join(self.dirname, "detectors")) + # not section is part of name. KEY = REGISTRATION + setting = cfg.items()[1] # depends on content in detectors/config.ini + self.assertEqual(setting.name, "INSTANT_REGISTRATION") + self.assertEqual(setting.section, "instant") + self.assertEqual(setting._value, "True") + + # Test setting extensions config. + cfg = configuration.UserConfig( + os.path.join(self.dirname, "extensions")) + setting = cfg.items()[0] # depends on extensions/config.ini not existing + # not section is part of name. KEY = REGISTRATION + self.assertEqual(setting.name, "INSTANT_REGISTRATION") + self.assertEqual(setting.section, "instant") + self.assertEqual(setting._value, "false") + + # cleanup + del(os.environ['ROUNDUP_MAIN_INSTANT_REGISTRATION']) + del(os.environ['ROUNDUP_DETECTORS_INSTANT_REGISTRATION']) + del(os.environ['ROUNDUP_EXTENSIONS_INSTANT_REGISTRATION']) + def testLoadConfig(self): """ run load to validate config """