PackageConfig should automatically add LOG to namespace.
[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         if 'LOG' not in dir(namespace):
81             namespace.LOG = logger
82
83     def _name(self):
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):
88                 return attr_name
89         raise IndexError('{} not found in the {} namespace'.format(
90                 self, self._namespace))
91
92     def _replace_self(self, new_config):
93         self._logger.debug('replacing {} package config {} with {}'.format(
94                 self._package_name, self, new_config))
95         new_config.setup()
96         name = self._name()
97         setattr(self._namespace, name, new_config)
98
99     def setup(self):
100         self._logger.setLevel(self['log-level'])
101         if self['syslog']:
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']]
107         else:
108             self._logger.handlers = [self._logger._handler_cache['stream']]
109         self._logger.info('setup {} packge config:\n{}'.format(
110                 self._package_name, self.dump()))
111
112     def clear(self):
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)
119
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]
127
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):
136                     self._logger.info(
137                         'base_config file found at {}'.format(filename))
138                     replacement = config(
139                         filename=filename,
140                         package_name=self._package_name,
141                         namespace=self._namespace,
142                         logger = self._logger)
143                     replacement.load()
144                     self._replace_self(replacement)
145                     return
146                 else:
147                     self._logger.debug(
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(
155             filename=filename,
156             package_name=self._package_name,
157             namespace=self._namespace,
158             logger = self._logger)
159         self._replace_self(replacement)
160
161
162 PackageConfig._clear_class = PackageConfig
163
164
165 _util.build_backend_classes(_sys.modules[__name__])
166
167 PackageConfig._backed_subclasses = [
168     ('.h5', HDF5_PackageConfig),
169     ('.yaml', YAML_PackageConfig)
170     ]