1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
""" Custom argparse actions """
import sys
import argparse
from Bcfg2.Options.Parser import get_parser
__all__ = ["ConfigFileAction", "ComponentAction", "PluginsAction"]
class FinalizableAction(argparse.Action):
""" A FinalizableAction requires some additional action to be taken
when storing the value, and as a result must be finalized if the
default value is used."""
def __init__(self, *args, **kwargs):
argparse.Action.__init__(self, *args, **kwargs)
self._final = False
def finalize(self, parser, namespace):
""" Finalize a default value by calling the action callable. """
if not self._final:
self.__call__(parser, namespace, getattr(namespace, self.dest,
self.default))
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
self._final = True
class ComponentAction(FinalizableAction):
""" 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 ``<name> => <object>`` 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()
FinalizableAction.__init__(self, *args, **kwargs)
def _import(self, module, name):
""" Import the given name from the given module, handling
errors """
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)
elif not self.fail_silently:
print("Could not load component %s" % name)
return cls
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)
FinalizableAction.__call__(self, parser, namespace, result,
option_string=option_string)
class ConfigFileAction(FinalizableAction):
""" 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):
parser.add_config_file(self.dest, values, reparse=False)
FinalizableAction.__call__(self, parser, namespace, values,
option_string=option_string)
class PluginsAction(ComponentAction):
""" :class:`Bcfg2.Options.ComponentAction` subclass for loading
Bcfg2 server plugins. """
bases = ['Bcfg2.Server.Plugins']
|