Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/conf.py: 100%
238 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-12-18 23:41 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2024-12-18 23:41 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24WuttJamaican - app configuration
25"""
27import configparser
28import importlib
29import logging
30import logging.config
31import os
32import sys
33import tempfile
35import config as configuration
37from wuttjamaican.util import (load_entry_points, load_object,
38 parse_bool, parse_list,
39 UNSPECIFIED)
40from wuttjamaican.exc import ConfigurationError
43log = logging.getLogger(__name__)
46class WuttaConfig:
47 """
48 Configuration class for Wutta Framework
50 A single instance of this class is typically created on app
51 startup, by calling :func:`make_config()`.
53 The global config object is mainly responsible for providing
54 config values to the app, via :meth:`get()` and similar methods.
56 The config object may have more than one place to look when
57 finding values. This can vary somewhat but often the priority for
58 lookup is like:
60 * settings table in the DB
61 * one or more INI files
62 * "defaults" provided by app logic
64 :param files: List of file paths from which to read config values.
66 :param defaults: Initial values to use as defaults. This gets
67 converted to :attr:`defaults` during construction.
69 :param appname: Value to assign for :attr:`appname`.
71 :param usedb: Flag indicating whether config values should ever be
72 looked up from the DB. Note that you can override this when
73 calling :meth:`get()`.
75 :param preferdb: Flag indicating whether values from DB should be
76 preferred over the values from INI files or app defaults. Note
77 that you can override this when calling :meth:`get()`.
79 :param configure_logging: Flag indicating whether logging should
80 be configured during object construction. If not specified,
81 the config values will determine behavior.
83 Attributes available on the config instance:
85 .. attribute:: appname
87 Code-friendly name ("key") for the app. This is used as the
88 basis for various config settings and will therefore determine
89 what is returned from :meth:`get_app()` etc.
91 For instance the default ``appname`` value is ``'wutta'`` which
92 means a sample config file might look like:
94 .. code-block:: ini
96 [wutta]
97 app.handler = wuttjamaican.app:AppHandler
99 [wutta.db]
100 default.url = sqlite://
102 But if the ``appname`` value is e.g. ``'rattail'`` then the
103 sample config should instead look like:
105 .. code-block:: ini
107 [rattail]
108 app.handler = wuttjamaican.app:AppHandler
110 [rattail.db]
111 default.url = sqlite://
113 .. attribute:: configuration
115 Reference to the
116 :class:`python-configuration:config.ConfigurationSet` instance
117 which houses the full set of config values which are kept in
118 memory. This does *not* contain settings from DB, but *does*
119 contain :attr:`defaults` as well as values read from INI files.
121 .. attribute:: defaults
123 Reference to the
124 :class:`python-configuration:config.Configuration` instance
125 containing config *default* values. This is exposed in case
126 it's useful, but in practice you should not update it directly;
127 instead use :meth:`setdefault()`.
129 .. attribute:: default_app_handler_spec
131 Spec string for the default app handler, if config does not
132 specify to use another.
134 The true default for this is ``'wuttjamaican.app:AppHandler'``
135 (aka. :class:`~wuttjamaican.app.AppHandler`).
137 .. attribute:: default_engine_maker_spec
139 Spec string for the default engine maker function, if config
140 does not specify to use another.
142 The true default for this is
143 ``'wuttjamaican.db.conf:make_engine_from_config'`` (aka.
144 :func:`~wuttjamaican.db.conf.make_engine_from_config()`).
146 .. attribute:: files_read
148 List of all INI config files which were read on app startup.
149 These are listed in the same order as they were read. This
150 sequence also reflects priority for value lookups, i.e. the
151 first file with the value wins.
153 .. attribute:: usedb
155 Whether the :term:`settings table` should be searched for
156 config settings. This is ``False`` by default but may be
157 enabled via config file:
159 .. code-block:: ini
161 [wutta.config]
162 usedb = true
164 See also :ref:`where-config-settings-come-from`.
166 .. attribute:: preferdb
168 Whether the :term:`settings table` should be preferred over
169 :term:`config files<config file>` when looking for config
170 settings. This is ``False`` by default, and in any case is
171 ignored unless :attr:`usedb` is ``True``.
173 Most apps will want to enable this flag so that when the
174 settings table is updated, it will immediately affect app
175 behavior regardless of what values are in the config files.
177 .. code-block:: ini
179 [wutta.config]
180 usedb = true
181 preferdb = true
183 See also :ref:`where-config-settings-come-from`.
184 """
185 default_app_handler_spec = 'wuttjamaican.app:AppHandler'
186 default_engine_maker_spec = 'wuttjamaican.db.conf:make_engine_from_config'
188 def __init__(
189 self,
190 files=[],
191 defaults={},
192 appname='wutta',
193 usedb=None,
194 preferdb=None,
195 configure_logging=None,
196 ):
197 self.appname = appname
198 configs = []
200 # read all files requested
201 self.files_read = []
202 for path in files:
203 self._load_ini_configs(path, configs, require=True)
205 # add config for use w/ setdefault()
206 self.defaults = configuration.Configuration(defaults)
207 configs.append(self.defaults)
209 # master config set
210 self.configuration = configuration.ConfigurationSet(*configs)
212 # establish logging
213 if configure_logging is None:
214 configure_logging = self.get_bool(f'{self.appname}.config.configure_logging',
215 default=False, usedb=False)
216 if configure_logging:
217 self._configure_logging()
219 # usedb flag
220 self.usedb = usedb
221 if self.usedb is None:
222 self.usedb = self.get_bool(f'{self.appname}.config.usedb',
223 default=False, usedb=False)
225 # preferdb flag
226 self.preferdb = preferdb
227 if self.usedb and self.preferdb is None:
228 self.preferdb = self.get_bool(f'{self.appname}.config.preferdb',
229 default=False, usedb=False)
231 # configure main app DB if applicable, or disable usedb flag
232 try:
233 from wuttjamaican.db import Session, get_engines
234 except ImportError:
235 if self.usedb:
236 log.warning("config created with `usedb = True`, but can't import "
237 "DB module(s), so setting `usedb = False` instead",
238 exc_info=True)
239 self.usedb = False
240 self.preferdb = False
241 else:
242 self.appdb_engines = get_engines(self, f'{self.appname}.db')
243 self.appdb_engine = self.appdb_engines.get('default')
244 Session.configure(bind=self.appdb_engine)
246 log.debug("config files read: %s", self.files_read)
248 def _load_ini_configs(self, path, configs, require=True):
249 path = os.path.abspath(path)
251 # no need to read a file twice; its first appearance sets priority
252 if path in self.files_read:
253 return
255 # try to load config with standard parser, and default vars
256 here = os.path.dirname(path)
257 config = configparser.ConfigParser(defaults={'here': here, '__file__': path})
258 if not config.read(path):
259 if require:
260 raise FileNotFoundError(f"could not read required config file: {path}")
261 return
263 # load all values into (yet another) temp config
264 temp_config = configparser.RawConfigParser()
265 for section in config.sections():
266 temp_config.add_section(section)
267 # nb. must interpolate most values but *not* for logging formatters
268 raw = section.startswith('formatter_')
269 for option in config.options(section):
270 temp_config.set(section, option, config.get(section, option, raw=raw))
272 # re-write as temp file with "final" values
273 fd, temp_path = tempfile.mkstemp(suffix='.ini')
274 os.close(fd)
275 with open(temp_path, 'wt') as f:
276 temp_config.write(f)
278 # and finally, load that into our main config
279 config = configuration.config_from_ini(temp_path, read_from_file=True)
280 configs.append(config)
281 self.files_read.append(path)
283 # bring in any "required" files
284 requires = config.get(f'{self.appname}.config.require')
285 if requires:
286 for path in self.parse_list(requires):
287 self._load_ini_configs(path, configs, require=True)
289 # bring in any "included" files
290 includes = config.get(f'{self.appname}.config.include')
291 if includes:
292 for path in self.parse_list(includes):
293 self._load_ini_configs(path, configs, require=False)
295 def get_prioritized_files(self):
296 """
297 Returns list of config files in order of priority.
299 By default, :attr:`files_read` should already be in the
300 correct order, but this is to make things more explicit.
301 """
302 return self.files_read
304 def setdefault(
305 self,
306 key,
307 value):
308 """
309 Establish a default config value for the given key.
311 Note that there is only *one* default value per key. If
312 multiple calls are made with the same key, the first will set
313 the default and subsequent calls have no effect.
315 :returns: The current config value, *outside of the DB*. For
316 various reasons this method may not be able to lookup
317 settings from the DB, e.g. during app init. So it can only
318 determine the value per INI files + config defaults.
319 """
320 # set default value, if not already set
321 self.defaults.setdefault(key, value)
323 # get current value, sans db
324 return self.get(key, usedb=False)
326 def get(
327 self,
328 key,
329 default=UNSPECIFIED,
330 require=False,
331 ignore_ambiguous=False,
332 message=None,
333 usedb=None,
334 preferdb=None,
335 session=None,
336 ):
337 """
338 Retrieve a string value from config.
340 .. warning::
342 While the point of this method is to return a *string*
343 value, it is possible for a key to be present in config
344 which corresponds to a "subset" of the config, and not a
345 simple value. For instance with this config file:
347 .. code-block:: ini
349 [foo]
350 bar = 1
351 bar.baz = 2
353 If you invoke ``config.get('foo.bar')`` the return value
354 is somewhat ambiguous. At first glance it should return
355 ``'1'`` - but just as valid would be to return the dict::
357 {'baz': '2'}
359 And similarly, if you invoke ``config.get('foo')`` then
360 the return value "should be" the dict::
362 {'bar': '1',
363 'bar.baz': '2'}
365 Despite all that ambiguity, again the whole point of this
366 method is to return a *string* value, only. Therefore in
367 any case where the return value "should be" a dict, per
368 logic described above, this method will *ignore* that and
369 simply return ``None`` (or rather the ``default`` value).
371 It is important also to understand that in fact, there is
372 no "real" ambiguity per se, but rather a dict (subset)
373 would always get priority over a simple string value. So
374 in the first example above, ``config.get('foo.bar')`` will
375 always return the ``default`` value. The string value
376 ``'1'`` will never be returned since the dict/subset
377 overshadows it, and this method will only return the
378 default value in lieu of any dict.
380 :param key: String key for which value should be returned.
382 :param default: Default value to be returned, if config does
383 not contain the key. If no default is specified, ``None``
384 will be assumed.
386 :param require: If set, an error will be raised if config does
387 not contain the key. If not set, default value is returned
388 (which may be ``None``).
390 Note that it is an error to specify a default value if you
391 also specify ``require=True``.
393 :param ignore_ambiguous: By default this method will log a
394 warning if an ambiguous value is detected (as described
395 above). Pass a true value for this flag to avoid the
396 warnings. Should use with caution, as the warnings are
397 there for a reason.
399 :param message: Optional first part of message to be used,
400 when raising a "value not found" error. If not specified,
401 a default error message will be generated.
403 :param usedb: Flag indicating whether config values should be
404 looked up from the DB. The default for this param is
405 ``None``, in which case the :attr:`usedb` flag determines
406 the behavior.
408 :param preferdb: Flag indicating whether config values from DB
409 should be preferred over values from INI files and/or app
410 defaults. The default for this param is ``None``, in which
411 case the :attr:`preferdb` flag determines the behavior.
413 :param session: Optional SQLAlchemy session to use for DB lookups.
414 NOTE: This param is not yet implemented; currently ignored.
416 :returns: Value as string.
418 """
419 if require and default is not UNSPECIFIED:
420 raise ValueError("must not specify default value when require=True")
422 # should we use/prefer db?
423 if usedb is None:
424 usedb = self.usedb
425 if usedb and preferdb is None:
426 preferdb = self.preferdb
428 # read from db first if so requested
429 if usedb and preferdb:
430 value = self.get_from_db(key, session=session)
431 if value is not None:
432 return value
434 # read from defaults + INI files
435 value = self.configuration.get(key)
436 if value is not None:
438 # nb. if the "value" corresponding to the given key is in
439 # fact a subset/dict of more config values, then we must
440 # "ignore" that. so only return the value if it is *not*
441 # such a config subset.
442 if not isinstance(value, configuration.Configuration):
443 return value
445 if not ignore_ambiguous:
446 log.warning("ambiguous config key '%s' returns: %s", key, value)
448 # read from db last if so requested
449 if usedb and not preferdb:
450 value = self.get_from_db(key, session=session)
451 if value is not None:
452 return value
454 # raise error if required value not found
455 if require:
456 message = message or "missing config"
457 raise ConfigurationError(f"{message}; set value for: {key}")
459 # give the default value if specified
460 if default is not UNSPECIFIED:
461 return default
463 def get_from_db(self, key, session=None):
464 """
465 Retrieve a config value from database settings table.
467 This is a convenience wrapper around
468 :meth:`~wuttjamaican.app.AppHandler.get_setting()`.
469 """
470 app = self.get_app()
471 with app.short_session(session=session) as s:
472 return app.get_setting(s, key)
474 def require(self, *args, **kwargs):
475 """
476 Retrieve a value from config, or raise error if no value can
477 be found. This is just a shortcut, so these work the same::
479 config.get('foo', require=True)
481 config.require('foo')
482 """
483 kwargs['require'] = True
484 return self.get(*args, **kwargs)
486 def get_bool(self, *args, **kwargs):
487 """
488 Retrieve a boolean value from config.
490 Accepts same params as :meth:`get()` but if a value is found,
491 it will be coerced to boolean via :meth:`parse_bool()`.
492 """
493 value = self.get(*args, **kwargs)
494 return self.parse_bool(value)
496 def get_int(self, *args, **kwargs):
497 """
498 Retrieve an integer value from config.
500 Accepts same params as :meth:`get()` but if a value is found,
501 it will be coerced to integer via the :class:`python:int()`
502 constructor.
503 """
504 value = self.get(*args, **kwargs)
505 if value is not None:
506 return int(value)
508 def get_list(self, *args, **kwargs):
509 """
510 Retrieve a list value from config.
512 Accepts same params as :meth:`get()` but if a value is found,
513 it will be coerced to list via :meth:`parse_list()`.
515 :returns: If a value is found, a list is returned. If no
516 value, returns ``None``.
517 """
518 value = self.get(*args, **kwargs)
519 if value is not None:
520 return self.parse_list(value)
522 def get_dict(self, prefix):
523 """
524 Retrieve a particular group of values, as a dictionary.
526 Please note, this will only return values from INI files +
527 defaults. It will *not* return values from DB settings. In
528 other words it assumes ``usedb=False``.
530 For example given this config file:
532 .. code-block:: ini
534 [wutta.db]
535 keys = default, host
536 default.url = sqlite:///tmp/default.sqlite
537 host.url = sqlite:///tmp/host.sqlite
538 host.pool_pre_ping = true
540 One can get the "dict" for SQLAlchemy engine config via::
542 config.get_dict('wutta.db')
544 And the dict would look like::
546 {'keys': 'default, host',
547 'default.url': 'sqlite:///tmp/default.sqlite',
548 'host.url': 'sqlite:///tmp/host.sqlite',
549 'host.pool_pre_ping': 'true'}
551 :param prefix: String prefix corresponding to a subsection of
552 the config.
554 :returns: Dictionary containing the config subsection.
555 """
556 try:
557 values = self.configuration[prefix]
558 except KeyError:
559 return {}
561 return values.as_dict()
563 def parse_bool(self, value):
564 """
565 Convenience wrapper for
566 :func:`wuttjamaican.util.parse_bool()`.
567 """
568 return parse_bool(value)
570 def parse_list(self, value):
571 """
572 Convenience wrapper for
573 :func:`wuttjamaican.util.parse_list()`.
574 """
575 return parse_list(value)
577 def _configure_logging(self):
578 """
579 This will save the current config parser defaults to a
580 temporary file, and use this file to configure Python's
581 standard logging module.
582 """
583 # write current values to file suitable for logging auto-config
584 path = self._write_logging_config_file()
585 try:
586 logging.config.fileConfig(path, disable_existing_loggers=False)
587 except configparser.NoSectionError as error:
588 log.warning("tried to configure logging, but got NoSectionError: %s", error)
589 else:
590 log.debug("configured logging")
591 log.debug("sys.argv: %s", sys.argv)
592 finally:
593 os.remove(path)
595 def _write_logging_config_file(self):
597 # load all current values into configparser
598 parser = configparser.RawConfigParser()
599 for section, values in self.configuration.items():
600 parser.add_section(section)
601 for option, value in values.items():
602 parser.set(section, option, value)
604 # write INI file and return path
605 fd, path = tempfile.mkstemp(suffix='.conf')
606 os.close(fd)
607 with open(path, 'wt') as f:
608 parser.write(f)
609 return path
611 def get_app(self):
612 """
613 Returns the global :class:`~wuttjamaican.app.AppHandler`
614 instance, creating it if necessary.
616 See also :doc:`/narr/handlers/app`.
617 """
618 if not hasattr(self, '_app'):
619 spec = self.get(f'{self.appname}.app.handler', usedb=False,
620 default=self.default_app_handler_spec)
621 factory = load_object(spec)
622 self._app = factory(self)
623 return self._app
625 def get_engine_maker(self):
626 """
627 Returns a callable to be used for constructing SQLAlchemy
628 engines fromc config.
630 Which callable is used depends on
631 :attr:`default_engine_maker_spec` but by default will be
632 :func:`wuttjamaican.db.conf.make_engine_from_config()`.
633 """
634 return load_object(self.default_engine_maker_spec)
636 def production(self):
637 """
638 Returns boolean indicating whether the app is running in
639 production mode.
641 This value may be set e.g. in config file:
643 .. code-block:: ini
645 [wutta]
646 production = true
647 """
648 return self.get_bool(f'{self.appname}.production', default=False)
651class WuttaConfigExtension:
652 """
653 Base class for all :term:`config extensions <config extension>`.
654 """
655 key = None
657 def __repr__(self):
658 return f"WuttaConfigExtension(key={self.key})"
660 def configure(self, config):
661 """
662 Subclass should override this method, to extend the config
663 object in any way necessary.
664 """
666 def startup(self, config):
667 """
668 This method is called after the config object is fully created
669 and all extensions have been applied, i.e. after
670 :meth:`configure()` has been called for each extension.
672 At this point the config *settings* for the running app should
673 be settled, and each extension is then allowed to act on those
674 initial settings if needed.
675 """
678def generic_default_files(appname):
679 """
680 Returns a list of default file paths which might be used for
681 making a config object. This function does not check if the paths
682 actually exist.
684 :param appname: App name to be used as basis for default filenames.
686 :returns: List of default file paths.
687 """
688 if sys.platform == 'win32':
689 # use pywin32 to fetch official defaults
690 try:
691 from win32com.shell import shell, shellcon
692 except ImportError:
693 return []
695 return [
696 # e.g. C:\..?? TODO: what is the user-specific path on win32?
697 os.path.join(shell.SHGetSpecialFolderPath(
698 0, shellcon.CSIDL_APPDATA), appname, f'{appname}.conf'),
699 os.path.join(shell.SHGetSpecialFolderPath(
700 0, shellcon.CSIDL_APPDATA), f'{appname}.conf'),
702 # e.g. C:\ProgramData\wutta\wutta.conf
703 os.path.join(shell.SHGetSpecialFolderPath(
704 0, shellcon.CSIDL_COMMON_APPDATA), appname, f'{appname}.conf'),
705 os.path.join(shell.SHGetSpecialFolderPath(
706 0, shellcon.CSIDL_COMMON_APPDATA), f'{appname}.conf'),
707 ]
709 # default paths for *nix
710 return [
711 f'{sys.prefix}/app/{appname}.conf',
713 os.path.expanduser(f'~/.{appname}/{appname}.conf'),
714 os.path.expanduser(f'~/.{appname}.conf'),
716 f'/usr/local/etc/{appname}/{appname}.conf',
717 f'/usr/local/etc/{appname}.conf',
719 f'/etc/{appname}/{appname}.conf',
720 f'/etc/{appname}.conf',
721 ]
724def get_config_paths(
725 files=None,
726 plus_files=None,
727 appname='wutta',
728 env_files_name=None,
729 env_plus_files_name=None,
730 env=None,
731 default_files=None,
732 winsvc=None):
733 """
734 This function determines which files should ultimately be provided
735 to the config constructor. It is normally called by
736 :func:`make_config()`.
738 In short, the files to be used are determined by typical priority:
740 * function params - ``files`` and ``plus_files``
741 * environment variables - e.g. ``WUTTA_CONFIG_FILES``
742 * app defaults - e.g. :func:`generic_default_files()`
744 The "main" and so-called "plus" config files are dealt with
745 separately, so that "defaults" can be used for the main files, and
746 any "plus" files are then added to the result.
748 In the end it combines everything it finds into a single list.
749 Note that it does not necessarily check to see if these files
750 exist.
752 :param files: Explicit set of "main" config files. If not
753 specified, environment variables and/or default lookup will be
754 done to get the "main" file set. Specify an empty list to
755 force an empty main file set.
757 :param plus_files: Explicit set of "plus" config files. Same
758 rules apply here as for the ``files`` param.
760 :param appname: The "app name" to use as basis for other things -
761 namely, constructing the default config file paths etc. For
762 instance the default ``appname`` value is ``'wutta'`` which
763 leads to default env vars like ``WUTTA_CONFIG_FILES``.
765 :param env_files_name: Name of the environment variable to read,
766 if ``files`` is not specified. The default is
767 ``WUTTA_CONFIG_FILES`` unless you override ``appname``.
769 :param env_plus_files_name: Name of the environment variable to
770 read, if ``plus_files`` is not specified. The default is
771 ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``.
773 :param env: Optional environment dict; if not specified
774 ``os.environ`` is used.
776 :param default_files: Optional lookup for "default" file paths.
778 This is only used a) for the "main" config file lookup (but not
779 "plus" files), and b) if neither ``files`` nor the environment
780 variables yielded anything.
782 If not specified, :func:`generic_default_files()` will be used
783 for the lookup.
785 You may specify a single file path as string, or a list of file
786 paths, or a callable which returns either of those things. For
787 example any of these could be used::
789 mydefaults = '/tmp/something.conf'
791 mydefaults = [
792 '/tmp/something.conf',
793 '/tmp/else.conf',
794 ]
796 def mydefaults(appname):
797 return [
798 f"/tmp/{appname}.conf",
799 f"/tmp/{appname}.ini",
800 ]
802 files = get_config_paths(default_files=mydefaults)
804 :param winsvc: Optional internal name of the Windows service for
805 which the config object is being made.
807 This is only needed for true Windows services running via
808 "Python for Windows Extensions" - which probably only includes
809 the Rattail File Monitor service.
811 In this context there is no way to tell the app which config
812 files to read on startup, so it can only look for "default"
813 files. But by passing a ``winsvc`` name to this function, it
814 will first load the default config file, then read a particular
815 value to determine the "real" config file(s) it should use.
817 So for example on Windows you might have a config file at
818 ``C:\\ProgramData\\rattail\\rattail.conf`` with contents:
820 .. code-block:: ini
822 [rattail.config]
823 winsvc.RattailFileMonitor = C:\\ProgramData\\rattail\\filemon.conf
825 And then ``C:\\ProgramData\\rattail\\filemon.conf`` would have
826 the actual config for the filemon service.
828 When the service starts it calls::
830 make_config(winsvc='RattailFileMonitor')
832 which first reads the ``rattail.conf`` file (since that is the
833 only sensible default), but then per config it knows to swap
834 that out for ``filemon.conf`` at startup. This is because it
835 finds a config value matching the requested service name. The
836 end result is as if it called this instead::
838 make_config(files=[r'C:\\ProgramData\\rattail\\filemon.conf'])
840 :returns: List of file paths.
841 """
842 if env is None:
843 env = os.environ
845 # first identify any "primary" config files
846 if files is None:
847 if not env_files_name:
848 env_files_name = f'{appname.upper()}_CONFIG_FILES'
850 files = env.get(env_files_name)
851 if files is not None:
852 files = files.split(os.pathsep)
854 elif default_files:
855 if callable(default_files):
856 files = default_files(appname) or []
857 elif isinstance(default_files, str):
858 files = [default_files]
859 else:
860 files = list(default_files)
861 files = [path for path in files
862 if os.path.exists(path)]
864 else:
865 files = []
866 for path in generic_default_files(appname):
867 if os.path.exists(path):
868 files.append(path)
870 elif isinstance(files, str):
871 files = [files]
872 else:
873 files = list(files)
875 # then identify any "plus" (config tweak) files
876 if plus_files is None:
877 if not env_plus_files_name:
878 env_plus_files_name = f'{appname.upper()}_CONFIG_PLUS_FILES'
880 plus_files = env.get(env_plus_files_name)
881 if plus_files is not None:
882 plus_files = plus_files.split(os.pathsep)
884 else:
885 plus_files = []
887 elif isinstance(plus_files, str):
888 plus_files = [plus_files]
889 else:
890 plus_files = list(plus_files)
892 # combine all files
893 files.extend(plus_files)
895 # when running as a proper windows service, must first read
896 # "default" file(s) and then consult config to see which file
897 # should "really" be used. because there isn't a way to specify
898 # which config file as part of the actual service definition in
899 # windows, so the service name is used for magic lookup here.
900 if winsvc:
901 config = configparser.ConfigParser()
902 config.read(files)
903 section = f'{appname}.config'
904 if config.has_section(section):
905 option = f'winsvc.{winsvc}'
906 if config.has_option(section, option):
907 # replace file paths with whatever config value says
908 files = parse_list(config.get(section, option))
910 return files
913def make_config(
914 files=None,
915 plus_files=None,
916 appname='wutta',
917 env_files_name=None,
918 env_plus_files_name=None,
919 env=None,
920 default_files=None,
921 winsvc=None,
922 usedb=None,
923 preferdb=None,
924 factory=None,
925 extend=True,
926 extension_entry_points=None,
927 **kwargs):
928 """
929 Make a new config (usually :class:`WuttaConfig`) object,
930 initialized per the given parameters and (usually) further
931 modified by all registered config extensions.
933 This function really does 3 things:
935 * determine the set of config files to use
936 * pass those files to config factory
937 * apply extensions to the resulting config object
939 Some params are described in :func:`get_config_paths()` since they
940 are passed as-is to that function for the first step.
942 :param appname: The :term:`app name` to use as basis for other
943 things - namely, it affects how config files are located. This
944 name is also passed to the config factory at which point it
945 becomes :attr:`~wuttjamaican.conf.WuttaConfig.appname`.
947 :param usedb: Passed to the config factory; becomes
948 :attr:`~wuttjamaican.conf.WuttaConfig.usedb`.
950 :param preferdb: Passed to the config factory; becomes
951 :attr:`~wuttjamaican.conf.WuttaConfig.preferdb`.
953 :param factory: Optional factory to use when making the object.
954 Default factory is :class:`WuttaConfig`.
956 :param extend: Whether to "auto-extend" the config with all
957 registered extensions.
959 As a general rule, ``make_config()`` should only be called
960 once, upon app startup. This is because some of the config
961 extensions may do things which should only happen one time.
962 However if ``extend=False`` is specified, then no extensions
963 are invoked, so this may be done multiple times.
965 (Why anyone would need this, is another question..maybe only
966 useful for tests.)
968 :param extension_entry_points: Name of the ``setuptools`` entry
969 points section, used to identify registered config extensions.
970 The default is ``wutta.config.extensions`` unless you override
971 ``appname``.
973 :returns: The new config object.
974 """
975 # collect file paths
976 files = get_config_paths(
977 files=files,
978 plus_files=plus_files,
979 appname=appname,
980 env_files_name=env_files_name,
981 env_plus_files_name=env_plus_files_name,
982 env=env,
983 default_files=default_files,
984 winsvc=winsvc)
986 # make config object
987 if not factory:
988 factory = WuttaConfig
989 config = factory(files, appname=appname,
990 usedb=usedb, preferdb=preferdb,
991 **kwargs)
993 # maybe extend config object
994 if extend:
995 if not extension_entry_points:
996 extension_entry_points = f'{appname}.config.extensions'
998 # apply all registered extensions
999 # TODO: maybe let config disable some extensions?
1000 extensions = load_entry_points(extension_entry_points)
1001 extensions = [ext() for ext in extensions.values()]
1002 for extension in extensions:
1003 log.debug("applying config extension: %s", extension.key)
1004 extension.configure(config)
1006 # let extensions run startup hooks if needed
1007 for extension in extensions:
1008 extension.startup(config)
1010 return config