Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/conf.py: 100%
238 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 11:33 -0500
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 11:33 -0500
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)
282 os.remove(temp_path)
284 # bring in any "required" files
285 requires = config.get(f'{self.appname}.config.require')
286 if requires:
287 for path in self.parse_list(requires):
288 self._load_ini_configs(path, configs, require=True)
290 # bring in any "included" files
291 includes = config.get(f'{self.appname}.config.include')
292 if includes:
293 for path in self.parse_list(includes):
294 self._load_ini_configs(path, configs, require=False)
296 def get_prioritized_files(self):
297 """
298 Returns list of config files in order of priority.
300 By default, :attr:`files_read` should already be in the
301 correct order, but this is to make things more explicit.
302 """
303 return self.files_read
305 def setdefault(
306 self,
307 key,
308 value):
309 """
310 Establish a default config value for the given key.
312 Note that there is only *one* default value per key. If
313 multiple calls are made with the same key, the first will set
314 the default and subsequent calls have no effect.
316 :returns: The current config value, *outside of the DB*. For
317 various reasons this method may not be able to lookup
318 settings from the DB, e.g. during app init. So it can only
319 determine the value per INI files + config defaults.
320 """
321 # set default value, if not already set
322 self.defaults.setdefault(key, value)
324 # get current value, sans db
325 return self.get(key, usedb=False)
327 def get(
328 self,
329 key,
330 default=UNSPECIFIED,
331 require=False,
332 ignore_ambiguous=False,
333 message=None,
334 usedb=None,
335 preferdb=None,
336 session=None,
337 ):
338 """
339 Retrieve a string value from config.
341 .. warning::
343 While the point of this method is to return a *string*
344 value, it is possible for a key to be present in config
345 which corresponds to a "subset" of the config, and not a
346 simple value. For instance with this config file:
348 .. code-block:: ini
350 [foo]
351 bar = 1
352 bar.baz = 2
354 If you invoke ``config.get('foo.bar')`` the return value
355 is somewhat ambiguous. At first glance it should return
356 ``'1'`` - but just as valid would be to return the dict::
358 {'baz': '2'}
360 And similarly, if you invoke ``config.get('foo')`` then
361 the return value "should be" the dict::
363 {'bar': '1',
364 'bar.baz': '2'}
366 Despite all that ambiguity, again the whole point of this
367 method is to return a *string* value, only. Therefore in
368 any case where the return value "should be" a dict, per
369 logic described above, this method will *ignore* that and
370 simply return ``None`` (or rather the ``default`` value).
372 It is important also to understand that in fact, there is
373 no "real" ambiguity per se, but rather a dict (subset)
374 would always get priority over a simple string value. So
375 in the first example above, ``config.get('foo.bar')`` will
376 always return the ``default`` value. The string value
377 ``'1'`` will never be returned since the dict/subset
378 overshadows it, and this method will only return the
379 default value in lieu of any dict.
381 :param key: String key for which value should be returned.
383 :param default: Default value to be returned, if config does
384 not contain the key. If no default is specified, ``None``
385 will be assumed.
387 :param require: If set, an error will be raised if config does
388 not contain the key. If not set, default value is returned
389 (which may be ``None``).
391 Note that it is an error to specify a default value if you
392 also specify ``require=True``.
394 :param ignore_ambiguous: By default this method will log a
395 warning if an ambiguous value is detected (as described
396 above). Pass a true value for this flag to avoid the
397 warnings. Should use with caution, as the warnings are
398 there for a reason.
400 :param message: Optional first part of message to be used,
401 when raising a "value not found" error. If not specified,
402 a default error message will be generated.
404 :param usedb: Flag indicating whether config values should be
405 looked up from the DB. The default for this param is
406 ``None``, in which case the :attr:`usedb` flag determines
407 the behavior.
409 :param preferdb: Flag indicating whether config values from DB
410 should be preferred over values from INI files and/or app
411 defaults. The default for this param is ``None``, in which
412 case the :attr:`preferdb` flag determines the behavior.
414 :param session: Optional SQLAlchemy session to use for DB lookups.
415 NOTE: This param is not yet implemented; currently ignored.
417 :returns: Value as string.
419 """
420 if require and default is not UNSPECIFIED:
421 raise ValueError("must not specify default value when require=True")
423 # should we use/prefer db?
424 if usedb is None:
425 usedb = self.usedb
426 if usedb and preferdb is None:
427 preferdb = self.preferdb
429 # read from db first if so requested
430 if usedb and preferdb:
431 value = self.get_from_db(key, session=session)
432 if value is not None:
433 return value
435 # read from defaults + INI files
436 value = self.configuration.get(key)
437 if value is not None:
439 # nb. if the "value" corresponding to the given key is in
440 # fact a subset/dict of more config values, then we must
441 # "ignore" that. so only return the value if it is *not*
442 # such a config subset.
443 if not isinstance(value, configuration.Configuration):
444 return value
446 if not ignore_ambiguous:
447 log.warning("ambiguous config key '%s' returns: %s", key, value)
449 # read from db last if so requested
450 if usedb and not preferdb:
451 value = self.get_from_db(key, session=session)
452 if value is not None:
453 return value
455 # raise error if required value not found
456 if require:
457 message = message or "missing config"
458 raise ConfigurationError(f"{message}; set value for: {key}")
460 # give the default value if specified
461 if default is not UNSPECIFIED:
462 return default
464 def get_from_db(self, key, session=None):
465 """
466 Retrieve a config value from database settings table.
468 This is a convenience wrapper around
469 :meth:`~wuttjamaican.app.AppHandler.get_setting()`.
470 """
471 app = self.get_app()
472 with app.short_session(session=session) as s:
473 return app.get_setting(s, key)
475 def require(self, *args, **kwargs):
476 """
477 Retrieve a value from config, or raise error if no value can
478 be found. This is just a shortcut, so these work the same::
480 config.get('foo', require=True)
482 config.require('foo')
483 """
484 kwargs['require'] = True
485 return self.get(*args, **kwargs)
487 def get_bool(self, *args, **kwargs):
488 """
489 Retrieve a boolean value from config.
491 Accepts same params as :meth:`get()` but if a value is found,
492 it will be coerced to boolean via :meth:`parse_bool()`.
493 """
494 value = self.get(*args, **kwargs)
495 return self.parse_bool(value)
497 def get_int(self, *args, **kwargs):
498 """
499 Retrieve an integer value from config.
501 Accepts same params as :meth:`get()` but if a value is found,
502 it will be coerced to integer via the :class:`python:int()`
503 constructor.
504 """
505 value = self.get(*args, **kwargs)
506 if value is not None:
507 return int(value)
509 def get_list(self, *args, **kwargs):
510 """
511 Retrieve a list value from config.
513 Accepts same params as :meth:`get()` but if a value is found,
514 it will be coerced to list via :meth:`parse_list()`.
516 :returns: If a value is found, a list is returned. If no
517 value, returns ``None``.
518 """
519 value = self.get(*args, **kwargs)
520 if value is not None:
521 return self.parse_list(value)
523 def get_dict(self, prefix):
524 """
525 Retrieve a particular group of values, as a dictionary.
527 Please note, this will only return values from INI files +
528 defaults. It will *not* return values from DB settings. In
529 other words it assumes ``usedb=False``.
531 For example given this config file:
533 .. code-block:: ini
535 [wutta.db]
536 keys = default, host
537 default.url = sqlite:///tmp/default.sqlite
538 host.url = sqlite:///tmp/host.sqlite
539 host.pool_pre_ping = true
541 One can get the "dict" for SQLAlchemy engine config via::
543 config.get_dict('wutta.db')
545 And the dict would look like::
547 {'keys': 'default, host',
548 'default.url': 'sqlite:///tmp/default.sqlite',
549 'host.url': 'sqlite:///tmp/host.sqlite',
550 'host.pool_pre_ping': 'true'}
552 :param prefix: String prefix corresponding to a subsection of
553 the config.
555 :returns: Dictionary containing the config subsection.
556 """
557 try:
558 values = self.configuration[prefix]
559 except KeyError:
560 return {}
562 return values.as_dict()
564 def parse_bool(self, value):
565 """
566 Convenience wrapper for
567 :func:`wuttjamaican.util.parse_bool()`.
568 """
569 return parse_bool(value)
571 def parse_list(self, value):
572 """
573 Convenience wrapper for
574 :func:`wuttjamaican.util.parse_list()`.
575 """
576 return parse_list(value)
578 def _configure_logging(self):
579 """
580 This will save the current config parser defaults to a
581 temporary file, and use this file to configure Python's
582 standard logging module.
583 """
584 # write current values to file suitable for logging auto-config
585 path = self._write_logging_config_file()
586 try:
587 logging.config.fileConfig(path, disable_existing_loggers=False)
588 except configparser.NoSectionError as error:
589 log.warning("tried to configure logging, but got NoSectionError: %s", error)
590 else:
591 log.debug("configured logging")
592 log.debug("sys.argv: %s", sys.argv)
593 finally:
594 os.remove(path)
596 def _write_logging_config_file(self):
598 # load all current values into configparser
599 parser = configparser.RawConfigParser()
600 for section, values in self.configuration.items():
601 parser.add_section(section)
602 for option, value in values.items():
603 parser.set(section, option, value)
605 # write INI file and return path
606 fd, path = tempfile.mkstemp(suffix='.conf')
607 os.close(fd)
608 with open(path, 'wt') as f:
609 parser.write(f)
610 return path
612 def get_app(self):
613 """
614 Returns the global :class:`~wuttjamaican.app.AppHandler`
615 instance, creating it if necessary.
617 See also :doc:`/narr/handlers/app`.
618 """
619 if not hasattr(self, '_app'):
620 spec = self.get(f'{self.appname}.app.handler', usedb=False,
621 default=self.default_app_handler_spec)
622 factory = load_object(spec)
623 self._app = factory(self)
624 return self._app
626 def get_engine_maker(self):
627 """
628 Returns a callable to be used for constructing SQLAlchemy
629 engines fromc config.
631 Which callable is used depends on
632 :attr:`default_engine_maker_spec` but by default will be
633 :func:`wuttjamaican.db.conf.make_engine_from_config()`.
634 """
635 return load_object(self.default_engine_maker_spec)
637 def production(self):
638 """
639 Returns boolean indicating whether the app is running in
640 production mode.
642 This value may be set e.g. in config file:
644 .. code-block:: ini
646 [wutta]
647 production = true
648 """
649 return self.get_bool(f'{self.appname}.production', default=False)
652class WuttaConfigExtension:
653 """
654 Base class for all :term:`config extensions <config extension>`.
655 """
656 key = None
658 def __repr__(self):
659 return f"WuttaConfigExtension(key={self.key})"
661 def configure(self, config):
662 """
663 Subclass should override this method, to extend the config
664 object in any way necessary.
665 """
667 def startup(self, config):
668 """
669 This method is called after the config object is fully created
670 and all extensions have been applied, i.e. after
671 :meth:`configure()` has been called for each extension.
673 At this point the config *settings* for the running app should
674 be settled, and each extension is then allowed to act on those
675 initial settings if needed.
676 """
679def generic_default_files(appname):
680 """
681 Returns a list of default file paths which might be used for
682 making a config object. This function does not check if the paths
683 actually exist.
685 :param appname: App name to be used as basis for default filenames.
687 :returns: List of default file paths.
688 """
689 if sys.platform == 'win32':
690 # use pywin32 to fetch official defaults
691 try:
692 from win32com.shell import shell, shellcon
693 except ImportError:
694 return []
696 return [
697 # e.g. C:\..?? TODO: what is the user-specific path on win32?
698 os.path.join(shell.SHGetSpecialFolderPath(
699 0, shellcon.CSIDL_APPDATA), appname, f'{appname}.conf'),
700 os.path.join(shell.SHGetSpecialFolderPath(
701 0, shellcon.CSIDL_APPDATA), f'{appname}.conf'),
703 # e.g. C:\ProgramData\wutta\wutta.conf
704 os.path.join(shell.SHGetSpecialFolderPath(
705 0, shellcon.CSIDL_COMMON_APPDATA), appname, f'{appname}.conf'),
706 os.path.join(shell.SHGetSpecialFolderPath(
707 0, shellcon.CSIDL_COMMON_APPDATA), f'{appname}.conf'),
708 ]
710 # default paths for *nix
711 return [
712 f'{sys.prefix}/app/{appname}.conf',
714 os.path.expanduser(f'~/.{appname}/{appname}.conf'),
715 os.path.expanduser(f'~/.{appname}.conf'),
717 f'/usr/local/etc/{appname}/{appname}.conf',
718 f'/usr/local/etc/{appname}.conf',
720 f'/etc/{appname}/{appname}.conf',
721 f'/etc/{appname}.conf',
722 ]
725def get_config_paths(
726 files=None,
727 plus_files=None,
728 appname='wutta',
729 env_files_name=None,
730 env_plus_files_name=None,
731 env=None,
732 default_files=None,
733 winsvc=None):
734 """
735 This function determines which files should ultimately be provided
736 to the config constructor. It is normally called by
737 :func:`make_config()`.
739 In short, the files to be used are determined by typical priority:
741 * function params - ``files`` and ``plus_files``
742 * environment variables - e.g. ``WUTTA_CONFIG_FILES``
743 * app defaults - e.g. :func:`generic_default_files()`
745 The "main" and so-called "plus" config files are dealt with
746 separately, so that "defaults" can be used for the main files, and
747 any "plus" files are then added to the result.
749 In the end it combines everything it finds into a single list.
750 Note that it does not necessarily check to see if these files
751 exist.
753 :param files: Explicit set of "main" config files. If not
754 specified, environment variables and/or default lookup will be
755 done to get the "main" file set. Specify an empty list to
756 force an empty main file set.
758 :param plus_files: Explicit set of "plus" config files. Same
759 rules apply here as for the ``files`` param.
761 :param appname: The "app name" to use as basis for other things -
762 namely, constructing the default config file paths etc. For
763 instance the default ``appname`` value is ``'wutta'`` which
764 leads to default env vars like ``WUTTA_CONFIG_FILES``.
766 :param env_files_name: Name of the environment variable to read,
767 if ``files`` is not specified. The default is
768 ``WUTTA_CONFIG_FILES`` unless you override ``appname``.
770 :param env_plus_files_name: Name of the environment variable to
771 read, if ``plus_files`` is not specified. The default is
772 ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``.
774 :param env: Optional environment dict; if not specified
775 ``os.environ`` is used.
777 :param default_files: Optional lookup for "default" file paths.
779 This is only used a) for the "main" config file lookup (but not
780 "plus" files), and b) if neither ``files`` nor the environment
781 variables yielded anything.
783 If not specified, :func:`generic_default_files()` will be used
784 for the lookup.
786 You may specify a single file path as string, or a list of file
787 paths, or a callable which returns either of those things. For
788 example any of these could be used::
790 mydefaults = '/tmp/something.conf'
792 mydefaults = [
793 '/tmp/something.conf',
794 '/tmp/else.conf',
795 ]
797 def mydefaults(appname):
798 return [
799 f"/tmp/{appname}.conf",
800 f"/tmp/{appname}.ini",
801 ]
803 files = get_config_paths(default_files=mydefaults)
805 :param winsvc: Optional internal name of the Windows service for
806 which the config object is being made.
808 This is only needed for true Windows services running via
809 "Python for Windows Extensions" - which probably only includes
810 the Rattail File Monitor service.
812 In this context there is no way to tell the app which config
813 files to read on startup, so it can only look for "default"
814 files. But by passing a ``winsvc`` name to this function, it
815 will first load the default config file, then read a particular
816 value to determine the "real" config file(s) it should use.
818 So for example on Windows you might have a config file at
819 ``C:\\ProgramData\\rattail\\rattail.conf`` with contents:
821 .. code-block:: ini
823 [rattail.config]
824 winsvc.RattailFileMonitor = C:\\ProgramData\\rattail\\filemon.conf
826 And then ``C:\\ProgramData\\rattail\\filemon.conf`` would have
827 the actual config for the filemon service.
829 When the service starts it calls::
831 make_config(winsvc='RattailFileMonitor')
833 which first reads the ``rattail.conf`` file (since that is the
834 only sensible default), but then per config it knows to swap
835 that out for ``filemon.conf`` at startup. This is because it
836 finds a config value matching the requested service name. The
837 end result is as if it called this instead::
839 make_config(files=[r'C:\\ProgramData\\rattail\\filemon.conf'])
841 :returns: List of file paths.
842 """
843 if env is None:
844 env = os.environ
846 # first identify any "primary" config files
847 if files is None:
848 if not env_files_name:
849 env_files_name = f'{appname.upper()}_CONFIG_FILES'
851 files = env.get(env_files_name)
852 if files is not None:
853 files = files.split(os.pathsep)
855 elif default_files:
856 if callable(default_files):
857 files = default_files(appname) or []
858 elif isinstance(default_files, str):
859 files = [default_files]
860 else:
861 files = list(default_files)
862 files = [path for path in files
863 if os.path.exists(path)]
865 else:
866 files = []
867 for path in generic_default_files(appname):
868 if os.path.exists(path):
869 files.append(path)
871 elif isinstance(files, str):
872 files = [files]
873 else:
874 files = list(files)
876 # then identify any "plus" (config tweak) files
877 if plus_files is None:
878 if not env_plus_files_name:
879 env_plus_files_name = f'{appname.upper()}_CONFIG_PLUS_FILES'
881 plus_files = env.get(env_plus_files_name)
882 if plus_files is not None:
883 plus_files = plus_files.split(os.pathsep)
885 else:
886 plus_files = []
888 elif isinstance(plus_files, str):
889 plus_files = [plus_files]
890 else:
891 plus_files = list(plus_files)
893 # combine all files
894 files.extend(plus_files)
896 # when running as a proper windows service, must first read
897 # "default" file(s) and then consult config to see which file
898 # should "really" be used. because there isn't a way to specify
899 # which config file as part of the actual service definition in
900 # windows, so the service name is used for magic lookup here.
901 if winsvc:
902 config = configparser.ConfigParser()
903 config.read(files)
904 section = f'{appname}.config'
905 if config.has_section(section):
906 option = f'winsvc.{winsvc}'
907 if config.has_option(section, option):
908 # replace file paths with whatever config value says
909 files = parse_list(config.get(section, option))
911 return files
914def make_config(
915 files=None,
916 plus_files=None,
917 appname='wutta',
918 env_files_name=None,
919 env_plus_files_name=None,
920 env=None,
921 default_files=None,
922 winsvc=None,
923 usedb=None,
924 preferdb=None,
925 factory=None,
926 extend=True,
927 extension_entry_points=None,
928 **kwargs):
929 """
930 Make a new config (usually :class:`WuttaConfig`) object,
931 initialized per the given parameters and (usually) further
932 modified by all registered config extensions.
934 This function really does 3 things:
936 * determine the set of config files to use
937 * pass those files to config factory
938 * apply extensions to the resulting config object
940 Some params are described in :func:`get_config_paths()` since they
941 are passed as-is to that function for the first step.
943 :param appname: The :term:`app name` to use as basis for other
944 things - namely, it affects how config files are located. This
945 name is also passed to the config factory at which point it
946 becomes :attr:`~wuttjamaican.conf.WuttaConfig.appname`.
948 :param usedb: Passed to the config factory; becomes
949 :attr:`~wuttjamaican.conf.WuttaConfig.usedb`.
951 :param preferdb: Passed to the config factory; becomes
952 :attr:`~wuttjamaican.conf.WuttaConfig.preferdb`.
954 :param factory: Optional factory to use when making the object.
955 Default factory is :class:`WuttaConfig`.
957 :param extend: Whether to "auto-extend" the config with all
958 registered extensions.
960 As a general rule, ``make_config()`` should only be called
961 once, upon app startup. This is because some of the config
962 extensions may do things which should only happen one time.
963 However if ``extend=False`` is specified, then no extensions
964 are invoked, so this may be done multiple times.
966 (Why anyone would need this, is another question..maybe only
967 useful for tests.)
969 :param extension_entry_points: Name of the ``setuptools`` entry
970 points section, used to identify registered config extensions.
971 The default is ``wutta.config.extensions`` unless you override
972 ``appname``.
974 :returns: The new config object.
975 """
976 # collect file paths
977 files = get_config_paths(
978 files=files,
979 plus_files=plus_files,
980 appname=appname,
981 env_files_name=env_files_name,
982 env_plus_files_name=env_plus_files_name,
983 env=env,
984 default_files=default_files,
985 winsvc=winsvc)
987 # make config object
988 if not factory:
989 factory = WuttaConfig
990 config = factory(files, appname=appname,
991 usedb=usedb, preferdb=preferdb,
992 **kwargs)
994 # maybe extend config object
995 if extend:
996 if not extension_entry_points:
997 extension_entry_points = f'{appname}.config.extensions'
999 # apply all registered extensions
1000 # TODO: maybe let config disable some extensions?
1001 extensions = load_entry_points(extension_entry_points)
1002 extensions = [ext() for ext in extensions.values()]
1003 for extension in extensions:
1004 log.debug("applying config extension: %s", extension.key)
1005 extension.configure(config)
1007 # let extensions run startup hooks if needed
1008 for extension in extensions:
1009 extension.startup(config)
1011 return config