summaryrefslogtreecommitdiffstats
path: root/doc/development/option_parsing.txt
blob: 091f43cdda50e4af628994e313921db9dade0fef (plain)
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
.. -*- mode: rst -*-

.. _development-option-parsing:

====================
Bcfg2 Option Parsing
====================

Bcfg2 uses an option parsing mechanism based on the Python
:mod:`argparse` module.  It does several very useful things that
``argparse`` does not:

* Collects options from various places, which lets us easily specify
  per-plugin options, for example;
* Automatically loads components (such as plugins);
* Synthesizes option values from the command line, config files, and
  environment variables;
* Can dynamically create commands with many subcommands (e.g.,
  bcfg2-info and bcfg2-admin); and
* Supports keeping documentation inline with the option declaration,
  which will make it easier to generate man pages.


Collecting Options
==================

One of the more important features of the option parser is its ability
to automatically collect options from loaded components (e.g., Bcfg2
server plugins).  Given the highly pluggable architecture of Bcfg2,
this helps ensure two things:

#. We do not have to specify all options in all places, or even in
   most places. Options are specified alongside the class(es) that use
   them.
#. All options needed for a given script to run are guaranteed to be
   loaded, without the need to specify all components that script uses
   manually.

For instance, assume a few plugins:

* The ``Foo`` plugin takes one option, ``--foo``
* The ``Bar`` plugin takes two options, ``--bar`` and ``--force``

The plugins are used by the ``bcfg2-quux`` command, which itself takes
two options: ``--plugins`` (which selects the plugins) and
``--test``.  The options would be selected at runtime, so for instance
these would be valid:

.. code-block:: bash

    bcfg2-quux --plugins Foo --foo --test
    bcfg2-quux --plugins Foo,Bar --foo --bar --force
    bcfg2-quux --plugins Bar --force

But this would not:

    bcfg2-quux --plugins Foo --bar

The help message would reflect the options that are available to the
default set of plugins.  (For this reason, allowing component lists to
be set in the config file is very useful; that way, usage messages
reflect the components in the config file.)

Components (in this example, the plugins) can be classes or modules.
There is no required interface for an option component.  They may
*optionally* have:

* An ``options`` attribute that is a list of
  :class:`Bcfg2.Options.Options.Option` objects or option groups.
* A boolean ``parse_first`` attribute; if set to True, the options for
  the component are parsed before all other options.  This is useful
  for, e.g., Django database settings, which must be parsed before
  plugins that use Django can be loaded.
* A function or static method, ``options_parsed_hook``, that is called
  when all options have been parsed.  (This will be called again if
  :func:`Bcfg2.Options.Parser.Parser.reparse` is called.)
* A function or static method, ``component_parsed_hook``, that is
  called when early option parsing for a given component has
  completed.  This is *only* called for components with
  ``parse_first`` set to True.  It is passed a single argument: a
  :class:`argparse.Namespace` object containing the complete set of
  early options.

Options are collected through two primary mechanisms:

#. The :class:`Bcfg2.Options.Actions.ComponentAction` class.  When a
   ComponentAction subclass is used as the action of an option, then
   options contained in the classes (or modules) given in the option
   value will be added to the parser.
#. Modules that are not loaded via a
   :class:`Bcfg2.Options.Actions.ComponentAction` option may load
   options at runtime.

Since it is preferred to add components instead of just options,
loading options at runtime is generally best accomplished by creating
a container object whose only purpose is to hold options.  For
instance:

.. code-block:: python

    def foo():
        # do stuff

    class _OptionContainer(object):
        options = [
            Bcfg2.Options.BooleanOption("--foo", help="Enable foo")]

        @staticmethod
        def options_parsed_hook():
            if Bcfg2.Options.setup.foo:
                foo()

    Bcfg2.Options.get_parser().add_component(_OptionContainer)

The Bcfg2.Options module
========================

.. currentmodule:: Bcfg2.Options

.. autodata:: setup

Options
-------

The base :class:`Bcfg2.Options.Option` object represents an option.
Unlike options in :mod:`argparse`, an Option object does not need to
be associated with an option parser; it exists on its own.

.. autoclass:: Option
.. autoclass:: PathOption
.. autoclass:: BooleanOption
.. autoclass:: PositionalArgument

The Parser
----------

.. autoclass:: Parser
.. autofunction:: get_parser
.. autoexception:: OptionParserException

Option Groups
-------------

Options can be grouped in various meaningful ways.  This uses a
variety of :mod:`argparse` functionality behind the scenes.

In all cases, options can be added to groups in-line by simply
specifying them in the object group constructor:

.. code-block:: python

    options = [
        Bcfg2.Options.ExclusiveOptionGroup(
            Bcfg2.Options.Option(...),
            Bcfg2.Options.Option(...),
            required=True),
        ....]

Nesting object groups is supported in theory, but barely tested.

.. autoclass:: OptionGroup
.. autoclass:: ExclusiveOptionGroup
.. autoclass:: Subparser
.. autoclass:: WildcardSectionGroup

Subcommands
-----------

This library makes it easier to work with programs that have a large
number of subcommands (e.g., :ref:`bcfg2-info <server-bcfg2-info>` and
:ref:`bcfg2-admin <server-admin-index>`).

The normal implementation pattern is this:

#. Define all of your subcommands as children of
   :class:`Bcfg2.Options.Subcommand`.
#. Define a :class:`Bcfg2.Options.CommandRegistry` object that will be
   used to register all of the commands.  Registering a command
   collect its options and adds it as a
   :class:`Bcfg2.Options.Subparser` option group to the main option
   parser.
#. Register your commands with
   :func:`Bcfg2.Options.register_commands`, parse options, and run.

:mod:`Bcfg2.Server.Admin` provides a fairly simple implementation,
where the CLI class is itself the command registry:

.. code-block:: python

    class CLI(Bcfg2.Options.CommandRegistry):
        def __init__(self):
            Bcfg2.Options.CommandRegistry.__init__(self)
            Bcfg2.Options.register_commands(self.__class__,
                                            globals().values(),
                                            parent=AdminCmd)
            parser = Bcfg2.Options.get_parser(
                description="Manage a running Bcfg2 server",
                components=[self])
            parser.parse()

In this case, commands are collected from amongst all global variables
(the most likely scenario), and they must be children of
:class:`Bcfg2.Server.Admin.AdminCmd`, which itself subclasses
:class:`Bcfg2.Options.Subcommand`.

Commands are defined by subclassing :class:`Bcfg2.Options.Subcommand`.
At a minimum, the :func:`Bcfg2.Options.Subcommand.run` method must be
overridden, and a docstring written.

.. autoclass:: Subcommand
.. autoclass:: HelpCommand
.. autoclass:: CommandRegistry
.. autofunction:: register_commands

Actions
-------

Several custom argparse `actions
<http://docs.python.org/dev/library/argparse.html#action>`_ provide
some of the option collection magic of :mod:`Bcfg2.Options`.

.. autoclass:: ConfigFileAction
.. autoclass:: ComponentAction
.. autoclass:: PluginsAction

Option Types
------------

:mod:`Bcfg2.Options` provides a number of useful types for use as the `type
<http://docs.python.org/dev/library/argparse.html#type>`_ keyword
argument to
the :class:`Bcfg2.Options.Option` constructor.

.. autofunction:: Bcfg2.Options.Types.path
.. autofunction:: Bcfg2.Options.Types.comma_list
.. autofunction:: Bcfg2.Options.Types.colon_list
.. autofunction:: Bcfg2.Options.Types.octal
.. autofunction:: Bcfg2.Options.Types.username
.. autofunction:: Bcfg2.Options.Types.groupname
.. autofunction:: Bcfg2.Options.Types.timeout
.. autofunction:: Bcfg2.Options.Types.size

Common Options
--------------

.. autoclass:: Common