38b872e2ff63f7ce4eab966cc66c336f5a7c53db
[h5config.git] / h5config / tools.py
1 # Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of h5config.
4 #
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.
9 #
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.
14 #
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/>.
17
18 """Tools for setting up a package using config files.
19
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:
23
24 TODO
25 > class _MyConfig
26 """
27
28 import logging as _logging
29 import os.path as _os_path
30 import sys as _sys
31
32 from . import config as _config
33 from . import log as _log
34 from . import util as _util
35
36
37 class PackageConfig (_config.Config):
38     """Configure package operation
39
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.
44
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).
48
49     Things get a bit interesting because we want to configure the
50     package from an internal class.  This leads to TODO
51     """
52     _backed_subclasses = ()
53     settings = [
54         _config.ChoiceSetting(
55             name='log-level',
56             help='Module logging level.',
57             default=_logging.WARN,
58             choices=[
59                 ('critical', _logging.CRITICAL),
60                 ('error', _logging.ERROR),
61                 ('warn', _logging.WARN),
62                 ('info', _logging.INFO),
63                 ('debug', _logging.DEBUG),
64                 ]),
65         _config.BooleanSetting(
66             name='syslog',
67             help='Log to syslog (otherwise log to stderr).',
68             default=False),
69         ]
70
71     def __init__(self, package_name, namespace=None, logger=None, **kwargs):
72         super(PackageConfig, self).__init__(**kwargs)
73         self._package_name = package_name
74         if not namespace:
75             namespace = _sys.modules[package_name]
76         self._namespace = namespace
77         if not logger:
78             logger = _log.get_basic_logger(package_name, level=_logging.WARN)
79         self._logger = logger
80
81     def _name(self):
82         "Find this instance's name in the bound namespace"
83         for attr_name in dir(self._namespace):
84             attr = getattr(self._namespace, attr_name, None)
85             if id(attr) == id(self):
86                 return attr_name
87         raise IndexError('{} not found in the {} namespace'.format(
88                 self, self._namespace))
89
90     def _replace_self(self, new_config):
91         self._logger.debug('replacing {} package config {} with {}'.format(
92                 self._package_name, self, new_config))
93         new_config.setup()
94         name = self._name()
95         setattr(self._namespace, name, new_config)
96
97     def setup(self):
98         self._logger.setLevel(self['log-level'])
99         if self['syslog']:
100             if 'syslog' not in self._logger._handler_cache:
101                 _syslog_handler = _logging_handlers.SysLogHandler()
102                 _syslog_handler.setLevel(_logging.DEBUG)
103                 self._logger._handler_cache['syslog'] = _syslog_handler
104                 self._logger.handlers = [self._logger._handler_cache['syslog']]
105         else:
106             self._logger.handlers = [self._logger._handler_cache['stream']]
107         self._logger.info('setup {} packge config:\n{}'.format(
108                 self._package_name, self.dump()))
109
110     def clear(self):
111         "Replace self with a non-backed version with default settings."
112         replacement = self._clear_class(
113                 package_name=self._package_name,
114                 namespace=self._namespace,
115                 logger = self._logger)
116         self._replace_self(replacement)
117
118     def _base_paths(self):
119         user_basepath = _os_path.join(
120             _os_path.expanduser('~'), '.{}rc'.format(self._package_name))
121         system_basepath = _os_path.join('/etc', self._package_name, 'config')
122         distributed_basepath =  _os_path.join(
123             '/usr', 'share', self._package_name, 'config')
124         return [user_basepath, system_basepath, distributed_basepath]
125
126     def load_system(self):
127         "Return the best `PackageConfig` match after scanning the filesystem"
128         self._logger.info('looking for package config file')
129         basepaths = self._base_paths()
130         for basepath in basepaths:
131             for extension,config in self._backed_subclasses:
132                 filename = basepath + extension
133                 if _os_path.exists(filename):
134                     self._logger.info(
135                         'base_config file found at {}'.format(filename))
136                     replacement = config(
137                         filename=filename,
138                         package_name=self._package_name,
139                         namespace=self._namespace,
140                         logger = self._logger)
141                     replacement.load()
142                     self._replace_self(replacement)
143                     return
144                 else:
145                     self._logger.debug(
146                         'no base_config file at {}'.format(filename))
147         # create (but don't save) the preferred file
148         basepath = basepaths[0]
149         extension,config = self._backed_subclasses[0]
150         filename = basepath + extension
151         self._logger.info('new base_config file at {}'.format(filename))
152         replacement = config(
153             filename=filename,
154             package_name=self._package_name,
155             namespace=self._namespace,
156             logger = self._logger)
157         self._replace_self(replacement)
158
159
160 PackageConfig._clear_class = PackageConfig
161
162
163 _util.build_backend_classes(_sys.modules[__name__])
164
165 PackageConfig._backed_subclasses = [
166     ('.h5', HDF5_PackageConfig),
167     ('.yaml', YAML_PackageConfig)
168     ]