From 4ec92cb9e7d1eb2f90d36e5255ee8814ca0a8513 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 22 Oct 2014 11:03:48 -0500 Subject: Options: ensure macros are always fixed up This fixes several cases in which macros would not be properly processed: options that are not added to the parser yet when early options are parsed; and config file options whose default value is used. --- src/lib/Bcfg2/Options/Options.py | 59 +++++++++++++++++++++--------- src/lib/Bcfg2/Options/Parser.py | 77 ++++++++++++++++++++++++++++------------ 2 files changed, 96 insertions(+), 40 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Options/Options.py b/src/lib/Bcfg2/Options/Options.py index bd1a72fc7..36148c279 100644 --- a/src/lib/Bcfg2/Options/Options.py +++ b/src/lib/Bcfg2/Options/Options.py @@ -17,7 +17,7 @@ from Bcfg2.Compat import ConfigParser __all__ = ["Option", "BooleanOption", "PathOption", "PositionalArgument", "_debug"] -unit_test = False +unit_test = False # pylint: disable=C0103 def _debug(msg): @@ -46,7 +46,7 @@ def _get_action_class(action_name): on. So we just instantiate a dummy parser, add a dummy argument, and determine the class that way. """ if (isinstance(action_name, type) and - issubclass(action_name, argparse.Action)): + issubclass(action_name, argparse.Action)): return action_name if action_name not in _action_map: action = argparse.ArgumentParser().add_argument(action_name, @@ -159,9 +159,10 @@ class Option(object): sources.append("%s.%s" % self.cf) if self.env: sources.append("$" + self.env) - spec = ["sources=%s" % sources, "default=%s" % self.default] - spec.append("%d parsers" % (len(self.parsers))) - return 'Option(%s: %s)' % (self.dest, ", ".join(spec)) + spec = ["sources=%s" % sources, "default=%s" % self.default, + "%d parsers" % len(self.parsers)] + return '%s(%s: %s)' % (self.__class__.__name__, + self.dest, ", ".join(spec)) def list_options(self): """ List options contained in this option. This exists to @@ -228,7 +229,7 @@ class Option(object): rv = self._type_func(self.get_config_value(cfp)) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): rv = None - _debug("Setting %s from config file(s): %s" % (self, rv)) + _debug("Getting value of %s from config file(s): %s" % (self, rv)) return rv def get_config_value(self, cfp): @@ -326,12 +327,11 @@ class Option(object): class PathOption(Option): - """ Shortcut for options that expect a path argument. Uses - :meth:`Bcfg2.Options.Types.path` to transform the argument into a - canonical path. + """Shortcut for options that expect a path argument. - The type of a path option can also be overridden to return a - file-like object. For example: + Uses :meth:`Bcfg2.Options.Types.path` to transform the argument + into a canonical path. The type of a path option can also be + overridden to return a file-like object. For example: .. code-block:: python @@ -339,25 +339,50 @@ class PathOption(Option): Bcfg2.Options.PathOption( "--input", type=argparse.FileType('r'), help="The input file")] + + PathOptions also do translation of ```` macros on the + fly. It's done this way instead of just fixing up all values at + the end of parsing because macro expansion needs to be done before + path canonicalization and other stuff. """ + repository = None def __init__(self, *args, **kwargs): self._original_type = kwargs.pop('type', lambda x: x) kwargs['type'] = self._type kwargs.setdefault('metavar', '') Option.__init__(self, *args, **kwargs) - self.repository = None def early_parsing_hook(self, early_opts): - self.repository = early_opts.repository + if hasattr(early_opts, "repository"): + if self.__class__.repository is None: + _debug("Setting repository to %s for PathOptions" % + early_opts.repository) + self.__class__.repository = early_opts.repository + else: + _debug("Repository is already set for %s" % self.__class__) + + def _get_default(self): + """ Getter for the ``default`` property """ + if self.__class__.repository is None or self._default is None: + return self._default + else: + return self._default.replace("", + self.__class__.repository) + + default = property(_get_default, Option._set_default) def _type(self, value): """Type function that fixes up macros.""" - if self.repository is None: - rv = value + if self.__class__.repository is None: + _debug("Cannot fix up macros yet for %s" % self) + return value else: - rv = value.replace("", self.repository) - return self._original_type(Types.path(rv)) + rv = self._original_type(Types.path( + value.replace("", self.__class__.repository))) + _debug("Fixing up macros in %s: %s -> %s" % + (self, value, rv)) + return rv class _BooleanOptionAction(argparse.Action): diff --git a/src/lib/Bcfg2/Options/Parser.py b/src/lib/Bcfg2/Options/Parser.py index ec470f8d3..6715d90f9 100644 --- a/src/lib/Bcfg2/Options/Parser.py +++ b/src/lib/Bcfg2/Options/Parser.py @@ -168,6 +168,8 @@ class Parser(argparse.ArgumentParser): (opt, value)) action(self, self.namespace, value) else: + _debug("Setting config file-only option %s to %s" % + (opt, value)) setattr(self.namespace, opt.dest, value) def _finalize(self): @@ -191,6 +193,53 @@ class Parser(argparse.ArgumentParser): _debug("Deleting %s" % attr) delattr(self.namespace, attr) + def _parse_early_options(self): + """Parse early options. + + Early options are options that need to be parsed before other + options for some reason. These fall into two basic cases: + + 1. Database options, which need to be parsed so that Django + modules can be imported, since Django configuration is all + done at import-time; + 2. The repository (``-Q``) option, so that ```` + macros in other options can be resolved. + """ + _debug("Option parsing phase 2: Parse early options") + early_opts = argparse.Namespace() + early_parser = Parser(add_help=False, namespace=early_opts, + early=True) + + # add the repo option so we can resolve + # macros + early_parser.add_options([repository]) + + early_components = [] + for component in self.components: + if getattr(component, "parse_first", False): + early_components.append(component) + early_parser.add_component(component) + early_parser.parse(self.argv) + + _debug("Fixing up macros in early options") + for attr_name in dir(early_opts): + if not attr_name.startswith("_"): + attr = getattr(early_opts, attr_name) + if hasattr(attr, "replace"): + setattr(early_opts, attr_name, + attr.replace("", + early_opts.repository)) + + _debug("Early parsing complete, calling hooks") + for component in early_components: + if hasattr(component, "component_parsed_hook"): + _debug("Calling component_parsed_hook on %s" % component) + getattr(component, "component_parsed_hook")(early_opts) + _debug("Calling early parsing hooks; early options: %s" % + early_opts) + for option in self.option_list: + option.early_parsing_hook(early_opts) + def add_config_file(self, dest, cfile, reparse=True): """ Add a config file, which triggers a full reparse of all options. """ @@ -207,10 +256,11 @@ class Parser(argparse.ArgumentParser): def reparse(self, argv=None): """ Reparse options after they have already been parsed. - :param argv: The argument list to parse. By default, + :param argv: The argument list to parse. By default, :attr:`Bcfg2.Options.Parser.argv` is reused. (I.e., the argument list that was initially - parsed.) :type argv: list + parsed.) + :type argv: list """ _debug("Reparsing all options") self._reset_namespace() @@ -249,26 +299,7 @@ class Parser(argparse.ArgumentParser): # phase 2: re-parse command line for early options; currently, # that's database options if not self._early: - _debug("Option parsing phase 2: Parse early options") - early_opts = argparse.Namespace() - early_parser = Parser(add_help=False, namespace=early_opts, - early=True) - # add the repo option so we can resolve - # macros - early_parser.add_options([repository]) - early_components = [] - for component in self.components: - if getattr(component, "parse_first", False): - early_components.append(component) - early_parser.add_component(component) - early_parser.parse(self.argv) - _debug("Early parsing complete, calling hooks") - for component in early_components: - if hasattr(component, "component_parsed_hook"): - _debug("Calling component_parsed_hook on %s" % component) - getattr(component, "component_parsed_hook")(early_opts) - for option in self.option_list: - option.early_parsing_hook(early_opts) + self._parse_early_options() else: _debug("Skipping parsing phase 2 in early mode") @@ -299,13 +330,13 @@ class Parser(argparse.ArgumentParser): remaining = [] while not self.parsed: self.parsed = True - self._set_defaults_from_config() _debug("Parsing known arguments") try: _, remaining = self.parse_known_args(args=self.argv, namespace=self.namespace) except OptionParserException: self.error(sys.exc_info()[1]) + self._set_defaults_from_config() self._parse_config_options() self._finalize() if len(remaining) and not self._early: -- cgit v1.2.3-1-g7c22