From bd8e639ad56422893e67c74a3b8dae3f27f92276 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 27 Jun 2013 10:25:03 -0400 Subject: Options: wrote completely new option parser --- src/lib/Bcfg2/Options/Actions.py | 164 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/lib/Bcfg2/Options/Actions.py (limited to 'src/lib/Bcfg2/Options/Actions.py') diff --git a/src/lib/Bcfg2/Options/Actions.py b/src/lib/Bcfg2/Options/Actions.py new file mode 100644 index 000000000..cb83f3ae7 --- /dev/null +++ b/src/lib/Bcfg2/Options/Actions.py @@ -0,0 +1,164 @@ +""" Custom argparse actions """ + +import sys +import argparse +from Parser import get_parser + +__all__ = ["ConfigFileAction", "ComponentAction", "PluginsAction"] + + +class ComponentAction(argparse.Action): + """ ComponentAction automatically imports classes and modules + based on the value of the option, and automatically collects + options from the loaded classes and modules. It cannot be used by + itself, but must be subclassed, with either :attr:`mapping` or + :attr:`bases` overridden. See + :class:`Bcfg2.Options.PluginsAction` for an example. + + ComponentActions expect to be given a list of class names. If + :attr:`bases` is overridden, then it will attempt to import those + classes from identically named modules within the given bases. + For instance: + + .. code-block:: python + + class FooComponentAction(Bcfg2.Options.ComponentAction): + bases = ["Bcfg2.Server.Foo"] + + + class FooLoader(object): + options = [ + Bcfg2.Options.Option( + "--foo", + type=Bcfg2.Options.Types.comma_list, + default=["One"], + action=FooComponentAction)] + + If "--foo One,Two,Three" were given on the command line, then + ``FooComponentAction`` would attempt to import + ``Bcfg2.Server.Foo.One.One``, ``Bcfg2.Server.Foo.Two.Two``, and + ``Bcfg2.Server.Foo.Three.Three``. (It would also call + :func:`Bcfg2.Options.Parser.add_component` with each of those + classes as arguments.) + + Note that, although ComponentActions expect lists of components + (by default; this can be overridden by setting :attr:`islist`), + you must still explicitly specify a ``type`` argument to the + :class:`Bcfg2.Options.Option` constructor to split the value into + a list. + + Note also that, unlike other actions, the default value of a + ComponentAction option does not need to be the actual literal + final value. (I.e., you don't have to import + ``Bcfg2.Server.Foo.One.One`` and set it as the default in the + example above; the string "One" suffices.) + """ + + #: A list of parent modules where modules or classes should be + #: imported from. + bases = [] + + #: A mapping of `` => `` that components will be + #: loaded from. This can be used to permit much more complex + #: behavior than just a list of :attr:`bases`. + mapping = dict() + + #: If ``module`` is True, then only the module will be loaded, not + #: a class from the module. For instance, in the example above, + #: ``FooComponentAction`` would attempt instead to import + #: ``Bcfg2.Server.Foo.One``, ``Bcfg2.Server.Foo.Two``, and + #: ``Bcfg2.Server.Foo.Three``. + module = False + + #: By default, ComponentActions expect a list of components to + #: load. If ``islist`` is False, then it will only expect a + #: single component. + islist = True + + #: If ``fail_silently`` is True, then failures to import modules + #: or classes will not be logged. This is useful when the default + #: is to import everything, some of which are expected to fail. + fail_silently = False + + def __init__(self, *args, **kwargs): + if self.mapping: + if 'choices' not in kwargs: + kwargs['choices'] = self.mapping.keys() + self._final = False + argparse.Action.__init__(self, *args, **kwargs) + + def _import(self, module, name): + try: + return getattr(__import__(module, fromlist=[name]), name) + except (AttributeError, ImportError): + if not self.fail_silently: + print("Failed to load %s from %s: %s" % + (name, module, sys.exc_info()[1])) + return None + + def _load_component(self, name): + """ Import a single class or module, adding it as a component to + the parser. + + :param name: The name of the class or module to import, without + the base prepended. + :type name: string + :returns: the imported class or module + """ + cls = None + if self.mapping and name in self.mapping: + cls = self.mapping[name] + elif "." in name: + cls = self._import(*name.rsplit(".", 1)) + else: + for base in self.bases: + if self.module: + mod = base + else: + mod = "%s.%s" % (base, name) + cls = self._import(mod, name) + if cls is not None: + break + if cls: + get_parser().add_component(cls) + else: + print("Could not load component %s" % name) + return cls + + def finalize(self, parser, namespace): + """ Finalize a default value by loading the components given + in it. This lets a default be specified with a list of + strings instead of a list of classes. """ + if not self._final: + self.__call__(parser, namespace, self.default) + + def __call__(self, parser, namespace, values, option_string=None): + if values is None: + result = None + else: + if self.islist: + result = [] + for val in values: + cls = self._load_component(val) + if cls is not None: + result.append(cls) + else: + result = self._load_component(values) + self._final = True + setattr(namespace, self.dest, values) + + +class ConfigFileAction(argparse.Action): + """ ConfigFileAction automatically loads and parses a + supplementary config file (e.g., ``bcfg2-web.conf`` or + ``bcfg2-lint.conf``). """ + + def __call__(self, parser, namespace, values, option_string=None): + get_parser().add_config_file(self.dest, values) + setattr(namespace, self.dest, values) + + +class PluginsAction(ComponentAction): + """ :class:`Bcfg2.Options.ComponentAction` subclass for loading + Bcfg2 server plugins. """ + bases = ['Bcfg2.Server.Plugins'] -- cgit v1.2.3-1-g7c22