2008-12-30 17:25:42 +01:00
|
|
|
# This file is part of Radicale Server - Calendar Server
|
2017-05-27 17:28:07 +02:00
|
|
|
# Copyright © 2008-2017 Guillaume Ayoub
|
2009-07-27 17:04:54 +02:00
|
|
|
# Copyright © 2008 Nicolas Kandel
|
|
|
|
# Copyright © 2008 Pascal Halter
|
2019-06-17 04:13:25 +02:00
|
|
|
# Copyright © 2017-2019 Unrud <unrud@outlook.com>
|
2008-12-30 17:25:42 +01:00
|
|
|
#
|
|
|
|
# This library is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This library is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2009-07-27 17:04:54 +02:00
|
|
|
"""
|
2020-01-12 23:32:28 +01:00
|
|
|
Configuration module
|
2009-07-27 17:04:54 +02:00
|
|
|
|
2020-01-12 23:32:28 +01:00
|
|
|
Use ``load()`` to obtain an instance of ``Configuration`` for use with
|
|
|
|
``radicale.app.Application``.
|
2010-02-10 18:57:21 +01:00
|
|
|
|
2009-07-27 17:04:54 +02:00
|
|
|
"""
|
|
|
|
|
2020-02-19 09:50:25 +01:00
|
|
|
import contextlib
|
2017-06-02 12:42:19 +02:00
|
|
|
import math
|
2010-01-18 10:48:06 +01:00
|
|
|
import os
|
2016-10-11 18:17:01 +02:00
|
|
|
from collections import OrderedDict
|
2019-06-17 04:13:25 +02:00
|
|
|
from configparser import RawConfigParser
|
2008-12-30 17:25:42 +01:00
|
|
|
|
2018-01-14 18:33:01 +01:00
|
|
|
from radicale import auth, rights, storage, web
|
2019-06-17 04:13:25 +02:00
|
|
|
|
|
|
|
DEFAULT_CONFIG_PATH = os.pathsep.join([
|
|
|
|
"?/etc/radicale/config",
|
|
|
|
"?~/.config/radicale/config"])
|
2017-06-21 09:48:57 +02:00
|
|
|
|
2017-06-02 12:42:19 +02:00
|
|
|
|
|
|
|
def positive_int(value):
|
|
|
|
value = int(value)
|
|
|
|
if value < 0:
|
|
|
|
raise ValueError("value is negative: %d" % value)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def positive_float(value):
|
|
|
|
value = float(value)
|
|
|
|
if not math.isfinite(value):
|
|
|
|
raise ValueError("value is infinite")
|
|
|
|
if math.isnan(value):
|
|
|
|
raise ValueError("value is not a number")
|
|
|
|
if value < 0:
|
|
|
|
raise ValueError("value is negative: %f" % value)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2018-08-16 08:00:02 +02:00
|
|
|
def logging_level(value):
|
|
|
|
if value not in ("debug", "info", "warning", "error", "critical"):
|
2019-06-17 04:13:25 +02:00
|
|
|
raise ValueError("unsupported level: %r" % value)
|
2018-08-16 08:00:02 +02:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
2019-06-17 04:13:25 +02:00
|
|
|
def filepath(value):
|
|
|
|
if not value:
|
|
|
|
return ""
|
|
|
|
value = os.path.expanduser(value)
|
|
|
|
if os.name == "nt":
|
|
|
|
value = os.path.expandvars(value)
|
|
|
|
return os.path.abspath(value)
|
|
|
|
|
|
|
|
|
|
|
|
def list_of_ip_address(value):
|
|
|
|
def ip_address(value):
|
|
|
|
try:
|
|
|
|
address, port = value.strip().rsplit(":", 1)
|
|
|
|
return address.strip("[] "), int(port)
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError("malformed IP address: %r" % value)
|
|
|
|
return [ip_address(s.strip()) for s in value.split(",")]
|
|
|
|
|
|
|
|
|
|
|
|
def _convert_to_bool(value):
|
|
|
|
if value.lower() not in RawConfigParser.BOOLEAN_STATES:
|
|
|
|
raise ValueError("Not a boolean: %r" % value)
|
|
|
|
return RawConfigParser.BOOLEAN_STATES[value.lower()]
|
|
|
|
|
|
|
|
|
2010-02-10 18:57:21 +01:00
|
|
|
# Default configuration
|
2019-06-17 04:13:25 +02:00
|
|
|
DEFAULT_CONFIG_SCHEMA = OrderedDict([
|
2016-10-11 18:17:01 +02:00
|
|
|
("server", OrderedDict([
|
2016-10-12 14:30:18 +02:00
|
|
|
("hosts", {
|
2020-02-19 09:49:56 +01:00
|
|
|
"value": "localhost:5232",
|
2016-10-12 14:30:18 +02:00
|
|
|
"help": "set server hostnames including ports",
|
2017-05-31 11:08:32 +02:00
|
|
|
"aliases": ["-H", "--hosts"],
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": list_of_ip_address}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("max_connections", {
|
2018-08-28 16:19:48 +02:00
|
|
|
"value": "8",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "maximum number of parallel connections",
|
2017-06-02 12:42:19 +02:00
|
|
|
"type": positive_int}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("max_content_length", {
|
2018-04-29 21:20:23 +02:00
|
|
|
"value": "100000000",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "maximum size of request body in bytes",
|
2017-06-02 12:42:19 +02:00
|
|
|
"type": positive_int}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("timeout", {
|
2018-04-29 21:20:23 +02:00
|
|
|
"value": "30",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "socket timeout",
|
2017-06-02 12:42:19 +02:00
|
|
|
"type": positive_int}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("ssl", {
|
|
|
|
"value": "False",
|
|
|
|
"help": "use SSL connection",
|
|
|
|
"aliases": ["-s", "--ssl"],
|
2017-05-31 11:08:32 +02:00
|
|
|
"opposite": ["-S", "--no-ssl"],
|
|
|
|
"type": bool}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("certificate", {
|
2017-03-04 14:06:09 +01:00
|
|
|
"value": "/etc/ssl/radicale.cert.pem",
|
2016-10-12 14:30:18 +02:00
|
|
|
"help": "set certificate file",
|
2017-05-31 11:08:32 +02:00
|
|
|
"aliases": ["-c", "--certificate"],
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": filepath}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("key", {
|
2017-03-04 14:06:09 +01:00
|
|
|
"value": "/etc/ssl/radicale.key.pem",
|
2016-10-12 14:30:18 +02:00
|
|
|
"help": "set private key file",
|
2017-05-31 11:08:32 +02:00
|
|
|
"aliases": ["-k", "--key"],
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": filepath}),
|
2017-06-02 12:41:03 +02:00
|
|
|
("certificate_authority", {
|
|
|
|
"value": "",
|
|
|
|
"help": "set CA certificate for validating clients",
|
|
|
|
"aliases": ["--certificate-authority"],
|
2020-02-19 09:48:38 +01:00
|
|
|
"type": filepath})])),
|
2016-10-11 18:17:01 +02:00
|
|
|
("encoding", OrderedDict([
|
2016-10-12 14:30:18 +02:00
|
|
|
("request", {
|
|
|
|
"value": "utf-8",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "encoding for responding requests",
|
|
|
|
"type": str}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("stock", {
|
|
|
|
"value": "utf-8",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "encoding for storing local collections",
|
|
|
|
"type": str})])),
|
2016-10-11 18:17:01 +02:00
|
|
|
("auth", OrderedDict([
|
2016-10-12 14:30:18 +02:00
|
|
|
("type", {
|
2017-06-02 12:43:23 +02:00
|
|
|
"value": "none",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "authentication method",
|
2017-06-21 09:48:57 +02:00
|
|
|
"type": str,
|
|
|
|
"internal": auth.INTERNAL_TYPES}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("htpasswd_filename", {
|
|
|
|
"value": "/etc/radicale/users",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "htpasswd filename",
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": filepath}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("htpasswd_encryption", {
|
2020-01-19 18:26:14 +01:00
|
|
|
"value": "md5",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "htpasswd encryption method",
|
|
|
|
"type": str}),
|
2018-08-16 08:00:01 +02:00
|
|
|
("realm", {
|
|
|
|
"value": "Radicale - Password Required",
|
|
|
|
"help": "message displayed when a password is needed",
|
|
|
|
"type": str}),
|
2017-05-23 03:11:41 +02:00
|
|
|
("delay", {
|
|
|
|
"value": "1",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "incorrect authentication delay",
|
2017-06-02 12:42:19 +02:00
|
|
|
"type": positive_float})])),
|
2016-10-11 18:17:01 +02:00
|
|
|
("rights", OrderedDict([
|
2016-10-12 14:30:18 +02:00
|
|
|
("type", {
|
2017-03-04 14:06:09 +01:00
|
|
|
"value": "owner_only",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "rights backend",
|
2017-06-21 09:48:57 +02:00
|
|
|
"type": str,
|
|
|
|
"internal": rights.INTERNAL_TYPES}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("file", {
|
2017-03-04 14:06:09 +01:00
|
|
|
"value": "/etc/radicale/rights",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "file for rights management from_file",
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": filepath})])),
|
2016-10-11 18:17:01 +02:00
|
|
|
("storage", OrderedDict([
|
2016-10-12 14:30:18 +02:00
|
|
|
("type", {
|
|
|
|
"value": "multifilesystem",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "storage backend",
|
2017-06-21 09:48:57 +02:00
|
|
|
"type": str,
|
|
|
|
"internal": storage.INTERNAL_TYPES}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("filesystem_folder", {
|
2019-06-17 04:13:25 +02:00
|
|
|
"value": "/var/lib/radicale/collections",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "path where collections are stored",
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": filepath}),
|
2017-06-02 12:44:39 +02:00
|
|
|
("max_sync_token_age", {
|
2018-09-09 14:58:43 +02:00
|
|
|
"value": "2592000", # 30 days
|
2017-06-02 12:44:39 +02:00
|
|
|
"help": "delete sync token that are older",
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": positive_int}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("hook", {
|
|
|
|
"value": "",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "command that is run after changes to storage",
|
|
|
|
"type": str})])),
|
2017-05-31 13:18:40 +02:00
|
|
|
("web", OrderedDict([
|
|
|
|
("type", {
|
2017-05-31 13:18:42 +02:00
|
|
|
"value": "internal",
|
2017-05-31 13:18:40 +02:00
|
|
|
"help": "web interface backend",
|
2017-06-21 09:48:57 +02:00
|
|
|
"type": str,
|
|
|
|
"internal": web.INTERNAL_TYPES})])),
|
2016-10-11 18:17:01 +02:00
|
|
|
("logging", OrderedDict([
|
2018-08-16 08:00:02 +02:00
|
|
|
("level", {
|
|
|
|
"value": "warning",
|
|
|
|
"help": "threshold for the logger",
|
|
|
|
"type": logging_level}),
|
2016-10-12 14:30:18 +02:00
|
|
|
("mask_passwords", {
|
|
|
|
"value": "True",
|
2017-05-31 11:08:32 +02:00
|
|
|
"help": "mask passwords in logs",
|
2019-06-17 04:13:25 +02:00
|
|
|
"type": bool})])),
|
|
|
|
("headers", OrderedDict([
|
|
|
|
("_allow_extra", True)])),
|
|
|
|
("internal", OrderedDict([
|
|
|
|
("_internal", True),
|
|
|
|
("filesystem_fsync", {
|
|
|
|
"value": "True",
|
|
|
|
"help": "sync all changes to filesystem during requests",
|
|
|
|
"type": bool}),
|
|
|
|
("internal_server", {
|
|
|
|
"value": "False",
|
|
|
|
"help": "the internal server is used",
|
2017-05-31 11:08:32 +02:00
|
|
|
"type": bool})]))])
|
2019-06-17 04:13:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
def parse_compound_paths(*compound_paths):
|
|
|
|
"""Parse a compound path and return the individual paths.
|
|
|
|
Paths in a compound path are joined by ``os.pathsep``. If a path starts
|
|
|
|
with ``?`` the return value ``IGNORE_IF_MISSING`` is set.
|
|
|
|
|
|
|
|
When multiple ``compound_paths`` are passed, the last argument that is
|
|
|
|
not ``None`` is used.
|
|
|
|
|
|
|
|
Returns a dict of the format ``[(PATH, IGNORE_IF_MISSING), ...]``
|
|
|
|
|
|
|
|
"""
|
|
|
|
compound_path = ""
|
|
|
|
for p in compound_paths:
|
|
|
|
if p is not None:
|
|
|
|
compound_path = p
|
|
|
|
paths = []
|
|
|
|
for path in compound_path.split(os.pathsep):
|
|
|
|
ignore_if_missing = path.startswith("?")
|
|
|
|
if ignore_if_missing:
|
|
|
|
path = path[1:]
|
|
|
|
path = filepath(path)
|
|
|
|
if path:
|
|
|
|
paths.append((path, ignore_if_missing))
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
|
|
def load(paths=()):
|
2020-01-12 23:32:28 +01:00
|
|
|
"""
|
|
|
|
Create instance of ``Configuration`` for use with
|
|
|
|
``radicale.app.Application``.
|
|
|
|
|
|
|
|
``paths`` a list of configuration files with the format
|
|
|
|
``[(PATH, IGNORE_IF_MISSING), ...]``.
|
|
|
|
If a configuration file is missing and IGNORE_IF_MISSING is set, the
|
|
|
|
config is set to ``Configuration.SOURCE_MISSING``.
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-01-12 23:32:28 +01:00
|
|
|
The configuration can later be changed with ``Configuration.update()``.
|
2019-06-17 04:13:25 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
configuration = Configuration(DEFAULT_CONFIG_SCHEMA)
|
|
|
|
for path, ignore_if_missing in paths:
|
|
|
|
parser = RawConfigParser()
|
|
|
|
config_source = "config file %r" % path
|
|
|
|
try:
|
|
|
|
if not parser.read(path):
|
|
|
|
config = Configuration.SOURCE_MISSING
|
|
|
|
if not ignore_if_missing:
|
2017-05-31 11:08:32 +02:00
|
|
|
raise RuntimeError("No such file: %r" % path)
|
2019-06-17 04:13:25 +02:00
|
|
|
else:
|
|
|
|
config = {s: {o: parser[s][o] for o in parser.options(s)}
|
|
|
|
for s in parser.sections()}
|
|
|
|
except Exception as e:
|
|
|
|
raise RuntimeError(
|
|
|
|
"Failed to load %s: %s" % (config_source, e)) from e
|
2020-01-12 23:32:26 +01:00
|
|
|
configuration.update(config, config_source)
|
2019-06-17 04:13:25 +02:00
|
|
|
return configuration
|
|
|
|
|
|
|
|
|
|
|
|
class Configuration:
|
|
|
|
SOURCE_MISSING = {}
|
|
|
|
|
|
|
|
def __init__(self, schema):
|
|
|
|
"""Initialize configuration.
|
|
|
|
|
|
|
|
``schema`` a dict that describes the configuration format.
|
|
|
|
See ``DEFAULT_CONFIG_SCHEMA``.
|
2020-01-13 15:51:10 +01:00
|
|
|
The content of ``schema`` must not change afterwards, it is kept
|
|
|
|
as an internal reference.
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-01-12 23:32:28 +01:00
|
|
|
Use ``load()`` to create an instance for use with
|
|
|
|
``radicale.app.Application``.
|
|
|
|
|
2019-06-17 04:13:25 +02:00
|
|
|
"""
|
|
|
|
self._schema = schema
|
|
|
|
self._values = {}
|
|
|
|
self._configs = []
|
|
|
|
values = {}
|
|
|
|
for section in schema:
|
|
|
|
values[section] = {}
|
|
|
|
for option in schema[section]:
|
|
|
|
if option.startswith("_"):
|
2017-06-21 09:48:57 +02:00
|
|
|
continue
|
2019-06-17 04:13:25 +02:00
|
|
|
values[section][option] = schema[section][option]["value"]
|
2020-01-12 23:32:26 +01:00
|
|
|
self.update(values, "default config", internal=True)
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-01-15 03:19:44 +01:00
|
|
|
def update(self, config, source=None, internal=False):
|
2019-06-17 04:13:25 +02:00
|
|
|
"""Update the configuration.
|
|
|
|
|
|
|
|
``config`` a dict of the format {SECTION: {OPTION: VALUE, ...}, ...}.
|
2020-01-12 23:32:28 +01:00
|
|
|
The configuration is checked for errors according to the config schema.
|
2020-01-13 15:51:10 +01:00
|
|
|
The content of ``config`` must not change afterwards, it is kept
|
|
|
|
as an internal reference.
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-01-12 23:32:28 +01:00
|
|
|
``source`` a description of the configuration source (used in error
|
|
|
|
messages).
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-01-12 23:32:28 +01:00
|
|
|
``internal`` allows updating "_internal" sections.
|
2019-06-17 04:13:25 +02:00
|
|
|
|
|
|
|
"""
|
2020-01-15 03:19:44 +01:00
|
|
|
source = source or "unspecified config"
|
2019-06-17 04:13:25 +02:00
|
|
|
new_values = {}
|
|
|
|
for section in config:
|
|
|
|
if (section not in self._schema or not internal and
|
|
|
|
self._schema[section].get("_internal", False)):
|
2017-05-31 11:08:32 +02:00
|
|
|
raise RuntimeError(
|
2019-06-17 04:13:25 +02:00
|
|
|
"Invalid section %r in %s" % (section, source))
|
|
|
|
new_values[section] = {}
|
|
|
|
if "_allow_extra" in self._schema[section]:
|
|
|
|
allow_extra_options = self._schema[section]["_allow_extra"]
|
|
|
|
elif "type" in self._schema[section]:
|
|
|
|
if "type" in config[section]:
|
|
|
|
plugin_type = config[section]["type"]
|
|
|
|
else:
|
|
|
|
plugin_type = self.get(section, "type")
|
|
|
|
allow_extra_options = plugin_type not in self._schema[section][
|
|
|
|
"type"].get("internal", [])
|
|
|
|
else:
|
|
|
|
allow_extra_options = False
|
|
|
|
for option in config[section]:
|
|
|
|
if option in self._schema[section]:
|
|
|
|
type_ = self._schema[section][option]["type"]
|
|
|
|
elif allow_extra_options:
|
|
|
|
type_ = str
|
|
|
|
else:
|
|
|
|
raise RuntimeError("Invalid option %r in section %r in "
|
|
|
|
"%s" % (option, section, source))
|
|
|
|
raw_value = config[section][option]
|
|
|
|
try:
|
2020-01-17 12:45:01 +01:00
|
|
|
if type_ == bool and not isinstance(raw_value, bool):
|
2019-06-17 04:13:25 +02:00
|
|
|
raw_value = _convert_to_bool(raw_value)
|
|
|
|
new_values[section][option] = type_(raw_value)
|
|
|
|
except Exception as e:
|
|
|
|
raise RuntimeError(
|
|
|
|
"Invalid %s value for option %r in section %r in %s: "
|
|
|
|
"%r" % (type_.__name__, option, section, source,
|
|
|
|
raw_value)) from e
|
2020-01-15 00:30:27 +01:00
|
|
|
self._configs.append((config, source, bool(internal)))
|
2019-06-17 04:13:25 +02:00
|
|
|
for section in new_values:
|
|
|
|
if section not in self._values:
|
|
|
|
self._values[section] = {}
|
|
|
|
for option in new_values[section]:
|
|
|
|
self._values[section][option] = new_values[section][option]
|
|
|
|
|
|
|
|
def get(self, section, option):
|
|
|
|
"""Get the value of ``option`` in ``section``."""
|
2020-02-19 09:50:25 +01:00
|
|
|
with contextlib.suppress(KeyError):
|
|
|
|
return self._values[section][option]
|
|
|
|
raise KeyError(section, option)
|
2019-06-17 04:13:25 +02:00
|
|
|
|
|
|
|
def get_raw(self, section, option):
|
|
|
|
"""Get the raw value of ``option`` in ``section``."""
|
|
|
|
for config, _, _ in reversed(self._configs):
|
2020-02-19 09:50:25 +01:00
|
|
|
if option in config.get(section, {}):
|
|
|
|
return config[section][option]
|
|
|
|
raise KeyError(section, option)
|
2019-06-17 04:13:25 +02:00
|
|
|
|
2020-02-19 09:50:19 +01:00
|
|
|
def get_source(self, section, option):
|
|
|
|
"""Get the source that provides ``option`` in ``section``."""
|
|
|
|
for config, source, _ in reversed(self._configs):
|
|
|
|
if option in config.get(section, {}):
|
|
|
|
return source
|
|
|
|
raise KeyError(section, option)
|
|
|
|
|
2019-06-17 04:13:25 +02:00
|
|
|
def sections(self):
|
|
|
|
"""List all sections."""
|
|
|
|
return self._values.keys()
|
|
|
|
|
|
|
|
def options(self, section):
|
|
|
|
"""List all options in ``section``"""
|
|
|
|
return self._values[section].keys()
|
|
|
|
|
2020-02-19 09:48:42 +01:00
|
|
|
def sources(self):
|
|
|
|
"""List all config sources."""
|
|
|
|
return [(source, config is self.SOURCE_MISSING) for
|
|
|
|
config, source, _ in self._configs]
|
|
|
|
|
2019-06-17 04:13:25 +02:00
|
|
|
def copy(self, plugin_schema=None):
|
|
|
|
"""Create a copy of the configuration
|
|
|
|
|
|
|
|
``plugin_schema`` is a optional dict that contains additional options
|
|
|
|
for usage with a plugin. See ``DEFAULT_CONFIG_SCHEMA``.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if plugin_schema is None:
|
|
|
|
schema = self._schema
|
|
|
|
else:
|
|
|
|
schema = self._schema.copy()
|
|
|
|
for section, options in plugin_schema.items():
|
|
|
|
if (section not in schema or "type" not in schema[section] or
|
|
|
|
"internal" not in schema[section]["type"]):
|
|
|
|
raise ValueError("not a plugin section: %r" % section)
|
|
|
|
schema[section] = schema[section].copy()
|
|
|
|
schema[section]["type"] = schema[section]["type"].copy()
|
|
|
|
schema[section]["type"]["internal"] = [
|
|
|
|
self.get(section, "type")]
|
|
|
|
for option, value in options.items():
|
|
|
|
if option in schema[section]:
|
|
|
|
raise ValueError("option already exists in %r: %r" % (
|
|
|
|
section, option))
|
|
|
|
schema[section][option] = value
|
2020-01-12 23:32:27 +01:00
|
|
|
copy = type(self)(schema)
|
|
|
|
for config, source, internal in self._configs:
|
|
|
|
copy.update(config, source, internal)
|
2019-06-17 04:13:25 +02:00
|
|
|
return copy
|