1 # Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
3 # This file is part of h5config.
5 # h5config is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
10 # h5config is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with h5config. If not, see <http://www.gnu.org/licenses/>.
18 """Tools for setting up a package using config files.
20 The benefit of subclassing `PackageConfig` over using something like
21 `configparser` is that you can easily store default `h5config` values
22 in the configuration file. Consider the following example:
28 import logging as _logging
29 import os.path as _os_path
32 from . import config as _config
33 from . import log as _log
34 from . import util as _util
37 class PackageConfig (_config.Config):
38 """Configure package operation
40 This basic implementation just creates and manages a package-wide
41 `LOG` instance. If you create this instance on your own (for
42 example, to work around bootstrapping issues), just pass your
43 instance in as `logger` when you initialize this class.
45 Because this class occasionally replaces itself, you should always
46 work with the version in the package namespace and not with a
47 local reference (which may be stale).
49 Things get a bit interesting because we want to configure the
50 package from an internal class. This leads to TODO
52 _backed_subclasses = ()
54 _config.ChoiceSetting(
56 help='Module logging level.',
57 default=_logging.WARN,
59 ('critical', _logging.CRITICAL),
60 ('error', _logging.ERROR),
61 ('warn', _logging.WARN),
62 ('info', _logging.INFO),
63 ('debug', _logging.DEBUG),
65 _config.BooleanSetting(
67 help='Log to syslog (otherwise log to stderr).',
71 def __init__(self, package_name, namespace=None, logger=None, **kwargs):
72 super(PackageConfig, self).__init__(**kwargs)
73 self._package_name = package_name
75 namespace = _sys.modules[package_name]
76 self._namespace = namespace
78 logger = _log.get_basic_logger(package_name, level=_logging.WARN)
80 if 'LOG' not in dir(namespace):
81 namespace.LOG = logger
84 "Find this instance's name in the bound namespace"
85 for attr_name in dir(self._namespace):
86 attr = getattr(self._namespace, attr_name, None)
87 if id(attr) == id(self):
89 raise IndexError('{} not found in the {} namespace'.format(
90 self, self._namespace))
92 def _replace_self(self, new_config):
93 self._logger.debug('replacing {} package config {} with {}'.format(
94 self._package_name, self, new_config))
97 setattr(self._namespace, name, new_config)
100 self._logger.setLevel(self['log-level'])
102 if 'syslog' not in self._logger._handler_cache:
103 _syslog_handler = _logging_handlers.SysLogHandler()
104 _syslog_handler.setLevel(_logging.DEBUG)
105 self._logger._handler_cache['syslog'] = _syslog_handler
106 self._logger.handlers = [self._logger._handler_cache['syslog']]
108 self._logger.handlers = [self._logger._handler_cache['stream']]
109 self._logger.info('setup {} packge config:\n{}'.format(
110 self._package_name, self.dump()))
113 "Replace self with a non-backed version with default settings."
114 replacement = self._clear_class(
115 package_name=self._package_name,
116 namespace=self._namespace,
117 logger = self._logger)
118 self._replace_self(replacement)
120 def _base_paths(self):
121 user_basepath = _os_path.join(
122 _os_path.expanduser('~'), '.{}rc'.format(self._package_name))
123 system_basepath = _os_path.join('/etc', self._package_name, 'config')
124 distributed_basepath = _os_path.join(
125 '/usr', 'share', self._package_name, 'config')
126 return [user_basepath, system_basepath, distributed_basepath]
128 def load_system(self):
129 "Return the best `PackageConfig` match after scanning the filesystem"
130 self._logger.info('looking for package config file')
131 basepaths = self._base_paths()
132 for basepath in basepaths:
133 for extension,config in self._backed_subclasses:
134 filename = basepath + extension
135 if _os_path.exists(filename):
137 'base_config file found at {}'.format(filename))
138 replacement = config(
140 package_name=self._package_name,
141 namespace=self._namespace,
142 logger = self._logger)
144 self._replace_self(replacement)
148 'no base_config file at {}'.format(filename))
149 # create (but don't save) the preferred file
150 basepath = basepaths[0]
151 extension,config = self._backed_subclasses[0]
152 filename = basepath + extension
153 self._logger.info('new base_config file at {}'.format(filename))
154 replacement = config(
156 package_name=self._package_name,
157 namespace=self._namespace,
158 logger = self._logger)
159 self._replace_self(replacement)
162 PackageConfig._clear_class = PackageConfig
165 _util.build_backend_classes(_sys.modules[__name__])
167 PackageConfig._backed_subclasses = [
168 ('.h5', HDF5_PackageConfig),
169 ('.yaml', YAML_PackageConfig)