RFL.settings 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.1
2
+ Name: RFL.settings
3
+ Version: 1.0.0
4
+ Summary: Rackslab Foundation Library: settings package
5
+ Author-email: Rémi Palancher <remi@rackslab.io>
6
+ License: GPLv3+
7
+ Project-URL: Homepage, https://github.com/rackslab/RFL
8
+ Project-URL: Bug Tracker, https://github.com/rackslab/RFL/issues
9
+ Keywords: utility,basic
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Systems Administration
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: RFL.core
19
+ Requires-Dist: PyYAML
20
+
21
+ # RFL: settings package
22
+
23
+ Manage configuration files settings.
@@ -0,0 +1,3 @@
1
+ # RFL: settings package
2
+
3
+ Manage configuration files settings.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.1
2
+ Name: RFL.settings
3
+ Version: 1.0.0
4
+ Summary: Rackslab Foundation Library: settings package
5
+ Author-email: Rémi Palancher <remi@rackslab.io>
6
+ License: GPLv3+
7
+ Project-URL: Homepage, https://github.com/rackslab/RFL
8
+ Project-URL: Bug Tracker, https://github.com/rackslab/RFL/issues
9
+ Keywords: utility,basic
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Systems Administration
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: RFL.core
19
+ Requires-Dist: PyYAML
20
+
21
+ # RFL: settings package
22
+
23
+ Manage configuration files settings.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ RFL.settings.egg-info/PKG-INFO
4
+ RFL.settings.egg-info/SOURCES.txt
5
+ RFL.settings.egg-info/dependency_links.txt
6
+ RFL.settings.egg-info/requires.txt
7
+ RFL.settings.egg-info/top_level.txt
8
+ rfl/settings/__init__.py
9
+ rfl/settings/definition.py
10
+ rfl/settings/errors.py
11
+ rfl/settings/loaders.py
12
+ rfl/tests/__init__.py
13
+ rfl/tests/test_settings.py
@@ -0,0 +1,2 @@
1
+ RFL.core
2
+ PyYAML
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "RFL.settings"
7
+ version = "1.0.0"
8
+ description = "Rackslab Foundation Library: settings package"
9
+ license = {text = "GPLv3+"}
10
+ requires-python = ">=3.6"
11
+ keywords = ["utility", "basic"]
12
+ authors = [
13
+ {name = "Rémi Palancher", email = "remi@rackslab.io"},
14
+ ]
15
+ dependencies = [
16
+ "RFL.core",
17
+ "PyYAML",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Environment :: Console",
22
+ "Intended Audience :: System Administrators",
23
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
24
+ "Programming Language :: Python :: 3",
25
+ "Topic :: System :: Systems Administration",
26
+ ]
27
+ readme = "README.md"
28
+
29
+
30
+ [project.urls]
31
+ "Homepage" = "https://github.com/rackslab/RFL"
32
+ "Bug Tracker" = "https://github.com/rackslab/RFL/issues"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["rfl.settings*"]
@@ -0,0 +1,151 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ from pathlib import Path
8
+ import urllib
9
+
10
+ import logging
11
+
12
+ from .definition import (
13
+ SettingsDefinition,
14
+ SettingsSectionDefinition,
15
+ SettingsParameterDefinition,
16
+ SettingsDefinitionLoaderYaml,
17
+ )
18
+ from .loaders import RuntimeSettingsSiteLoader, RuntimeSettingsSiteLoaderIni
19
+ from .errors import SettingsOverrideError
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class RuntimeSettingsSection:
25
+ def __init__(self, section: SettingsSectionDefinition):
26
+ self._name = section.name
27
+ for parameter in section.parameters:
28
+ setattr(self, parameter.name, parameter.default)
29
+
30
+
31
+ class RuntimeSettings:
32
+ def __init__(self, definition: SettingsDefinition):
33
+ # set to hold all sections names
34
+ self._sections = set()
35
+ self._definition = definition
36
+ for section in definition.sections:
37
+ o_section = type(
38
+ f"{self.__class__.__name__}{section.name.capitalize()}",
39
+ (RuntimeSettingsSection,),
40
+ dict(),
41
+ )(section)
42
+ setattr(self, o_section._name, o_section)
43
+ self._sections.add(o_section._name)
44
+
45
+ def override(self, loader: RuntimeSettingsSiteLoader):
46
+ # load site ini file and check it matches definition
47
+ for section_name in loader.content.sections():
48
+ if not self._definition.has_section(section_name):
49
+ raise SettingsOverrideError(
50
+ f"Section {section_name} loaded in settings overrides is not "
51
+ "defined in settings definition"
52
+ )
53
+ section = self._definition.section(section_name)
54
+ for parameter_name in loader.content[section.name]:
55
+ if not section.has_parameter(parameter_name):
56
+ raise SettingsOverrideError(
57
+ f"Parameter {parameter_name} loaded in settings overrides is "
58
+ f"not defined in section {section.name} of settings definition"
59
+ )
60
+ parameter = section.parameter(parameter_name)
61
+ self._override_parameter(loader, section, parameter)
62
+
63
+ # check required parameters
64
+ self._check_required()
65
+
66
+ def _check_required(self):
67
+ """Verify that all parameters declared as required in settings definition are
68
+ properly defined with a real value or raise SettingsOverrideError."""
69
+ for section in self._definition.sections:
70
+ for parameter in section.parameters:
71
+ if (
72
+ parameter.required
73
+ and getattr(getattr(self, parameter.section), parameter.name)
74
+ is None
75
+ ):
76
+ raise SettingsOverrideError(
77
+ f"Parameter {str(parameter)} is missing but required in "
78
+ "settings overrides"
79
+ )
80
+
81
+ def _override_parameter(
82
+ self,
83
+ loader: RuntimeSettingsSiteLoader,
84
+ section: SettingsSectionDefinition,
85
+ parameter: SettingsParameterDefinition,
86
+ ) -> None:
87
+ """Load value from INI file, convert it to expected type, verify it is a valid
88
+ choice and set corresponding object attribute. Special logic is applied for list
89
+ to iterate over the items."""
90
+ raw = loader.content[section.name].get(parameter.name)
91
+ if parameter._type == "list":
92
+ value = [
93
+ self._load_parameter_value(parameter, parameter.content, raw)
94
+ for raw in raw.strip().split("\n")
95
+ ]
96
+ for item in value:
97
+ self._validate_choice(parameter, item)
98
+ else:
99
+ value = self._load_parameter_value(parameter, parameter._type, raw)
100
+ self._validate_choice(parameter, value)
101
+ setattr(getattr(self, section.name), parameter.name, value)
102
+
103
+ def _load_parameter_value(self, parameter, _type: str, raw: str):
104
+ """Try to convert raw value to expected type or raise SettingsOverrideError."""
105
+ if _type == "path":
106
+ # Converting a string to a pathlib.Path basically never fails.
107
+ return Path(raw)
108
+ elif _type == "uri":
109
+ # Converting a string to a urllib.ParseResult basically never fails.
110
+ return urllib.parse.urlparse(raw)
111
+ elif _type == "int":
112
+ try:
113
+ return int(raw)
114
+ except ValueError as err:
115
+ raise SettingsOverrideError(
116
+ f"Invalid integer value '{raw}' for {parameter} in site overrides"
117
+ ) from err
118
+ elif _type == "float":
119
+ try:
120
+ return float(raw)
121
+ except ValueError as err:
122
+ raise SettingsOverrideError(
123
+ f"Invalid float value '{raw}' for {parameter} in site overrides"
124
+ ) from err
125
+ elif _type == "bool":
126
+ if raw.lower() in ["1", "yes", "true", "on"]:
127
+ return True
128
+ elif raw.lower() in ["0", "no", "false", "off"]:
129
+ return False
130
+ else:
131
+ raise SettingsOverrideError(
132
+ f"Invalid boolean value '{raw}' for {parameter} in site overrides"
133
+ )
134
+ else:
135
+ # Return unmodified value
136
+ return raw
137
+
138
+ def _validate_choice(self, parameter, value):
139
+ """Validate value is in parameter choices or raise SettingsOverrideError."""
140
+ if parameter.choices is not None and value not in parameter.choices:
141
+ raise SettingsOverrideError(
142
+ f"Value {value} for parameter {parameter} in site overrides is "
143
+ f"not one of possible choices {parameter.choices}"
144
+ )
145
+
146
+ def override_ini(self, path: Path) -> None:
147
+ self.override(RuntimeSettingsSiteLoaderIni(path=path))
148
+
149
+ @classmethod
150
+ def yaml_definition(cls, path: Path):
151
+ return cls(SettingsDefinition(SettingsDefinitionLoaderYaml(path=path)))
@@ -0,0 +1,201 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ from pathlib import Path
8
+ import urllib
9
+
10
+ import yaml
11
+
12
+ from .errors import SettingsDefinitionError
13
+
14
+
15
+ class SettingsDefinitionLoader:
16
+ pass
17
+
18
+
19
+ class SettingsDefinitionLoaderYaml(SettingsDefinitionLoader):
20
+ def __init__(self, raw: str = None, path: Path = None):
21
+ try:
22
+ if raw is not None:
23
+ self.content = yaml.safe_load(raw)
24
+ elif path is not None:
25
+ try:
26
+ with open(path) as fh:
27
+ self.content = yaml.safe_load(fh)
28
+ except FileNotFoundError as err:
29
+ raise SettingsDefinitionError(
30
+ f"Settings definition file {path} not found"
31
+ ) from err
32
+ else:
33
+ raise SettingsDefinitionError(
34
+ "Either a raw string value or a path must be given to load YAML "
35
+ "settings definition"
36
+ )
37
+ except yaml.parser.ParserError as err:
38
+ raise SettingsDefinitionError(
39
+ f"Invalid YAML settings definition: {str(err)}"
40
+ ) from err
41
+ except yaml.scanner.ScannerError as err:
42
+ raise SettingsDefinitionError(f"YAML scanner error: {str(err)}") from err
43
+
44
+
45
+ class SettingsParameterDefinition:
46
+ # Map properties that can be found in definition file with corresponding
47
+ # object attributes names.
48
+ POSSIBLE_PROPERTIES = {
49
+ "type": "_type",
50
+ "content": "content",
51
+ "default": "default",
52
+ "choices": "choices",
53
+ "doc": "doc",
54
+ "ex": "example",
55
+ "required": "required",
56
+ }
57
+ EXPECTED_TYPES = {
58
+ "str": str,
59
+ "int": int,
60
+ "float": float,
61
+ "path": Path,
62
+ "bool": bool,
63
+ "uri": urllib.parse.ParseResult,
64
+ "list": list,
65
+ }
66
+
67
+ def __init__(self, section: str, name: str, properties: dict):
68
+ self.section = section
69
+ self.name = name
70
+
71
+ # Check for unsupported properties
72
+ unsupported_properties = list(
73
+ set(properties.keys()).difference(self.POSSIBLE_PROPERTIES.keys())
74
+ )
75
+ if len(unsupported_properties):
76
+ raise SettingsDefinitionError(
77
+ f"Unsupported properties found for {str(self)}: "
78
+ f"{','.join(unsupported_properties)}"
79
+ )
80
+
81
+ for _property, attribute in self.POSSIBLE_PROPERTIES.items():
82
+ value = None
83
+ if _property in properties:
84
+ value = properties[_property]
85
+ setattr(self, attribute, value)
86
+
87
+ # Check the type of defined parameter is actually supported
88
+ if self._type not in self.EXPECTED_TYPES:
89
+ raise SettingsDefinitionError(
90
+ f"Unsupported type {self._type} for {str(self)}"
91
+ )
92
+
93
+ # Check content property is defined for list and check content type is actually
94
+ # supported.
95
+ if self._type == "list":
96
+ if "content" not in properties:
97
+ raise SettingsDefinitionError(
98
+ f"List content type for parameter {str(self)} must be defined"
99
+ )
100
+ elif self.content not in set(self.EXPECTED_TYPES.keys()) - {"list"}:
101
+ raise SettingsDefinitionError(
102
+ f"Unsupported list content type {self.content} for {str(self)}"
103
+ )
104
+ # Check content property is not defined if not list
105
+ if "content" in properties and self._type != "list":
106
+ raise SettingsDefinitionError(
107
+ f"Content property is forbidden for parameter {str(self)} with type "
108
+ f"{self._type}"
109
+ )
110
+
111
+ # If the default value is defined, convert to the expected type and validate it
112
+ # is a valid choice.
113
+ if self.default is not None:
114
+ if self._type == "list":
115
+ if not isinstance(self.default, list):
116
+ raise SettingsDefinitionError(
117
+ f"Default value {self.default} for parameter {str(self)} is "
118
+ "not a valid list"
119
+ )
120
+ self.default = [
121
+ self._load_default(self.content, default)
122
+ for default in self.default
123
+ ]
124
+ for item in self.default:
125
+ self._validate_choice(item)
126
+ else:
127
+ self.default = self._load_default(self._type, self.default)
128
+ self._validate_choice(self.default)
129
+
130
+ if self.required is None:
131
+ self.required = False
132
+ elif not isinstance(self.required, bool):
133
+ raise SettingsDefinitionError(
134
+ f"Invalid boolean value of {str(self)} required property"
135
+ )
136
+
137
+ def _load_default(self, _type, default):
138
+ """Verify default has the expected type and convert to advanded type if
139
+ needed."""
140
+ if _type == "path":
141
+ return Path(default)
142
+ elif _type == "uri":
143
+ return urllib.parse.urlparse(default)
144
+ if not isinstance(default, self.EXPECTED_TYPES[_type]):
145
+ raise SettingsDefinitionError(
146
+ f"Default value {default} for parameter {str(self)} has not "
147
+ f"the expected type {_type}"
148
+ )
149
+ return default
150
+
151
+ def _validate_choice(self, default):
152
+ """Check default is a valid choice or raise SettingsDefinitionError."""
153
+ if self.choices is not None and default not in self.choices:
154
+ raise SettingsDefinitionError(
155
+ f"Default value {default} for parameter {str(self)} is not "
156
+ f"one of possible choices {self.choices}"
157
+ )
158
+
159
+ def __str__(self):
160
+ return f"[{self.section}]>{self.name}"
161
+
162
+
163
+ class SettingsSectionDefinition:
164
+ def __init__(self, section: str, parameters: dict):
165
+ self.name = section
166
+ self.parameters = [
167
+ SettingsParameterDefinition(section, parameter, properties)
168
+ for parameter, properties in parameters.items()
169
+ ]
170
+
171
+ def has_parameter(self, name: str) -> bool:
172
+ for parameter in self.parameters:
173
+ if parameter.name == name:
174
+ return True
175
+ return False
176
+
177
+ def parameter(self, name: str):
178
+ for parameter in self.parameters:
179
+ if parameter.name == name:
180
+ return parameter
181
+ return None
182
+
183
+
184
+ class SettingsDefinition:
185
+ def __init__(self, loader: SettingsDefinitionLoader):
186
+ self.sections = [
187
+ SettingsSectionDefinition(section, parameters)
188
+ for section, parameters in loader.content.items()
189
+ ]
190
+
191
+ def has_section(self, name: str) -> bool:
192
+ for section in self.sections:
193
+ if section.name == name:
194
+ return True
195
+ return False
196
+
197
+ def section(self, name: str):
198
+ for section in self.sections:
199
+ if section.name == name:
200
+ return section
201
+ return None
@@ -0,0 +1,23 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ from rfl.core.errors import RFLError
8
+
9
+
10
+ class SettingsError(RFLError):
11
+ pass
12
+
13
+
14
+ class SettingsDefinitionError(SettingsError):
15
+ pass
16
+
17
+
18
+ class SettingsSiteLoaderError(SettingsError):
19
+ pass
20
+
21
+
22
+ class SettingsOverrideError(SettingsError):
23
+ pass
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ import configparser
8
+ from pathlib import Path
9
+ import logging
10
+
11
+ from .errors import SettingsSiteLoaderError
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class RuntimeSettingsSiteLoader:
17
+ pass
18
+
19
+
20
+ class RuntimeSettingsSiteLoaderIni(RuntimeSettingsSiteLoader):
21
+ def __init__(self, raw: str = None, path: Path = None):
22
+ self.content = configparser.ConfigParser(
23
+ interpolation=configparser.ExtendedInterpolation()
24
+ )
25
+ try:
26
+ if raw is not None:
27
+ logger.debug("Loading site settings in INI format from raw value")
28
+ self.content.read_string(raw)
29
+ elif path is not None:
30
+ logger.debug("Loading site settings file %s", path)
31
+ self.content.read(path)
32
+ else:
33
+ raise SettingsSiteLoaderError(
34
+ "Either a raw string value or a path must be given to load site "
35
+ "settings"
36
+ )
37
+ except configparser.Error as error:
38
+ raise SettingsSiteLoaderError(error)
File without changes
@@ -0,0 +1,364 @@
1
+ # Copyright (c) 2023 Rackslab
2
+ #
3
+ # This file is part of RFL.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from rfl.settings.definition import SettingsDefinition, SettingsDefinitionLoaderYaml
11
+ from rfl.settings.loaders import RuntimeSettingsSiteLoaderIni
12
+ from rfl.settings import RuntimeSettings, RuntimeSettingsSection
13
+
14
+ from rfl.settings.errors import SettingsDefinitionError, SettingsOverrideError
15
+
16
+ VALID_DEFINITION = """
17
+ ---
18
+ section1:
19
+ param_str:
20
+ type: str
21
+ default: value1
22
+
23
+ section2:
24
+ param_int:
25
+ type: int
26
+ default: 10
27
+ doc: documentation of param2
28
+ ex: 500
29
+ choices:
30
+ - 10
31
+ - 100
32
+ - 500
33
+ param_path:
34
+ type: path
35
+ default: /path/to/file
36
+ doc: documentation of param3
37
+ ex: /other/path/to/file
38
+ param_uri:
39
+ type: uri
40
+ default: https://localhost:5900/resources
41
+ param_float:
42
+ type: float
43
+ param_bool:
44
+ type: bool
45
+ default: yes
46
+ param_list:
47
+ type: list
48
+ content: str
49
+ default:
50
+ - value1
51
+ - value2
52
+ param_required:
53
+ type: str
54
+ required: true
55
+ """
56
+
57
+ VALID_SITE = """
58
+ [section1]
59
+ param_str = site_value1
60
+
61
+ [section2]
62
+ param_path = /site/path/to/file
63
+ param_list =
64
+ value3
65
+ #value4
66
+ value5
67
+ param_required = required_value
68
+ """
69
+
70
+
71
+ class TestSettingsDefinition(unittest.TestCase):
72
+ def test_valid_content(self):
73
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
74
+ definition = SettingsDefinition(loader)
75
+ self.assertTrue(definition.has_section("section1"))
76
+ self.assertFalse(definition.has_section("unknown"))
77
+ self.assertTrue(definition.section("section1").has_parameter("param_str"))
78
+ self.assertFalse(definition.section("section1").has_parameter("unknown"))
79
+ self.assertEqual(
80
+ definition.section("section1").parameter("param_str").default, "value1"
81
+ )
82
+ self.assertEqual(
83
+ definition.section("section2").parameter("param_int").example, 500
84
+ )
85
+ self.assertEqual(
86
+ len(definition.section("section2").parameter("param_int").choices), 3
87
+ )
88
+ self.assertEqual(
89
+ definition.section("section2").parameter("param_uri")._type, "uri"
90
+ )
91
+
92
+ def test_invalid_yaml(self):
93
+ with self.assertRaisesRegex(
94
+ SettingsDefinitionError, "^Invalid YAML settings definition: "
95
+ ):
96
+ SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION + "\n fail")
97
+ with self.assertRaisesRegex(
98
+ SettingsDefinitionError,
99
+ "^YAML scanner error: mapping values are not allowed here.*",
100
+ ):
101
+ SettingsDefinitionLoaderYaml(raw="fail: again: ")
102
+
103
+ def test_unsupported_property(self):
104
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
105
+ loader.content["section2"]["param_int"]["unknown"] = "fail"
106
+ with self.assertRaisesRegex(
107
+ SettingsDefinitionError,
108
+ "^Unsupported properties found for \[section2\]>param_int: unknown$",
109
+ ):
110
+ SettingsDefinition(loader)
111
+
112
+ def test_unsupported_type(self):
113
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
114
+ loader.content["section2"]["param_int"]["type"] = "fail"
115
+ with self.assertRaisesRegex(
116
+ SettingsDefinitionError,
117
+ "^Unsupported type fail for \[section2\]>param_int$",
118
+ ):
119
+ SettingsDefinition(loader)
120
+
121
+ def test_required_not_bool(self):
122
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
123
+ loader.content["section2"]["param_required"]["required"] = "fail"
124
+ with self.assertRaisesRegex(
125
+ SettingsDefinitionError,
126
+ "^Invalid boolean value of \[section2\]>param_required required property$",
127
+ ):
128
+ SettingsDefinition(loader)
129
+
130
+ def test_default_invalid_type_int(self):
131
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
132
+ loader.content["section2"]["param_int"]["default"] = "fail"
133
+ with self.assertRaisesRegex(
134
+ SettingsDefinitionError,
135
+ "^Default value fail for parameter \[section2\]>param_int has not the "
136
+ "expected type int$",
137
+ ):
138
+ SettingsDefinition(loader)
139
+
140
+ def test_default_invalid_type_bool(self):
141
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
142
+ loader.content["section2"]["param_bool"]["default"] = "fail"
143
+ with self.assertRaisesRegex(
144
+ SettingsDefinitionError,
145
+ "^Default value fail for parameter \[section2\]>param_bool has not the "
146
+ "expected type bool$",
147
+ ):
148
+ SettingsDefinition(loader)
149
+
150
+ def test_default_invalid_choice(self):
151
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
152
+ loader.content["section2"]["param_int"]["default"] = 12
153
+ with self.assertRaisesRegex(
154
+ SettingsDefinitionError,
155
+ "^Default value 12 for parameter \[section2\]>param_int is not one of "
156
+ "possible choices \[10, 100, 500\]$",
157
+ ):
158
+ SettingsDefinition(loader)
159
+
160
+ def test_default_invalid_type_list(self):
161
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
162
+ loader.content["section2"]["param_list"]["default"] = "fail"
163
+ with self.assertRaisesRegex(
164
+ SettingsDefinitionError,
165
+ "^Default value fail for parameter \[section2\]>param_list is not a valid "
166
+ "list$",
167
+ ):
168
+ SettingsDefinition(loader)
169
+
170
+ def test_type_list_without_content(self):
171
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
172
+ del loader.content["section2"]["param_list"]["content"]
173
+ with self.assertRaisesRegex(
174
+ SettingsDefinitionError,
175
+ "^List content type for parameter \[section2\]>param_list must be defined$",
176
+ ):
177
+ SettingsDefinition(loader)
178
+
179
+ def test_type_list_invalid_content_type(self):
180
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
181
+ loader.content["section2"]["param_list"]["content"] = "fail"
182
+ with self.assertRaisesRegex(
183
+ SettingsDefinitionError,
184
+ "^Unsupported list content type fail for \[section2\]>param_list$",
185
+ ):
186
+ SettingsDefinition(loader)
187
+ loader.content["section2"]["param_list"]["content"] = "list"
188
+ with self.assertRaisesRegex(
189
+ SettingsDefinitionError,
190
+ "^Unsupported list content type list for \[section2\]>param_list$",
191
+ ):
192
+ SettingsDefinition(loader)
193
+
194
+ def test_type_content_on_other_type(self):
195
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
196
+ loader.content["section2"]["param_list"]["type"] = "str"
197
+ with self.assertRaisesRegex(
198
+ SettingsDefinitionError,
199
+ "^Content property is forbidden for parameter \[section2\]>param_list with "
200
+ "type str$",
201
+ ):
202
+ SettingsDefinition(loader)
203
+
204
+ def test_type_list_invalid_default_type(self):
205
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
206
+ loader.content["section2"]["param_list"]["content"] = "bool"
207
+ with self.assertRaisesRegex(
208
+ SettingsDefinitionError,
209
+ "^Default value value1 for parameter \[section2\]>param_list has not the "
210
+ "expected type bool$",
211
+ ):
212
+ SettingsDefinition(loader)
213
+
214
+ def test_type_list_invalid_choice(self):
215
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
216
+ loader.content["section2"]["param_list"]["choices"] = ["value1", "value3"]
217
+ with self.assertRaisesRegex(
218
+ SettingsDefinitionError,
219
+ "^Default value value2 for parameter \[section2\]>param_list is not one of "
220
+ "possible choices \['value1', 'value3'\]$",
221
+ ):
222
+ SettingsDefinition(loader)
223
+
224
+
225
+ class TestRuntimeSettings(unittest.TestCase):
226
+ def test_valid_settings(self):
227
+ loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
228
+ definition = SettingsDefinition(loader)
229
+ settings = RuntimeSettings(definition)
230
+ self.assertEqual(settings.section1.param_str, "value1")
231
+ self.assertEqual(settings.section2.param_int, 10)
232
+ self.assertEqual(settings.section2.param_path, Path("/path/to/file"))
233
+ self.assertIsInstance(settings.section2.param_uri, tuple)
234
+ self.assertEqual(settings.section2.param_uri.scheme, "https")
235
+ self.assertIsInstance(settings.section1, RuntimeSettingsSection)
236
+ self.assertEqual(type(settings.section1).__name__, "RuntimeSettingsSection1")
237
+
238
+ def test_site_overrides(self):
239
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
240
+ definition = SettingsDefinition(def_loader)
241
+ settings = RuntimeSettings(definition)
242
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
243
+ settings.override(site_loader)
244
+ self.assertEqual(settings.section1.param_str, "site_value1")
245
+ self.assertEqual(settings.section2.param_path, Path("/site/path/to/file"))
246
+ self.assertEqual(settings.section2.param_list, ["value3", "value5"])
247
+
248
+ def test_site_override_invalid_section(self):
249
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
250
+ definition = SettingsDefinition(def_loader)
251
+ settings = RuntimeSettings(definition)
252
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
253
+ site_loader.content["big"] = {}
254
+ with self.assertRaisesRegex(
255
+ SettingsOverrideError,
256
+ "^Section big loaded in settings overrides is not defined in "
257
+ "settings definition$",
258
+ ):
259
+ settings.override(site_loader)
260
+
261
+ def test_site_override_invalid_parameter(self):
262
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
263
+ definition = SettingsDefinition(def_loader)
264
+ settings = RuntimeSettings(definition)
265
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
266
+ site_loader.content["section2"]["unknown"] = "fail"
267
+ with self.assertRaisesRegex(
268
+ SettingsOverrideError,
269
+ "^Parameter unknown loaded in settings overrides is not defined in "
270
+ "section section2 of settings definition$",
271
+ ):
272
+ settings.override(site_loader)
273
+
274
+ def test_site_override_invalid_type_int(self):
275
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
276
+ definition = SettingsDefinition(def_loader)
277
+ settings = RuntimeSettings(definition)
278
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
279
+ site_loader.content["section2"]["param_int"] = "fail"
280
+ with self.assertRaisesRegex(
281
+ SettingsOverrideError,
282
+ "^Invalid integer value 'fail' for \[section2\]>param_int in site "
283
+ "overrides$",
284
+ ):
285
+ settings.override(site_loader)
286
+
287
+ def test_site_override_invalid_type_float(self):
288
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
289
+ definition = SettingsDefinition(def_loader)
290
+ settings = RuntimeSettings(definition)
291
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
292
+ site_loader.content["section2"]["param_float"] = "fail"
293
+ with self.assertRaisesRegex(
294
+ SettingsOverrideError,
295
+ "^Invalid float value 'fail' for \[section2\]>param_float in site "
296
+ "overrides$",
297
+ ):
298
+ settings.override(site_loader)
299
+
300
+ def test_site_override_invalid_type_bool(self):
301
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
302
+ definition = SettingsDefinition(def_loader)
303
+ settings = RuntimeSettings(definition)
304
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
305
+ site_loader.content["section2"]["param_bool"] = "fail"
306
+ with self.assertRaisesRegex(
307
+ SettingsOverrideError,
308
+ "^Invalid boolean value 'fail' for \[section2\]>param_bool in site "
309
+ "overrides$",
310
+ ):
311
+ settings.override(site_loader)
312
+
313
+ def test_site_override_invalid_choice(self):
314
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
315
+ definition = SettingsDefinition(def_loader)
316
+ settings = RuntimeSettings(definition)
317
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
318
+ site_loader.content["section2"]["param_int"] = "12"
319
+ with self.assertRaisesRegex(
320
+ SettingsOverrideError,
321
+ "^Value 12 for parameter \[section2\]>param_int in site overrides is not "
322
+ "one of possible choices \[10, 100, 500\]$",
323
+ ):
324
+ settings.override(site_loader)
325
+
326
+ def test_site_override_undefined_required(self):
327
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
328
+ definition = SettingsDefinition(def_loader)
329
+ settings = RuntimeSettings(definition)
330
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
331
+ del site_loader.content["section2"]["param_required"]
332
+ with self.assertRaisesRegex(
333
+ SettingsOverrideError,
334
+ "^Parameter \[section2\]>param_required is missing but required in "
335
+ "settings overrides$",
336
+ ):
337
+ settings.override(site_loader)
338
+
339
+ def test_site_override_list_invalid_content_type(self):
340
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
341
+ def_loader.content["section2"]["param_list"]["content"] = "int"
342
+ def_loader.content["section2"]["param_list"]["default"] = [2, 5]
343
+ definition = SettingsDefinition(def_loader)
344
+ settings = RuntimeSettings(definition)
345
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
346
+ with self.assertRaisesRegex(
347
+ SettingsOverrideError,
348
+ "^Invalid integer value 'value3' for \[section2\]>param_list in site "
349
+ "overrides$",
350
+ ):
351
+ settings.override(site_loader)
352
+
353
+ def test_site_override_list_invalid_choice(self):
354
+ def_loader = SettingsDefinitionLoaderYaml(raw=VALID_DEFINITION)
355
+ def_loader.content["section2"]["param_list"]["choices"] = ["value1", "value2"]
356
+ definition = SettingsDefinition(def_loader)
357
+ settings = RuntimeSettings(definition)
358
+ site_loader = RuntimeSettingsSiteLoaderIni(VALID_SITE)
359
+ with self.assertRaisesRegex(
360
+ SettingsOverrideError,
361
+ "^Value value3 for parameter \[section2\]>param_list in site overrides is "
362
+ "not one of possible choices \['value1', 'value2'\]$",
363
+ ):
364
+ settings.override(site_loader)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+