Easier and more self documenting patterns for loading/saving Persistent
[python_utils.git] / config.py
index a83433c68733178b634dad288cc4c14428a6b81b..4d885149529901aedfa884ab602d3e1c3c096f17 100644 (file)
--- a/config.py
+++ b/config.py
@@ -90,11 +90,6 @@ import re
 import sys
 from typing import Any, Dict, List, Optional, Tuple
 
-from kazoo.client import KazooClient
-from kazoo.protocol.states import WatchedEvent
-
-import scott_secrets
-
 # This module is commonly used by others in here and should avoid
 # taking any unnecessary dependencies back on them.
 
@@ -188,6 +183,12 @@ GROUP.add_argument(
     action='store_true',
     help='If present, config will raise an exception if it doesn\'t recognize an argument.  The default behavior is to ignore unknown arguments so as to allow interoperability with programs that want to use their own argparse calls to parse their own, separate commandline args.',
 )
+GROUP.add_argument(
+    '--config_exit_after_parse',
+    default=False,
+    action='store_true',
+    help='If present, halt the program after parsing config.  Useful, for example, to write a --config_savefile and then terminate.',
+)
 
 
 class Config:
@@ -219,7 +220,7 @@ class Config:
         # A zookeeper client that is lazily created so as to not incur
         # the latency of connecting to zookeeper for programs that are
         # not reading or writing their config data into zookeeper.
-        self.zk: Optional[KazooClient] = None
+        self.zk: Optional[Any] = None
 
         # Per known zk file, what is the max version we have seen?
         self.max_version: Dict[str, int] = {}
@@ -234,6 +235,9 @@ class Config:
     def __contains__(self, key: str) -> bool:
         return key in self.config
 
+    def get(self, key: str, default: Any = None) -> Optional[Any]:
+        return self.config.get(key, default)
+
     @staticmethod
     def add_commandline_args(title: str, description: str = "") -> argparse._ArgumentGroup:
         """Create a new context for arguments and return a handle.
@@ -324,6 +328,46 @@ class Config:
             env = env[1:]
         return var, env, chunks
 
+    @staticmethod
+    def _to_bool(in_str: str) -> bool:
+        """
+        Args:
+            in_str: the string to convert to boolean
+
+        Returns:
+            A boolean equivalent of the original string based on its contents.
+            All conversion is case insensitive.  A positive boolean (True) is
+            returned if the string value is any of the following:
+
+            * "true"
+            * "t"
+            * "1"
+            * "yes"
+            * "y"
+            * "on"
+
+            Otherwise False is returned.
+
+        >>> to_bool('True')
+        True
+
+        >>> to_bool('1')
+        True
+
+        >>> to_bool('yes')
+        True
+
+        >>> to_bool('no')
+        False
+
+        >>> to_bool('huh?')
+        False
+
+        >>> to_bool('on')
+        True
+        """
+        return in_str.lower() in ("true", "1", "yes", "y", "t", "on")
+
     def _augment_sys_argv_from_environment_variables(self):
         """Internal.  Look at the system environment for variables that match
         commandline arg names.  This is done via some munging such that:
@@ -360,16 +404,14 @@ class Config:
                                 self.saved_messages.append(
                                     f'Initialized from environment: {var} = {value}'
                                 )
-                                from string_utils import to_bool
-
-                                if len(chunks) == 1 and to_bool(value):
+                                if len(chunks) == 1 and Config._to_bool(value):
                                     sys.argv.append(var)
                                 elif len(chunks) > 1:
                                     sys.argv.append(var)
                                     sys.argv.append(value)
                     arg = ''
 
-    def _process_dynamic_args(self, event: WatchedEvent):
+    def _process_dynamic_args(self, event):
         assert self.zk
         logger = logging.getLogger(__name__)
         contents, meta = self.zk.get(event.path, watch=self._process_dynamic_args)
@@ -413,8 +455,13 @@ class Config:
         if loadfile is not None:
             zkpath = None
             if loadfile[:3] == 'zk:':
+                from kazoo.client import KazooClient
+
+                import scott_secrets
+
                 try:
                     if self.zk is None:
+
                         self.zk = KazooClient(
                             hosts=scott_secrets.ZOOKEEPER_NODES,
                             use_ssl=True,
@@ -535,6 +582,10 @@ class Config:
                     zkpath = re.sub(r'//+', '/', zkpath)
                 try:
                     if not self.zk:
+                        from kazoo.client import KazooClient
+
+                        import scott_secrets
+
                         self.zk = KazooClient(
                             hosts=scott_secrets.ZOOKEEPER_NODES,
                             use_ssl=True,
@@ -571,6 +622,11 @@ class Config:
             self.dump_config()
 
         self.config_parse_called = True
+        if config['config_exit_after_parse']:
+            print("Exiting because of --config_exit_after_parse.")
+            if self.zk:
+                self.zk.stop()
+            sys.exit(0)
         return self.config
 
     def has_been_parsed(self) -> bool: