def _setup_admin_options(self, values_source_list): base_namespace = Namespace() base_namespace.admin = admin = Namespace() admin.add_option(name='print_conf', default=None, doc='write current config to stdout (%s)' % ', '.join(file_extension_dispatch.keys())) admin.add_option( name='dump_conf', default='', doc='a pathname to which to write the current config', ) admin.add_option(name='strict', default=False, doc='mismatched options generate exceptions rather' ' than just warnings') admin.add_option( name='expose_secrets', default=False, doc='should options marked secret get written out or hidden?') # only offer the config file admin options if they've been requested in # the values source list if ConfigFileFutureProxy in values_source_list: default_config_pathname = self._get_config_pathname() admin.add_option( name='conf', default=default_config_pathname, doc='the pathname of the config file (path/filename)', ) return base_namespace
def _some_namespaces(self): """set up some namespaces""" n = Namespace(doc='top') n.add_option('aaa', '2011-05-04T15:10:00', 'the a', short_form='a', from_string_converter=datetime_from_ISO_string) n.c = Namespace(doc='c space') n.c.add_option( 'fred', 'stupid', # deliberate whitespace to test that it gets stripped ' husband from Flintstones ') n.c.add_option('wilma', 'waspish', 'wife from Flintstones') n.c.e = Namespace(doc='e space') n.c.e.add_option('dwight', default=97, doc='my uncle') n.c.add_option('dwight', default=98, doc='your uncle') n.d = Namespace(doc='d space') n.d.add_option('fred', 'crabby', 'male neighbor from I Love Lucy') n.d.add_option('ethel', 'silly', 'female neighbor from I Love Lucy') n.x = Namespace(doc='x space') n.x.add_option('size', 100, 'how big in tons', short_form='s') n.x.add_option('password', 'secret', 'the password') return n
def _some_namespaces(self): """set up some namespaces""" n = Namespace(doc='top') n.add_option( 'aaa', '2011-05-04T15:10:00', 'the a', short_form='a', from_string_converter=datetime_from_ISO_string ) n.c = Namespace(doc='c space') n.c.add_option( 'dwight', 'stupid, deadly', 'husband from Flintstones' ) n.c.add_option('wilma', "waspish's", 'wife from Flintstones') n.d = Namespace(doc='d space') n.d.add_option('dwight', "crabby", 'male neighbor from I Love Lucy') n.d.add_option( 'ethel', 'silly', 'female neighbor from I Love Lucy' ) n.x = Namespace(doc='x space') n.x.add_option('size', 100, 'how big in tons', short_form='s') n.x.add_option('password', 'secret "message"', 'the password') return n
def setup_definitions(source, destination): for key, val in source.items(): if key.startswith('__'): continue # ignore these if isinstance(val, Option): destination[key] = val if not val.name: val.name = key val.set_value(val.default) elif isinstance(val, Aggregation): destination[key] = val elif isinstance(val, collections.Mapping): if 'name' in val and 'default' in val: # this is an Option in the form of a dict, not a Namespace if key == 'not_for_definition' and val is True: continue # ignore this element params = str_dict_keys(val) destination[key] = Option(**params) elif 'function' in val: # this is an Aggregation params = str_dict_keys(val) destination[key] = Aggregation(**params) else: # this is a Namespace if key not in destination: try: destination[key] = Namespace(doc=val._doc) except AttributeError: destination[key] = Namespace() # recurse! setup_definitions(val, destination[key]) else: destination[key] = Option(name=key, doc=key, default=val)
def test_write_json(self): n = Namespace(doc='top') n.add_option('aaa', '2011-05-04T15:10:00', 'the a', short_form='a', from_string_converter=datetime_from_ISO_string) c = ConfigurationManager([n], use_admin_controls=True, use_auto_help=False, argv_source=[]) out = StringIO() c.write_conf(for_json, opener=stringIO_context_wrapper(out)) received = out.getvalue() out.close() jrec = json.loads(received) expect_to_find = { "short_form": "a", "default": "2011-05-04T15:10:00", "doc": "the a", "value": "2011-05-04T15:10:00", "from_string_converter": "configman.datetime_util.datetime_from_ISO_string", "name": "aaa" } for key, value in expect_to_find.items(): self.assertEqual(jrec['aaa'][key], value)
def test_write_ini_with_custom_converters(self): def dict_encoder(dict_): return ','.join('%s:%s' % (k, v) for (k, v) in dict_.items()) def dict_decoder(string): return dict(x.split(':') for x in string.split(',')) n = Namespace(doc='top') n.add_option( 'a', default={'one': 'One'}, doc='the doc string', to_string_converter=dict_encoder, from_string_converter=dict_decoder, ) c = ConfigurationManager([n], use_admin_controls=True, use_auto_help=False, argv_source=[]) expected = "# the doc string\n#a=one:One\n" out = StringIO() c.write_conf(for_configobj, opener=stringIO_context_wrapper(out)) received = out.getvalue() out.close() self.assertEqual(expected.strip(), received.strip())
def donttest_for_configobj_basics_3(self): n = Namespace() n.add_option("name", default='lars') n.add_option("awesome", default='lars') n.namespace('othersection') n.othersection.add_option('foo', default=23) tmp_filename = os.path.join(tempfile.gettempdir(), 'test.ini') open(tmp_filename, 'w').write(""" # comment name=Peter awesome= # comment [othersection] bad_option=bar # other comment """) try: self.assertRaises( NotAnOptionError, ConfigurationManager, [n], [tmp_filename], ) finally: if os.path.isfile(tmp_filename): os.remove(tmp_filename)
def get_required_config(self): """because of the exsistance of subparsers, the configman options that correspond with argparse arguments are not a constant. We need to produce a copy of the namespace rather than the actual embedded namespace.""" required_config = Namespace() # add current options to a copy of required config for k, v in iteritems_breadth_first(self.required_config): required_config[k] = v # get any option found in any subparsers try: subparser_namespaces = ( self.configman_subparsers_option.foreign_data.argparse. subprocessor_from_string_converter) subparsers = (self._argparse_subparsers._configman_option. foreign_data.argparse.subparsers) # each subparser needs to have its configman options set up # in the subparser's configman option. This routine copies # the required_config of each subparser into the # SubparserFromStringConverter defined above. for subparser_name, subparser_data in six.iteritems(subparsers): subparser_namespaces.add_namespace( subparser_name, subparser_data.subparser.get_required_config()) except AttributeError: # there is no subparser pass return required_config
class SubparserValue(str): """Instances of this class/closure serve as the value given out as the final value of the subparser configman option. It is a string, that also has a 'get_required_config' method that can bring in the the arguments defined in the subparser in the local namespace. The mechanism works in the same manner that configman normally does expansion of dynamically loaded classes.""" required_config = Namespace() try: # define the class dynamically, giving it the required_config # that corresponds with the confiman options defined for the # subparser of the same name. required_config = self.namespaces[subparser_name] except KeyError: raise CannotConvertError('%s is not a known sub-command' % subparser_name) #------------------------------------------------------------------ def __new__(cls): """deriving from string is tricky business. You cannot set the value in an __init__ method because strings are immutable and __init__ time is too late. The 'new' method is the only chance to properly set the value.""" obj = str.__new__(cls, subparser_name) return obj #------------------------------------------------------------------ def get_required_config(self): return self.required_config #------------------------------------------------------------------ def to_str(self): return subparser_name
def get_required_config(cls): result = Namespace() for a_class in reversed(cls.__mro__): try: result.update(a_class.required_config) except AttributeError: pass return result
def __init__(self, *args, **kwargs): self.get_parser_id() self.subparser_name = kwargs.pop('subparser_name', None) self.configman_subparsers_option = kwargs.pop( 'configman_subparsers_option', None) super(IntermediateConfigmanParser, self).__init__(*args, **kwargs) self.required_config = Namespace() self._use_argparse_add_help = kwargs.get('add_help', False)
class InnerClassList(RequiredConfig): """This nested class is a proxy list for the classes. It collects all the config requirements for the listed classes and places them each into their own Namespace. """ # we're dynamically creating a class here. The following block of # code is actually adding class level attributes to this new class required_config = Namespace() # 1st requirement for configman subordinate_namespace_names = [] # to help the programmer know # what Namespaces we added namespace_template = template_for_namespace # save the template # for future reference class_option_name = name_of_class_option # save the class's option # name for the future # for each class in the class list for namespace_index, a_class in enumerate(class_list): # figure out the Namespace name namespace_name = template_for_namespace % namespace_index subordinate_namespace_names.append(namespace_name) # create the new Namespace required_config[namespace_name] = Namespace() # add the option for the class itself required_config[namespace_name].add_option( name_of_class_option, #doc=a_class.__doc__ # not helpful if too verbose default=a_class, from_string_converter=class_converter) if instantiate_classes: # add an aggregator to instantiate the class required_config[namespace_name].add_aggregation( "%s_instance" % name_of_class_option, lambda c, lc, a: lc[name_of_class_option](lc)) @classmethod def to_str(cls): """this method takes this inner class object and turns it back into the original string of classnames. This is used primarily as for the output of the 'help' option""" return ', '.join( py_obj_to_str(v[name_of_class_option].value) for v in cls.get_required_config().values() if isinstance(v, Namespace))
def __init__(self, *args, **kwargs): self.original_args = args self.original_kwargs = kwargs.copy() self.version = kwargs.get("version") # py3 argparse doesn't define kwargs['add_help'] = False # stop help, reintroduce it later self.subparser_name = kwargs.pop('subparser_name', None) self.configman_subparsers_option = kwargs.pop( 'configman_subparsers_option', None) super(ArgumentParser, self).__init__(*args, **kwargs) self.value_source_list = [environ, ConfigFileFutureProxy, argparse] self.required_config = Namespace()
def test_setup_definitions_1(self): d = DotDict() def fake_mapping_func(source, destination): self.assertTrue(isinstance(source, collections.Mapping)) self.assertEqual(d, destination) saved_original = defsrc.definition_dispatch.copy() try: defsrc.definition_dispatch[collections.Mapping] = fake_mapping_func s = {} defsrc.setup_definitions(s, d) s = DotDict() defsrc.setup_definitions(s, d) s = Namespace() defsrc.setup_definitions(s, d) finally: defsrc.definition_dispatch = saved_original
def test_json_round_trip(self): n = Namespace(doc='top') n.add_option('aaa', '2011-05-04T15:10:00', 'the a', short_form='a', from_string_converter=datetime_from_ISO_string) expected_date = datetime_from_ISO_string('2011-05-04T15:10:00') n.add_option('bbb', '37', 'the a', short_form='a', from_string_converter=int) n.add_option('write', 'json') n.add_aggregation('bbb_minus_one', bbb_minus_one) name = '/tmp/test.json' import functools opener = functools.partial(open, name, 'w') c1 = ConfigurationManager([n], [], use_admin_controls=True, use_auto_help=False, app_name='/tmp/test', app_version='0', app_description='', argv_source=[]) c1.write_conf('json', opener) d1 = {'bbb': 88} d2 = {'bbb': '-99'} try: with open(name) as jfp: j = json.load(jfp) c2 = ConfigurationManager((j, ), (d1, d2), use_admin_controls=True, use_auto_help=False, argv_source=[]) config = c2.get_config() self.assertEqual(config.aaa, expected_date) self.assertEqual(config.bbb, -99) self.assertEqual(config.bbb_minus_one, -100) finally: os.unlink(name)
def __init__( self, definition_source=None, values_source_list=None, argv_source=None, #use_config_files=True, use_auto_help=True, use_admin_controls=True, quit_after_admin=True, options_banned_from_help=None, app_name='', app_version='', app_description='', config_pathname='.', config_optional=True, value_source_object_hook=DotDict, ): """create and initialize a configman object. parameters: definition_source - a namespace or list of namespaces from which configman is to fetch the definitions of the configuration parameters. values_source_list - (optional) a hierarchical list of sources for values for the configuration parameters. As values are copied from these sources, conficting values are resolved with sources on the right getting preference over sources on the left. argv_source - if the values_source_list contains a commandline source, this value is an alternative source for actual command line arguments. Useful for testing or preprocessing command line arguments. use_auto_help - set to True if configman is to automatically set up help output for command line invocations. use_admin_controls - configman can add command line flags that it interprets independently of the app defined arguments. True enables this capability, while, False supresses it. quit_after_admin - if True and admin controls are enabled and used, call sys.exit to end the app. This is useful to stop the app from running if all that was done was to write a config file or stop after help. options_banned_from_help - a list of strings that will censor the output of help to prevent specified options from being listed in the help output. This is useful for hiding debug or secret command line arguments. app_name - assigns a name to the app. This is used in help output and as a default basename for config files. app_version - assigns a version for the app used help output. app_description - assigns a description for the app to be used in the help output. config_pathname - a hard coded path to the directory of or the full path and name of the configuration file. config_optional - a boolean indicating if a missing default config file is optional. Note: this is only for the default config file. If a config file is specified on the commandline, it _must_ exist. value_source_object_hook - a class used for the internal representation of a value source. This is used to enable any special processing, like key translations. """ # instead of allowing mutables as default keyword argument values... if definition_source is None: definition_source_list = [] elif (isinstance(definition_source, collections.Sequence) and not isinstance(definition_source, (six.binary_type, six.text_type))): definition_source_list = list(definition_source) else: if isinstance(definition_source, (six.binary_type, six.text_type)): definition_source = to_str(definition_source) definition_source_list = [definition_source] if argv_source is None: self.argv_source = sys.argv[1:] self.app_invocation_name = sys.argv[0] else: self.argv_source = argv_source self.app_invocation_name = app_name if options_banned_from_help is None: options_banned_from_help = ['application'] self.config_pathname = config_pathname self.config_optional = config_optional self.use_auto_help = use_auto_help self.value_source_object_hook = value_source_object_hook self.app_name = app_name self.app_version = app_version self.app_description = app_description self.args = [] # extra commandline arguments that are not switches # will be stored here. self._config = None # eventual container for DOM-like config object self.option_definitions = Namespace() self.definition_source_list = definition_source_list command_line_value_source = command_line if values_source_list is None: # nothing set, assume defaults if use_admin_controls: values_source_list = (ConfigFileFutureProxy, environment, command_line_value_source) else: values_source_list = (environment, command_line_value_source) # determine which command_line facility to use for help if self.use_auto_help: # we need to iterate through all of our value sources looking for # one that can interact with the user on the commandline. for a_value_source in values_source_list: if inspect.ismodule(a_value_source): handler = \ type_handler_dispatch[a_value_source][0].ValueSource try: # if a value source is able to handle the command line # it will have defined 'command_line_value_source' as # true. Not all values sources may have this attribute if handler.command_line_value_source: handler._setup_auto_help(self) break except AttributeError: # not a commandline source because it doesn't have # the 'command_line_value_source' OR it doesn't have # a method that allows it to setup a help system. # this is OK, we can ignore it and move on until we # find an appropriate source. pass else: # While not actually necessary to have implemented, this # is the case where the value source is not a module. # So we know nothing about its interface. We cannot even # try to use it as a commandline value source. pass admin_tasks_done = False self.keys_blocked_from_output = [ 'help', 'admin.conf', 'admin.dump_conf', 'admin.print_conf', 'admin.strict', 'admin.expose_secrets', ] self.options_banned_from_help = options_banned_from_help if use_admin_controls: admin_options = self._setup_admin_options(values_source_list) self.definition_source_list.append(admin_options) # iterate through the option definitions to create the nested dict # hierarchy of all the options called 'option_definitions' for a_definition_source in self.definition_source_list: try: safe_copy_of_def_source = a_definition_source.safe_copy() except AttributeError: # apparently, the definition source was not in the form of a # Namespace object. This isn't a show stopper, but we don't # know how to make a copy of this object safely: we know from # experience that the stock copy.copy method leads to grief # as many sub-objects within an option definition source can # not be copied that way (classes, for example). # The only action we can take is to trust and continue with the # original copy of the definition source. safe_copy_of_def_source = a_definition_source setup_definitions(safe_copy_of_def_source, self.option_definitions) if use_admin_controls: # the name of the config file needs to be loaded from the command # line prior to processing the rest of the command line options. config_filename = config_filename_from_commandline(self) if (config_filename and ConfigFileFutureProxy in values_source_list): self.option_definitions.admin.conf.default = config_filename self.values_source_list = wrap_with_value_source_api( values_source_list, self) known_keys = self._overlay_expand() self._check_for_mismatches(known_keys) # the app_name, app_version and app_description are to come from # if 'application' option if it is present. If it is not present, # get the app_name,et al, from parameters passed into the constructor. # if those are empty, set app_name, et al, to empty strings try: app_option = self._get_option('application') self.app_name = getattr(app_option.value, 'app_name', '') self.app_version = getattr(app_option.value, 'app_version', '') self.app_description = getattr(app_option.value, 'app_description', '') except NotAnOptionError: # there is no 'application' option, continue to use the # 'app_name' from the parameters passed in, if they exist. pass try: if use_auto_help and self._get_option('help').value: self.output_summary() admin_tasks_done = True except NotAnOptionError: # the current command-line implementation already has a help # mechanism of its own that doesn't require the use of a # option in configman. This error is ignorable pass # keys that end with a "$" are called "blocked_by_suffix". # This means that these options are not to be written out to # configuration files. keys_blocked_by_suffix = [ key for key in self.option_definitions.keys_breadth_first() if key.endswith('$') ] self.keys_blocked_from_output.extend(keys_blocked_by_suffix) if use_admin_controls and self._get_option('admin.print_conf').value: self.print_conf() admin_tasks_done = True if use_admin_controls and self._get_option('admin.dump_conf').value: self.dump_conf() admin_tasks_done = True if quit_after_admin and admin_tasks_done: sys.exit()
def generate_docs(self, clspath, more_content): """Generate documentation for this configman class""" obj = import_class(clspath) sourcename = 'docstring of %s' % clspath # Add the header modname, clsname = split_clspath(clspath) self.add_line('.. %s:%s:: %s.%s' % ('py', 'class', modname, clsname), sourcename) self.add_line('', sourcename) # Add the docstring if there is one docstring = getattr(obj, '__doc__', None) if docstring: docstringlines = prepare_docstring(docstring, ignore=1) for i, line in enumerate(docstringlines): self.add_line(' ' + line, sourcename, i) self.add_line('', '') # Add additional content from the directive if there was any if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(' ' + line, src[0], src[1]) self.add_line('', '') # Add configman related content namespace = Namespace() for cls in reversed(obj.__mro__): try: namespace.update(cls.required_config) except AttributeError: pass if namespace: self.add_line(' Configuration:', '') self.add_line('', '') sourcename = 'class definition' def generate_namespace_docs(namespace, basename=''): for name, value in namespace.iteritems(): if isinstance(value, Namespace): generate_namespace_docs(value, name + '_') elif isinstance(value, Option): self.add_line( ' ``%s``' % (basename + value.name), sourcename) self.add_line( ' :default: ``%r``' % value.default, sourcename) self.add_line( ' :converter: %r' % value.from_string_converter, sourcename) if value.reference_value_from: self.add_line( ' :base: ``%s``' % (value.reference_value_from + '.' + value.name), sourcename) self.add_line('', '') self.add_line(' %s' % value.doc, sourcename) self.add_line('', '') elif isinstance(value, Aggregation): # Ignore aggregations--they're for setting something up # using a bunch of configuratino things (I think) pass else: raise Exception('No idea what to do with %r' % value) generate_namespace_docs(namespace)
def _overlay_expand(self): """This method overlays each of the value sources onto the default in each of the defined options. It does so using a breadth first iteration, overlaying and expanding each level of the tree in turn. As soon as no changes were made to any level, the loop breaks and the work is done. The actual action of the overlay is to take the value from the source and copy into the 'default' member of each Option object. "expansion" means converting an option value into its real type from string. The conversion is accomplished by simply calling the 'set_value' method of the Option object. If the resultant type has its own configuration options, bring those into the current namespace and then proceed to overlay/expand those. """ new_keys_have_been_discovered = True # loop control, False breaks loop finished_keys = set() all_reference_values = {} while new_keys_have_been_discovered: # loop until nothing more is done # names_of_all_exsting_options holds a list of all keys in the # option definitons in breadth first order using this form: # [ 'x', 'y', 'z', 'x.a', 'x.b', 'z.a', 'z.b', 'x.a.j', 'x.a.k', # 'x.b.h'] names_of_all_exsting_options = [ x for x in self.option_definitions.keys_breadth_first() if isinstance(self.option_definitions[x], Option) ] new_keys_have_been_discovered = False # setup to break loop # create alternate paths options set_of_reference_value_option_names = \ self._create_reference_value_options( names_of_all_exsting_options, finished_keys ) for a_ref_option_name in set_of_reference_value_option_names: if a_ref_option_name not in all_reference_values: all_reference_values[a_ref_option_name] = [] all_keys = list(set_of_reference_value_option_names) \ + names_of_all_exsting_options # previous versions of this method pulled the values from the # values sources deeper within the following nested loops. # that was not necessary and caused a lot of redundant work. # the 'values_from_all_sources' now holds all the the values # from each of the value sources. values_from_all_sources = [ a_value_source.get_values( self, # pass in the config_manager itself True, # ignore mismatches self.value_source_object_hook # build with this class ) for a_value_source in self.values_source_list ] # overlay process: # fetch all the default values from the value sources before # applying the from string conversions for key in all_keys: if key in finished_keys: continue #if not isinstance(an_option, Option): # continue # aggregations and other types are ignored # loop through all the value sources looking for values # that match this current key. if self.option_definitions[key].reference_value_from: reference_value_from = ( self.option_definitions[key].reference_value_from) top_key = key.split('.')[-1] self.option_definitions[key].default = ( self.option_definitions[reference_value_from] [top_key].default) all_reference_values['.'.join( (reference_value_from, top_key))].append(key) an_option = self.option_definitions[key] if key in all_reference_values: # make sure that this value gets propagated to keys # even if the keys have already been overlaid finished_keys -= set(all_reference_values[key]) for val_src_dict in values_from_all_sources: try: # overlay the default with the new value from # the value source. This assignment may come # via acquisition, so the key given may not have # been an exact match for what was returned. an_option.has_changed = (an_option.default != val_src_dict[key]) an_option.default = val_src_dict[key] if key in all_reference_values: # make sure that this value gets propagated to keys # even if the keys have already been overlaid finished_keys -= set(all_reference_values[key]) except KeyError as x: pass # okay, that source doesn't have this value # expansion process: # step through all the keys converting them to their proper # types and bringing in any new keys in the process for key in all_keys: if key in finished_keys: continue # mark this key as having been seen and processed finished_keys.add(key) an_option = self.option_definitions[key] #if not isinstance(an_option, Option): # continue # aggregations, namespaces are ignored # apply the from string conversion to make the real value an_option.set_value(an_option.default) # new values have been seen, don't let loop break new_keys_have_been_discovered = True try: try: # try to fetch new requirements from this value new_requirements = \ an_option.value.get_required_config() except (AttributeError, KeyError): new_requirements = getattr(an_option.value, 'required_config', None) # make sure what we got as new_req is actually a # Mapping of some sort if not isinstance(new_requirements, collections.Mapping): # we didn't get a mapping, perhaps the option value # was a Mock object - in any case we can't try to # interpret 'new_req' as a configman requirement # collection. We must abandon processing this # option further continue if not isinstance(new_requirements, Namespace): new_requirements = Namespace( initializer=new_requirements) # get the parent namespace current_namespace = self.option_definitions.parent(key) if current_namespace is None: # we're at the top level, use the base namespace current_namespace = self.option_definitions if current_namespace._reference_value_from: # don't expand things that are in reference value # namespaces, they will be populated by expanding the # targets continue # some new Options to be brought in may have already been # seen and in the finished_keys set. They must be reset # as unfinished so that a new default doesn't permanently # overwrite any of the values already placed by the # overlays. So we've got to remove those keys from the # finished keys list. # Before we can do that however, we need the fully # qualified names for the new keys. qualified_parent_name_list = key.rsplit('.', 1) if len(qualified_parent_name_list) > 1: qualified_parent_name = qualified_parent_name_list[0] else: qualified_parent_name = '' finished_keys = finished_keys.difference( '.'.join((qualified_parent_name, ref_option_name)) for ref_option_name in new_requirements) # add the new Options to the namespace new_namespace = new_requirements.safe_copy( an_option.reference_value_from) for new_key in new_namespace.keys_breadth_first(): if new_key not in current_namespace: current_namespace[new_key] = new_namespace[new_key] except AttributeError as x: # there are apparently no new Options to bring in from # this option's value pass return finished_keys