More writing examples and improving documentation.
[pyutils.git] / src / pyutils / config.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2022, Scott Gasch
4
5 """Global program configuration driven by commandline arguments and,
6 optionally, from saved (local or Zookeeper) configuration files... with
7 optional support for dynamic arguments (i.e. that can change during runtime).
8
9 Let's start with an example of how to use :py:mod:`pyutils.config`.  It's
10 pretty easy for normal commandline arguments because it wraps :py:mod:`argparse`
11 (see https://docs.python.org/3/library/argparse.html):
12
13     In your file.py::
14
15         from pyutils import config
16
17         # Call add_commandline_args to get an argparse.ArgumentParser
18         # for file.py.  Each file uses a separate ArgumentParser
19         # chained off the main namespace.
20         parser = config.add_commandline_args(
21             "Module",
22             "Args related to module doing the thing.",
23         )
24
25         # Then simply add argparse-style arguments to it, as usual.
26         parser.add_argument(
27             "--module_do_the_thing",
28             type=bool,
29             default=True,
30             help="Should the module do the thing?"
31         )
32
33     In your main.py::
34
35         from pyutils import config
36
37         # main.py may have some arguments of its own, so add them.
38         parser = config.add_commandline_args(
39             "Main",
40             "A program that does the thing.",
41         )
42         parser.add_argument(
43             "--dry_run",
44             type=bool,
45             default=False,
46             help="Should we really do the thing?"
47         )
48
49         def main() -> None:
50             config.parse()   # Then remember to call config.parse() early on.
51
52     If you set this up and remember to invoke :py:meth:`pyutils.config.parse`,
53     all commandline arguments will play nicely together across all modules / files
54     in your program automatically.  Argparse help messages will group flags by
55     the file they affect.
56
57     If you use :py:meth:`pyutils.bootstrap.initialize`, a decorator that can
58     optionally wrap your program's entry point, it will remember to call
59     :py:meth:`pyutils.config.parse` for you so you can omit the last part.
60     That looks like this::
61
62         from pyutils import bootstrap
63
64         @bootstrap.initialize
65         def main():
66             whatever
67
68         if __name__ == '__main__':
69             main()
70
71     Either way, you'll get an aggregated usage message along with flags broken
72     down per file in help::
73
74         % main.py -h
75         usage: main.py [-h]
76                        [--module_do_the_thing MODULE_DO_THE_THING]
77                        [--dry_run DRY_RUN]
78
79         Module:
80           Args related to module doing the thing.
81
82           --module_do_the_thing MODULE_DO_THE_THING
83                        Should the module do the thing?
84
85         Main:
86           A program that does the thing
87
88           --dry_run
89                        Should we really do the thing?
90
91     Once :py:meth:`pyutils.config.parse` has been called (either automatically
92     by :py:mod:`puytils.bootstrap` or manually, the program configuration
93     state is ready in a dict-like object called `config.config`.  For example,
94     to check the state of the `--dry_run` flag::
95
96         if not config.config['dry_run']:
97             module.do_the_thing()
98
99     Using :py:mod:`pyutils.config` allows you to "save" and "load" whole
100     sets of commandline arguments using the `--config_savefile` and the
101     `--config_loadfile` arguments.  The former saves all arguments (other than
102     itself) to an ascii file whose path you provide.  The latter reads all
103     arguments from an ascii file whose path you provide.
104
105     Saving and loading sets of arguments can make complex operations easier
106     to set up.  They also allows for dynamic arguments.
107
108     If you use Apache Zookeeper, you can prefix paths to
109     `--config_savefile` and `--config_loadfile` with the string "zk:"
110     to cause the path to be interpreted as a Zookeeper path rather
111     than one on the local filesystem.  When loading arguments from
112     Zookeeker, the :py:mod:`pyutils.config` code registers a listener
113     to be notified on state change (e.g. when some other instance
114     overwrites your Zookeeper based configuration).  Listeners then
115     dynamically update the value of any flag in the `config.config`
116     dict whose name contains the string "dynamic".  So, for example,
117     the `--dynamic_database_connect_string` argument would be
118     modifiable at runtime when using Zookeeper based configurations.
119     Flags that do not contain the string "dynamic" will not change.
120     And nothing is dynamic unless we're reading configuration from
121     Zookeeper.
122
123     For more information about Zookeeper, see https://zookeeper.apache.org/.
124 """
125
126 import argparse
127 import logging
128 import os
129 import pprint
130 import re
131 import sys
132 from typing import Any, Dict, List, Optional
133
134 # This module is commonly used by others in here and should avoid
135 # taking any unnecessary dependencies back on them.
136
137 # Make a copy of the original program arguments immediately upon module load.
138 PROGRAM_NAME: str = os.path.basename(sys.argv[0])
139 ORIG_ARGV: List[str] = sys.argv.copy()
140
141
142 class OptionalRawFormatter(argparse.HelpFormatter):
143     """This formatter has the same bahavior as the normal argparse
144     text formatter except when the help text of an argument begins
145     with "RAW|".  In that case, the line breaks are preserved and the
146     text is not wrapped.  It is enabled automatically if you use
147     :py:mod:`pyutils.config`.
148
149     Use this by prepending "RAW|" in your help message to disable
150     word wrapping and indicate that the help message is already
151     formatted and should be preserved.  Here's an example usage::
152
153         args.add_argument(
154             '--mode',
155             type=str,
156             default='PLAY',
157             choices=['CHEAT', 'AUTOPLAY', 'SELFTEST', 'PRECOMPUTE', 'PLAY'],
158             metavar='MODE',
159             help='''RAW|Our mode of operation.  One of:
160
161                 PLAY = play wordle with me!  Pick a random solution or
162                        specify a solution with --template.
163
164                CHEAT = given a --template and, optionally, --letters_in_word
165                        and/or --letters_to_avoid, return the best guess word;
166
167             AUTOPLAY = given a complete word in --template, guess it step
168                        by step showing work;
169
170             SELFTEST = autoplay every possible solution keeping track of
171                        wins/losses and average number of guesses;
172
173           PRECOMPUTE = populate hash table with optimal guesses.
174             ''',
175         )
176
177     """
178
179     def _split_lines(self, text, width):
180         if text.startswith('RAW|'):
181             return text[4:].splitlines()
182         return argparse.HelpFormatter._split_lines(self, text, width)
183
184
185 # A global argparser that we will collect arguments in.  Each module (including
186 # us) will add arguments to a separate argument group.
187 ARGS = argparse.ArgumentParser(
188     description=None,
189     formatter_class=OptionalRawFormatter,
190     fromfile_prefix_chars="@",
191     epilog=f'{PROGRAM_NAME} uses config.py ({__file__}) for global, cross-module configuration setup and parsing.',
192     # I don't fully understand why but when loaded by sphinx sometimes
193     # the same module is loaded many times causing any arguments it
194     # registers via module-level code to be redefined.  Work around
195     # this iff the program is 'sphinx-build'
196     conflict_handler='resolve' if PROGRAM_NAME == 'sphinx-build' else 'error',
197 )
198
199 # Arguments specific to config.py.  Other users should get their own group by
200 # invoking config.add_commandline_args.
201 GROUP = ARGS.add_argument_group(
202     f'Global Config ({__file__})',
203     'Args that control the global config itself; how meta!',
204 )
205 GROUP.add_argument(
206     '--config_loadfile',
207     metavar='FILENAME',
208     default=None,
209     help='Config file (populated via --config_savefile) from which to read args in lieu or in addition to those passed via the commandline.  Note that if the given path begins with "zk:" then it is interpreted as a zookeeper path instead of as a filesystem path.  When loading config from zookeeper, any argument with the string "dynamic" in the name (e.g. --module_dynamic_url) may be modified at runtime by changes made to zookeeper (using --config_savefile=zk:path).  You should therefore either write your code to handle dynamic argument changes or avoid naming arguments "dynamic" if you use zookeeper configuration paths.',
210 )
211 GROUP.add_argument(
212     '--config_dump',
213     default=False,
214     action='store_true',
215     help='Display the global configuration (possibly derived from multiple sources) on STDERR at program startup time.',
216 )
217 GROUP.add_argument(
218     '--config_savefile',
219     type=str,
220     metavar='FILENAME',
221     default=None,
222     help='Populate a config file (compatible with --config_loadfile) and write it at the given path for later [re]use.  If the given path begins with "zk:" it is interpreted as a zookeeper path instead of a local filesystem path.  When updating zookeeper-based configs, all running programs that read their configuration from zookeeper (via --config_loadfile=zk:<path>) will see the update.  Those that also enabled --config_allow_dynamic_updates will change the value of any flags with the string "dynamic" in their names (e.g. --my_dynamic_flag or --dynamic_database_connect_string).',
223 )
224 GROUP.add_argument(
225     '--config_allow_dynamic_updates',
226     default=False,
227     help='If enabled, allow config flags with the string "dynamic" in their names to change at runtime when a new Zookeeper based configuration is created.  See the --config_savefile help message for more information about this option.',
228 )
229 GROUP.add_argument(
230     '--config_rejects_unrecognized_arguments',
231     default=False,
232     action='store_true',
233     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.',
234 )
235 GROUP.add_argument(
236     '--config_exit_after_parse',
237     default=False,
238     action='store_true',
239     help='If present, halt the program after parsing config.  Useful, for example, to write a --config_savefile and then terminate.',
240 )
241
242
243 class Config:
244     """
245     .. warning::
246
247         Do not instantiate this class directly; it is meant to be a
248         global singleton called `pyutils.config.CONFIG`.  Instead, use
249         :py:meth:`pyutils.config.add_commandline_args` to get an
250         `ArgumentGroup` and add your arguments to it.  Then call
251         :py:meth:`pyutils.config.parse` to parse global configuration
252         from your main program entry point.
253
254     Everything in the config module used to be module-level functions and
255     variables but it made the code ugly and harder to maintain.  Now, this
256     class does the heavy lifting.  We still rely on some globals, though:
257
258         - ARGS and GROUP to interface with argparse
259         - PROGRAM_NAME stores argv[0] close to program invocation
260         - ORIG_ARGV stores the original argv list close to program invocation
261         - CONFIG and config: hold the (singleton) instance of this class.
262     """
263
264     def __init__(self):
265         # Has our parse() method been invoked yet?
266         self.config_parse_called = False
267
268         # A configuration dictionary that will contain parsed
269         # arguments.  This is the data that is most interesting to our
270         # callers as it will hold the configuration result.
271         self.config: Dict[str, Any] = {}
272
273         # Defer logging messages until later when logging has been
274         # initialized.
275         self.saved_messages: List[str] = []
276
277         # A zookeeper client that is lazily created so as to not incur
278         # the latency of connecting to zookeeper for programs that are
279         # not reading or writing their config data into zookeeper.
280         self.zk: Optional[Any] = None
281
282         # Per known zk file, what is the max version we have seen?
283         self.max_version: Dict[str, int] = {}
284
285     def __getitem__(self, key: str) -> Optional[Any]:
286         """If someone uses []'s on us, pass it onto self.config."""
287         return self.config.get(key, None)
288
289     def __setitem__(self, key: str, value: Any) -> None:
290         self.config[key] = value
291
292     def __contains__(self, key: str) -> bool:
293         return key in self.config
294
295     def get(self, key: str, default: Any = None) -> Optional[Any]:
296         return self.config.get(key, default)
297
298     @staticmethod
299     def add_commandline_args(
300         title: str, description: str = ""
301     ) -> argparse._ArgumentGroup:
302         """Create a new context for arguments and return an ArgumentGroup
303         to the caller for module-level population.
304
305         Args:
306             title: A title for your module's commandline arguments group.
307             description: A helpful description of your module.
308
309         Returns:
310             An argparse._ArgumentGroup to be populated by the caller.
311         """
312         return ARGS.add_argument_group(title, description)
313
314     @staticmethod
315     def overwrite_argparse_epilog(msg: str) -> None:
316         """Allows your code to override the default epilog created by
317         argparse.
318
319         Args:
320             msg: The epilog message to substitute for the default.
321         """
322         ARGS.epilog = msg
323
324     @staticmethod
325     def is_flag_already_in_argv(var: str) -> bool:
326         """
327         Returns:
328             True if a particular flag is passed on the commandline
329             and False otherwise.
330
331         Args:
332             var: The flag to search for.
333         """
334         for _ in sys.argv:
335             if var in _:
336                 return True
337         return False
338
339     @staticmethod
340     def print_usage() -> None:
341         """Prints the normal help usage message out."""
342         ARGS.print_help()
343
344     @staticmethod
345     def usage() -> str:
346         """
347         Returns:
348             program usage help text as a string.
349         """
350         return ARGS.format_usage()
351
352     @staticmethod
353     def _reorder_arg_action_groups_before_help(entry_module: Optional[str]):
354         """Internal.  Used to reorder the arguments before dumping out a
355         generated help string such that the main program's arguments come
356         last.
357
358         """
359         reordered_action_groups = []
360         for grp in ARGS._action_groups:
361             if entry_module is not None and entry_module in grp.title:  # type: ignore
362                 reordered_action_groups.append(grp)
363             elif PROGRAM_NAME in GROUP.title:  # type: ignore
364                 reordered_action_groups.append(grp)
365             else:
366                 reordered_action_groups.insert(0, grp)
367         return reordered_action_groups
368
369     @staticmethod
370     def _to_bool(in_str: str) -> bool:
371         """
372         Args:
373             in_str: the string to convert to boolean
374
375         Returns:
376             A boolean equivalent of the original string based on its contents.
377             All conversion is case insensitive.  A positive boolean (True) is
378             returned if the string value is any of the following:
379
380             * "true"
381             * "t"
382             * "1"
383             * "yes"
384             * "y"
385             * "on"
386
387             Otherwise False is returned.
388
389         >>> to_bool('True')
390         True
391
392         >>> to_bool('1')
393         True
394
395         >>> to_bool('yes')
396         True
397
398         >>> to_bool('no')
399         False
400
401         >>> to_bool('huh?')
402         False
403
404         >>> to_bool('on')
405         True
406         """
407         return in_str.lower() in ("true", "1", "yes", "y", "t", "on")
408
409     def _process_dynamic_args(self, event):
410         """Invoked as a callback when a zk-based config changed."""
411
412         if not self.zk:
413             return
414         logger = logging.getLogger(__name__)
415         try:
416             contents, meta = self.zk.get(event.path, watch=self._process_dynamic_args)
417             logger.debug('Update for %s at version=%d.', event.path, meta.version)
418             logger.debug(
419                 'Max known version for %s is %d.',
420                 event.path,
421                 self.max_version.get(event.path, 0),
422             )
423         except Exception as e:
424             raise Exception('Error reading data from zookeeper') from e
425
426         # Make sure we process changes in order.
427         if meta.version > self.max_version.get(event.path, 0):
428             self.max_version[event.path] = meta.version
429             contents = contents.decode()
430             temp_argv = []
431             for arg in contents.split():
432
433                 # Our rule is that arguments must contain the word
434                 # 'dynamic' if we are going to allow them to change at
435                 # runtime as a signal that the programmer is expecting
436                 # this.
437                 if 'dynamic' in arg and config.config['config_allow_dynamic_updates']:
438                     temp_argv.append(arg)
439                     logger.info("Updating %s from zookeeper async config change.", arg)
440
441             if len(temp_argv) > 0:
442                 old_argv = sys.argv
443                 sys.argv = temp_argv
444                 known, _ = ARGS.parse_known_args()
445                 sys.argv = old_argv
446                 self.config.update(vars(known))
447
448     def _read_config_from_zookeeper(self, zkpath: str) -> Optional[str]:
449         from pyutils import zookeeper
450
451         if not zkpath.startswith('/config/'):
452             zkpath = '/config/' + zkpath
453             zkpath = re.sub(r'//+', '/', zkpath)
454
455         try:
456             if self.zk is None:
457                 self.zk = zookeeper.get_started_zk_client()
458             if not self.zk.exists(zkpath):
459                 return None
460
461             # Note: we're putting a watch on this config file.  Our
462             # _process_dynamic_args routine will be called to reparse
463             # args when/if they change.
464             contents, meta = self.zk.get(zkpath, watch=self._process_dynamic_args)
465             contents = contents.decode()
466             self.saved_messages.append(
467                 f'Setting {zkpath}\'s max_version to {meta.version}'
468             )
469             self.max_version[zkpath] = meta.version
470             self.saved_messages.append(f'Read config from zookeeper {zkpath}.')
471             return contents
472         except Exception as e:
473             self.saved_messages.append(
474                 f'Failed to read {zkpath} from zookeeper: exception {e}'
475             )
476             return None
477
478     def _read_config_from_disk(self, filepath: str) -> Optional[str]:
479         if not os.path.exists(filepath):
480             return None
481         with open(filepath, 'r') as rf:
482             self.saved_messages.append(f'Read config from disk file {filepath}')
483             return rf.read()
484
485     def _augment_sys_argv_from_loadfile(self):
486         """Internal.  Augment with arguments persisted in a saved file."""
487
488         # Check for --config_loadfile in the args manually; argparse isn't
489         # invoked yet and can't be yet.
490         loadfile = None
491         saw_other_args = False
492         grab_next_arg = False
493         for arg in sys.argv[1:]:
494             if 'config_loadfile' in arg:
495                 pieces = arg.split('=')
496                 if len(pieces) > 1:
497                     loadfile = pieces[1]
498                 else:
499                     grab_next_arg = True
500             elif grab_next_arg:
501                 loadfile = arg
502             else:
503                 saw_other_args = True
504
505         if not loadfile or len(loadfile) == 0:
506             return
507
508         # Get contents from wherever.
509         contents = None
510         if loadfile[:3] == 'zk:':
511             contents = self._read_config_from_zookeeper(loadfile[3:])
512         else:
513             contents = self._read_config_from_disk(loadfile)
514
515         if contents:
516             if saw_other_args:
517                 msg = f'Augmenting commandline arguments with those from {loadfile}.'
518             else:
519                 msg = f'Reading commandline arguments from {loadfile}.'
520             print(msg, file=sys.stderr)
521             self.saved_messages.append(msg)
522         else:
523             msg = f'Failed to read/parse contents from {loadfile}'
524             print(msg, file=sys.stderr)
525             self.saved_messages.append(msg)
526             return
527
528         # Augment args with new ones.
529         newargs = [
530             arg.strip('\n')
531             for arg in contents.split('\n')
532             if 'config_savefile' not in arg
533         ]
534         sys.argv += newargs
535
536     def dump_config(self):
537         """Print the current config to stdout."""
538         print("Global Configuration:", file=sys.stderr)
539         pprint.pprint(self.config, stream=sys.stderr)
540         print()
541
542     def _write_config_to_disk(self, data: str, filepath: str) -> None:
543         with open(filepath, 'w') as wf:
544             wf.write(data)
545
546     def _write_config_to_zookeeper(self, data: str, zkpath: str) -> None:
547         if not zkpath.startswith('/config/'):
548             zkpath = '/config/' + zkpath
549             zkpath = re.sub(r'//+', '/', zkpath)
550         try:
551             if not self.zk:
552                 from pyutils import zookeeper
553
554                 self.zk = zookeeper.get_started_zk_client()
555             encoded_data = data.encode()
556             if len(encoded_data) > 1024 * 1024:
557                 raise Exception(
558                     f'Saved args are too large ({len(encoded_data)} bytes exceeds zk limit)'
559                 )
560             if not self.zk.exists(zkpath):
561                 self.zk.create(zkpath, encoded_data)
562                 self.saved_messages.append(
563                     f'Just created {zkpath}; setting its max_version to 0'
564                 )
565                 self.max_version[zkpath] = 0
566             else:
567                 meta = self.zk.set(zkpath, encoded_data)
568                 self.saved_messages.append(
569                     f'Setting {zkpath}\'s max_version to {meta.version}'
570                 )
571                 self.max_version[zkpath] = meta.version
572         except Exception as e:
573             raise Exception(f'Failed to create zookeeper path {zkpath}') from e
574         self.saved_messages.append(f'Saved config to zookeeper in {zkpath}')
575
576     def parse(self, entry_module: Optional[str]) -> Dict[str, Any]:
577         """Main program should invoke this early in main().  Note that the
578         :py:meth:`pyutils.bootstrap.initialize` wrapper takes care of this automatically.
579         This should only be called once per program invocation.
580
581         Args:
582             entry_module: Optional string to ensure we understand which module
583                 contains the program entry point.  Determined heuristically if not
584                 provided.
585
586         Returns:
587             A dict containing the parsed program configuration.  Note that this can
588                 be safely ignored since it is also saved in `config.config` and may
589                 be used directly using that identifier.
590         """
591         if self.config_parse_called:
592             return self.config
593
594         # If we're about to do the usage message dump, put the main
595         # module's argument group last in the list (if possible) so that
596         # when the user passes -h or --help, it will be visible on the
597         # screen w/o scrolling.  This just makes for a nicer --help screen.
598         for arg in sys.argv:
599             if arg in ('--help', '-h'):
600                 if entry_module is not None:
601                     entry_module = os.path.basename(entry_module)
602                 ARGS._action_groups = Config._reorder_arg_action_groups_before_help(
603                     entry_module
604                 )
605
606         # Look for --config_loadfile argument and, if found, read/parse
607         # Note that this works by jamming values onto sys.argv; kinda ugly.
608         self._augment_sys_argv_from_loadfile()
609
610         # Parse (possibly augmented, possibly completely overwritten)
611         # commandline args with argparse normally and populate config.
612         known, unknown = ARGS.parse_known_args()
613         self.config.update(vars(known))
614
615         # Reconstruct the sys.argv with unrecognized flags for the
616         # benefit of future argument parsers.  For example,
617         # unittest_main in python has some of its own flags.  If we
618         # didn't recognize it, maybe someone else will.  Or, if
619         # --config_rejects_unrecognized_arguments was passed, die
620         # if we have unknown arguments.
621         if len(unknown) > 0:
622             if config['config_rejects_unrecognized_arguments']:
623                 raise Exception(
624                     f'Encountered unrecognized config argument(s) {unknown} with --config_rejects_unrecognized_arguments enabled; halting.'
625                 )
626             self.saved_messages.append(
627                 f'Config encountered unrecognized commandline arguments: {unknown}'
628             )
629         sys.argv = sys.argv[:1] + unknown
630
631         # Check for savefile and populate it if requested.
632         savefile = config['config_savefile']
633         if savefile and len(savefile) > 0:
634             data = '\n'.join(ORIG_ARGV[1:])
635             if savefile[:3] == 'zk:':
636                 self._write_config_to_zookeeper(savefile[3:], data)
637             else:
638                 self._write_config_to_disk(savefile, data)
639
640         # Also dump the config on stderr if requested.
641         if config['config_dump']:
642             self.dump_config()
643
644         # Finally, maybe exit now if the user passed
645         # --config_exit_after_parse indicating they want to just
646         # update a config file and halt.
647         self.config_parse_called = True
648         if config['config_exit_after_parse']:
649             print("Exiting because of --config_exit_after_parse.")
650             if self.zk:
651                 self.zk.stop()
652             sys.exit(0)
653         return self.config
654
655     def has_been_parsed(self) -> bool:
656         """Returns True iff the global config has already been parsed"""
657         return self.config_parse_called
658
659     def late_logging(self):
660         """Log messages saved earlier now that logging has been initialized."""
661         logger = logging.getLogger(__name__)
662         logger.debug('Original commandline was: %s', ORIG_ARGV)
663         for _ in self.saved_messages:
664             logger.debug(_)
665
666
667 # A global singleton instance of the Config class.
668 CONFIG = Config()
669
670 # A lot of client code uses config.config['whatever'] to lookup
671 # configuration so to preserve this we make this, config.config, with
672 # a __getitem__ method on it.
673 config = CONFIG
674
675 # Config didn't use to be a class; it was a mess of module-level
676 # functions and data.  The functions below preserve the old interface
677 # so that existing clients do not need to be changed.  As you can see,
678 # they mostly just thunk into the config class.
679
680
681 def add_commandline_args(title: str, description: str = "") -> argparse._ArgumentGroup:
682     """Create a new context for arguments and return a handle.  An alias
683     for config.config.add_commandline_args.
684
685     Args:
686         title: A title for your module's commandline arguments group.
687         description: A helpful description of your module.
688
689     Returns:
690         An argparse._ArgumentGroup to be populated by the caller.
691     """
692     return CONFIG.add_commandline_args(title, description)
693
694
695 def parse(entry_module: Optional[str]) -> Dict[str, Any]:
696     """Main program should call this early in main().  Note that the
697     :code:`bootstrap.initialize` wrapper takes care of this automatically.
698     This should only be called once per program invocation.  Subsequent
699     calls do not reparse the configuration settings but rather just
700     return the current state.
701     """
702     return CONFIG.parse(entry_module)
703
704
705 def has_been_parsed() -> bool:
706     """Returns True iff the global config has already been parsed"""
707     return CONFIG.has_been_parsed()
708
709
710 def late_logging() -> None:
711     """Log messages saved earlier now that logging has been initialized."""
712     CONFIG.late_logging()
713
714
715 def dump_config() -> None:
716     """Print the current config to stdout."""
717     CONFIG.dump_config()
718
719
720 def overwrite_argparse_epilog(msg: str) -> None:
721     """Allows your code to override the default epilog created by
722     argparse.
723
724     Args:
725         msg: The epilog message to substitute for the default.
726     """
727     Config.overwrite_argparse_epilog(msg)
728
729
730 def is_flag_already_in_argv(var: str) -> bool:
731     """Returns true if a particular flag is passed on the commandline
732     and false otherwise.
733
734     Args:
735         var: The flag to search for.
736     """
737     return Config.is_flag_already_in_argv(var)
738
739
740 def print_usage() -> None:
741     """Prints the normal help usage message out."""
742     Config.print_usage()
743
744
745 def usage() -> str:
746     """
747     Returns:
748         program usage help text as a string.
749     """
750     return Config.usage()