toml-combine 0.5.0__tar.gz → 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.
- toml_combine-1.0.0/CONTRIBUTING.md +34 -0
- toml_combine-1.0.0/LICENSE +7 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/PKG-INFO +27 -20
- {toml_combine-0.5.0 → toml_combine-1.0.0}/README.md +25 -19
- {toml_combine-0.5.0 → toml_combine-1.0.0}/tests/test.toml +4 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/tests/test_cli.py +2 -1
- {toml_combine-0.5.0 → toml_combine-1.0.0}/tests/test_combiner.py +229 -227
- {toml_combine-0.5.0 → toml_combine-1.0.0}/tests/test_lib.py +52 -23
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/combiner.py +36 -72
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/exceptions.py +2 -2
- {toml_combine-0.5.0 → toml_combine-1.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/.github/renovate.json5 +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/.github/workflows/ci.yml +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/.pre-commit-config.yaml +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/pyproject.toml +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/tests/result.json +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/__init__.py +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/__main__.py +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/cli.py +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/lib.py +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/toml_combine/toml.py +0 -0
- {toml_combine-0.5.0 → toml_combine-1.0.0}/uv.lock +0 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
## Development
|
2
|
+
|
3
|
+
This project uses [uv](https://docs.astral.sh/uv/).
|
4
|
+
|
5
|
+
If you don't have uv installed, install it with:
|
6
|
+
|
7
|
+
```console
|
8
|
+
$ curl -LsSf https://astral.sh/uv/install.sh | sh
|
9
|
+
```
|
10
|
+
|
11
|
+
or using a package manager, such as [brew](https://brew.sh/):
|
12
|
+
|
13
|
+
```console
|
14
|
+
$ brew install uv
|
15
|
+
```
|
16
|
+
|
17
|
+
Then you can directly launch the tests with:
|
18
|
+
|
19
|
+
```console
|
20
|
+
$ uv run pytest
|
21
|
+
```
|
22
|
+
|
23
|
+
or create a virtual environment to work on the package and activate it:
|
24
|
+
|
25
|
+
```console
|
26
|
+
$ uv sync
|
27
|
+
$ source .venv/bin/activate
|
28
|
+
```
|
29
|
+
|
30
|
+
or launch the command itself with uv (no need to activate the venv):
|
31
|
+
|
32
|
+
```console
|
33
|
+
$ uv run toml-combine --help
|
34
|
+
```
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2025- Joachim Jablon
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,9 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: toml-combine
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: A tool for combining complex configurations in TOML format.
|
5
5
|
Author-email: Joachim Jablon <ewjoachim@gmail.com>
|
6
6
|
License-Expression: MIT
|
7
|
+
License-File: LICENSE
|
7
8
|
Classifier: Development Status :: 4 - Beta
|
8
9
|
Classifier: Intended Audience :: Developers
|
9
10
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -19,6 +20,12 @@ Description-Content-Type: text/markdown
|
|
19
20
|
|
20
21
|
# Toml-combine
|
21
22
|
|
23
|
+
[](https://pypi.org/pypi/toml-combine)
|
24
|
+
[](https://pypi.org/pypi/toml-combine)
|
25
|
+
[](https://github.com/ewjoachim/toml-combine/)
|
26
|
+
[](https://github.com/ewjoachim/toml-combine/actions?workflow=CI)
|
27
|
+
[](https://github.com/ewjoachim/toml-combine/blob/main/LICENSE)
|
28
|
+
|
22
29
|
`toml-combine` is a Python lib and CLI-tool that reads a TOML configuration file
|
23
30
|
defining a default configuration alongside with overrides, and merges everything
|
24
31
|
following rules you define to get final configurations. Let's say: you have multiple
|
@@ -62,17 +69,19 @@ The common configuration to start from, before we start overlaying overrides on
|
|
62
69
|
### Overrides
|
63
70
|
|
64
71
|
Overrides define a set of condition where they apply (`when.<dimension> =
|
65
|
-
"<value>"`) and the values that are
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
"<value>"`) and the values that are overridgden when they're applicable.
|
73
|
+
|
74
|
+
- In case 2 overrides are applicable and define a value for the same key, if one is more
|
75
|
+
specific than the other (e.g. env=prod,region=us is more specific than env=prod) then
|
76
|
+
its values will have precedence.
|
77
|
+
- If they are mutually exclusive, (env=prod vs env=staging) precedence is irrelevant.
|
78
|
+
- If you try to generate the configuration and 2 applicable overrides define a value for
|
79
|
+
the same key, an error will be raised (e.g. env=staging and region=eu). In that case,
|
80
|
+
you should add dimensions to either override to make them mutually exclusive or make
|
81
|
+
one more specific than the other.
|
82
|
+
|
83
|
+
Note that it's not a problem if incompatible overrides exist in your configuration, as
|
84
|
+
long as they are not both applicable in the same call.
|
76
85
|
|
77
86
|
> [!Note]
|
78
87
|
> Defining a list as the value of one or more conditions in an override
|
@@ -80,14 +89,11 @@ specific to more specific, each one overriding the values of the previous ones:
|
|
80
89
|
|
81
90
|
### The configuration itself
|
82
91
|
|
83
|
-
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
84
|
-
in the configuration is completely up to you. That said, only nested
|
92
|
+
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
93
|
+
define in the configuration is completely up to you. That said, only nested
|
85
94
|
"dictionnaries"/"objects"/"tables"/"mapping" (those are all the same things in
|
86
|
-
Python/JS/Toml lingo) will be merged between the default and the overrides,
|
87
|
-
arrays will just replace one another. See `Arrays` below.
|
88
|
-
|
89
|
-
In the generated configuration, the dimensions of the output will appear in the generated
|
90
|
-
object as an object under the `dimensions` key.
|
95
|
+
Python/JS/Toml lingo) will be merged between the default and the applicable overrides,
|
96
|
+
while arrays will just replace one another. See `Arrays` below.
|
91
97
|
|
92
98
|
### Arrays
|
93
99
|
|
@@ -191,8 +197,9 @@ container.image_name = "my-image-backend"
|
|
191
197
|
container.port = 8080
|
192
198
|
|
193
199
|
[[override]]
|
194
|
-
|
200
|
+
when.service = "backend"
|
195
201
|
when.environment = "dev"
|
202
|
+
name = "service-dev"
|
196
203
|
container.env.DEBUG = true
|
197
204
|
|
198
205
|
[[override]]
|
@@ -1,5 +1,11 @@
|
|
1
1
|
# Toml-combine
|
2
2
|
|
3
|
+
[](https://pypi.org/pypi/toml-combine)
|
4
|
+
[](https://pypi.org/pypi/toml-combine)
|
5
|
+
[](https://github.com/ewjoachim/toml-combine/)
|
6
|
+
[](https://github.com/ewjoachim/toml-combine/actions?workflow=CI)
|
7
|
+
[](https://github.com/ewjoachim/toml-combine/blob/main/LICENSE)
|
8
|
+
|
3
9
|
`toml-combine` is a Python lib and CLI-tool that reads a TOML configuration file
|
4
10
|
defining a default configuration alongside with overrides, and merges everything
|
5
11
|
following rules you define to get final configurations. Let's say: you have multiple
|
@@ -43,17 +49,19 @@ The common configuration to start from, before we start overlaying overrides on
|
|
43
49
|
### Overrides
|
44
50
|
|
45
51
|
Overrides define a set of condition where they apply (`when.<dimension> =
|
46
|
-
"<value>"`) and the values that are
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
"<value>"`) and the values that are overridgden when they're applicable.
|
53
|
+
|
54
|
+
- In case 2 overrides are applicable and define a value for the same key, if one is more
|
55
|
+
specific than the other (e.g. env=prod,region=us is more specific than env=prod) then
|
56
|
+
its values will have precedence.
|
57
|
+
- If they are mutually exclusive, (env=prod vs env=staging) precedence is irrelevant.
|
58
|
+
- If you try to generate the configuration and 2 applicable overrides define a value for
|
59
|
+
the same key, an error will be raised (e.g. env=staging and region=eu). In that case,
|
60
|
+
you should add dimensions to either override to make them mutually exclusive or make
|
61
|
+
one more specific than the other.
|
62
|
+
|
63
|
+
Note that it's not a problem if incompatible overrides exist in your configuration, as
|
64
|
+
long as they are not both applicable in the same call.
|
57
65
|
|
58
66
|
> [!Note]
|
59
67
|
> Defining a list as the value of one or more conditions in an override
|
@@ -61,14 +69,11 @@ specific to more specific, each one overriding the values of the previous ones:
|
|
61
69
|
|
62
70
|
### The configuration itself
|
63
71
|
|
64
|
-
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
65
|
-
in the configuration is completely up to you. That said, only nested
|
72
|
+
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
73
|
+
define in the configuration is completely up to you. That said, only nested
|
66
74
|
"dictionnaries"/"objects"/"tables"/"mapping" (those are all the same things in
|
67
|
-
Python/JS/Toml lingo) will be merged between the default and the overrides,
|
68
|
-
arrays will just replace one another. See `Arrays` below.
|
69
|
-
|
70
|
-
In the generated configuration, the dimensions of the output will appear in the generated
|
71
|
-
object as an object under the `dimensions` key.
|
75
|
+
Python/JS/Toml lingo) will be merged between the default and the applicable overrides,
|
76
|
+
while arrays will just replace one another. See `Arrays` below.
|
72
77
|
|
73
78
|
### Arrays
|
74
79
|
|
@@ -172,8 +177,9 @@ container.image_name = "my-image-backend"
|
|
172
177
|
container.port = 8080
|
173
178
|
|
174
179
|
[[override]]
|
175
|
-
|
180
|
+
when.service = "backend"
|
176
181
|
when.environment = "dev"
|
182
|
+
name = "service-dev"
|
177
183
|
container.env.DEBUG = true
|
178
184
|
|
179
185
|
[[override]]
|
@@ -49,6 +49,7 @@ cloudsql_instance = "staging-postgres"
|
|
49
49
|
containers.app.env.APP_FOO = "qux"
|
50
50
|
|
51
51
|
[[override]]
|
52
|
+
when.stack = "django"
|
52
53
|
# The following line defines when in an array. It's not useful, as there's only one
|
53
54
|
# value, but we want to test that arrays work too.
|
54
55
|
when.service = ["admin"]
|
@@ -57,6 +58,7 @@ containers.app.env.APP_ADMIN_ENABLED = true
|
|
57
58
|
containers.app.env.APP_ID = 1234
|
58
59
|
|
59
60
|
[[override]]
|
61
|
+
when.stack = "django"
|
60
62
|
when.service = "admin"
|
61
63
|
when.environment = "staging"
|
62
64
|
containers.app.env.APP_ID = 5678
|
@@ -66,11 +68,13 @@ when.type = "job"
|
|
66
68
|
containers.app.job.max_retries = 1
|
67
69
|
|
68
70
|
[[override]]
|
71
|
+
when.stack = "django"
|
69
72
|
when.job = "manage"
|
70
73
|
containers.app.name = "manage"
|
71
74
|
containers.app.command = ["./manage.py"]
|
72
75
|
|
73
76
|
[[override]]
|
77
|
+
when.stack = "django"
|
74
78
|
when.job = "special-command"
|
75
79
|
containers.app.name = "special-command"
|
76
80
|
containers.app.container_cpu = 8
|
@@ -8,7 +8,7 @@ from toml_combine import cli, toml
|
|
8
8
|
|
9
9
|
def test_cli__json(capsys):
|
10
10
|
"""Test the CLI."""
|
11
|
-
cli.cli(
|
11
|
+
exit_code = cli.cli(
|
12
12
|
argv=[
|
13
13
|
"tests/test.toml",
|
14
14
|
"--format",
|
@@ -28,6 +28,7 @@ def test_cli__json(capsys):
|
|
28
28
|
print(out)
|
29
29
|
print("err:")
|
30
30
|
print(err)
|
31
|
+
assert exit_code == 0
|
31
32
|
|
32
33
|
expected = json.loads((pathlib.Path(__file__).parent / "result.json").read_text())
|
33
34
|
assert json.loads(out) == expected["staging-service-django-admin"]
|
@@ -5,39 +5,6 @@ import pytest
|
|
5
5
|
from toml_combine import combiner, exceptions, toml
|
6
6
|
|
7
7
|
|
8
|
-
@pytest.mark.parametrize(
|
9
|
-
"small_override, large_override, dimensions",
|
10
|
-
[
|
11
|
-
pytest.param(
|
12
|
-
{"env": "prod"},
|
13
|
-
{"env": "prod", "region": "eu"},
|
14
|
-
{"env": ["prod"], "region": ["eu"]},
|
15
|
-
id="less_specific_override_comes_first",
|
16
|
-
),
|
17
|
-
pytest.param(
|
18
|
-
{"env": "prod", "region": "eu"},
|
19
|
-
{"env": "prod", "service": "web"},
|
20
|
-
{"env": ["prod"], "region": ["eu"], "service": ["web"]},
|
21
|
-
id="different_dimensions_sorted_by_dimension",
|
22
|
-
),
|
23
|
-
pytest.param(
|
24
|
-
{"env": "prod"},
|
25
|
-
{"region": "eu"},
|
26
|
-
{"env": ["prod"], "region": ["eu"]},
|
27
|
-
id="completely_different_dimensions",
|
28
|
-
),
|
29
|
-
],
|
30
|
-
)
|
31
|
-
def test_override_sort_key(small_override, large_override, dimensions):
|
32
|
-
small_key = combiner.override_sort_key(
|
33
|
-
combiner.Override(when=small_override, config={}), dimensions
|
34
|
-
)
|
35
|
-
large_key = combiner.override_sort_key(
|
36
|
-
combiner.Override(when=large_override, config={}), dimensions
|
37
|
-
)
|
38
|
-
assert small_key < large_key
|
39
|
-
|
40
|
-
|
41
8
|
@pytest.mark.parametrize(
|
42
9
|
"a, b, expected",
|
43
10
|
[
|
@@ -64,90 +31,6 @@ def test_merge_configs__dicts_error():
|
|
64
31
|
combiner.merge_configs({"a": 1}, {"a": {"b": 2}})
|
65
32
|
|
66
33
|
|
67
|
-
@pytest.mark.parametrize(
|
68
|
-
"mapping, expected",
|
69
|
-
[
|
70
|
-
pytest.param(
|
71
|
-
{"env": "dev"},
|
72
|
-
{
|
73
|
-
"a": 1,
|
74
|
-
"b": 2,
|
75
|
-
"c": 3,
|
76
|
-
"d": {"e": {"h": {"i": {"j": 4}}}},
|
77
|
-
"g": 6,
|
78
|
-
},
|
79
|
-
id="no_matches",
|
80
|
-
),
|
81
|
-
pytest.param(
|
82
|
-
{"env": "prod"},
|
83
|
-
{
|
84
|
-
"a": 10,
|
85
|
-
"b": 2,
|
86
|
-
"c": 30,
|
87
|
-
"d": {"e": {"h": {"i": {"j": 40}}}},
|
88
|
-
"g": 60,
|
89
|
-
},
|
90
|
-
id="single_match",
|
91
|
-
),
|
92
|
-
pytest.param(
|
93
|
-
{"env": "staging"},
|
94
|
-
{
|
95
|
-
"a": 1,
|
96
|
-
"b": 200,
|
97
|
-
"c": 300,
|
98
|
-
"d": {"e": {"h": {"i": {"j": 400}}}},
|
99
|
-
"f": 500,
|
100
|
-
"g": 6,
|
101
|
-
},
|
102
|
-
id="dont_override_if_match_is_more_specific",
|
103
|
-
),
|
104
|
-
],
|
105
|
-
)
|
106
|
-
def __full_chain(mapping: dict, expected: dict[str, int]):
|
107
|
-
default = {
|
108
|
-
"a": 1,
|
109
|
-
"b": 2,
|
110
|
-
"c": 3,
|
111
|
-
"d": {"e": {"h": {"i": {"j": 4}}}},
|
112
|
-
"g": 6,
|
113
|
-
}
|
114
|
-
|
115
|
-
overrides = [
|
116
|
-
combiner.Override(
|
117
|
-
when={"env": ["prod"]},
|
118
|
-
config={
|
119
|
-
"a": 10,
|
120
|
-
"c": 30,
|
121
|
-
"d": {"e": {"h": {"i": {"j": 40}}}},
|
122
|
-
"g": 60,
|
123
|
-
},
|
124
|
-
),
|
125
|
-
combiner.Override(
|
126
|
-
when={"env": ["staging"]},
|
127
|
-
config={
|
128
|
-
"b": 200,
|
129
|
-
"c": 300,
|
130
|
-
"d": {"e": {"h": {"i": {"j": 400}}}},
|
131
|
-
"f": 500,
|
132
|
-
},
|
133
|
-
),
|
134
|
-
combiner.Override(
|
135
|
-
when={"env": ["staging"], "region": ["us"]},
|
136
|
-
config={"f": 5000, "g": 6000},
|
137
|
-
),
|
138
|
-
]
|
139
|
-
|
140
|
-
result = combiner.generate_for_mapping(
|
141
|
-
config=combiner.Config(
|
142
|
-
dimensions={"env": ["prod", "staging"], "region": ["us"]},
|
143
|
-
default=default,
|
144
|
-
overrides=overrides,
|
145
|
-
),
|
146
|
-
mapping=mapping,
|
147
|
-
)
|
148
|
-
assert result == expected
|
149
|
-
|
150
|
-
|
151
34
|
@pytest.mark.parametrize(
|
152
35
|
"mapping, override, expected",
|
153
36
|
[
|
@@ -192,16 +75,18 @@ def test_build_config():
|
|
192
75
|
raw_config = """
|
193
76
|
[dimensions]
|
194
77
|
env = ["dev", "staging", "prod"]
|
78
|
+
region = ["eu"]
|
195
79
|
|
196
80
|
[default]
|
197
81
|
foo = "bar"
|
198
82
|
|
199
83
|
[[override]]
|
200
84
|
when.env = ["dev", "staging"]
|
85
|
+
when.region = ["eu"]
|
201
86
|
foo = "baz"
|
202
87
|
|
203
88
|
[[override]]
|
204
|
-
when.env = "
|
89
|
+
when.env = "dev"
|
205
90
|
foo = "qux"
|
206
91
|
"""
|
207
92
|
|
@@ -209,86 +94,23 @@ def test_build_config():
|
|
209
94
|
config = combiner.build_config(config_dict)
|
210
95
|
|
211
96
|
assert config == combiner.Config(
|
212
|
-
dimensions={"env": ["dev", "staging", "prod"]},
|
97
|
+
dimensions={"env": ["dev", "staging", "prod"], "region": ["eu"]},
|
213
98
|
default={"foo": "bar"},
|
214
99
|
overrides=[
|
100
|
+
# Note: The order of the overrides is important: more specific overrides
|
101
|
+
# must be listed last.
|
215
102
|
combiner.Override(
|
216
|
-
when={"env": ["dev"
|
217
|
-
config={"foo": "
|
103
|
+
when={"env": ["dev"]},
|
104
|
+
config={"foo": "qux"},
|
218
105
|
),
|
219
106
|
combiner.Override(
|
220
|
-
when={"env": ["
|
221
|
-
config={"foo": "
|
107
|
+
when={"env": ["dev", "staging"], "region": ["eu"]},
|
108
|
+
config={"foo": "baz"},
|
222
109
|
),
|
223
110
|
],
|
224
111
|
)
|
225
112
|
|
226
113
|
|
227
|
-
def test_build_config__duplicate_overrides():
|
228
|
-
raw_config = """
|
229
|
-
[dimensions]
|
230
|
-
env = ["prod"]
|
231
|
-
|
232
|
-
[[override]]
|
233
|
-
when.env = "prod"
|
234
|
-
foo = "baz"
|
235
|
-
|
236
|
-
[[override]]
|
237
|
-
when.env = "prod"
|
238
|
-
foo = "qux"
|
239
|
-
"""
|
240
|
-
|
241
|
-
config = toml.loads(raw_config)
|
242
|
-
with pytest.raises(exceptions.DuplicateError):
|
243
|
-
combiner.build_config(config)
|
244
|
-
|
245
|
-
|
246
|
-
def test_build_config__duplicate_overrides_different_vars():
|
247
|
-
raw_config = """
|
248
|
-
[dimensions]
|
249
|
-
env = ["prod"]
|
250
|
-
|
251
|
-
[[override]]
|
252
|
-
when.env = "prod"
|
253
|
-
foo = "baz"
|
254
|
-
|
255
|
-
[[override]]
|
256
|
-
when.env = "prod"
|
257
|
-
baz = "qux"
|
258
|
-
"""
|
259
|
-
|
260
|
-
config = toml.loads(raw_config)
|
261
|
-
assert len(combiner.build_config(config).overrides) == 2
|
262
|
-
|
263
|
-
|
264
|
-
def test_build_config__duplicate_overrides_list():
|
265
|
-
raw_config = """
|
266
|
-
[dimensions]
|
267
|
-
env = ["prod", "dev"]
|
268
|
-
|
269
|
-
[[override]]
|
270
|
-
when.env = ["prod"]
|
271
|
-
foo = "baz"
|
272
|
-
hello = 1
|
273
|
-
|
274
|
-
[[override]]
|
275
|
-
when.env = ["prod", "dev"]
|
276
|
-
foo = "qux"
|
277
|
-
hello = 1
|
278
|
-
"""
|
279
|
-
|
280
|
-
config = toml.loads(raw_config)
|
281
|
-
with pytest.raises(exceptions.DuplicateError) as excinfo:
|
282
|
-
combiner.build_config(config)
|
283
|
-
|
284
|
-
# Message is a bit complex so we test it too.
|
285
|
-
assert (
|
286
|
-
str(excinfo.value) == "In override {'env': ['prod', 'dev']}: "
|
287
|
-
"Overrides with the same dimensions cannot define the same configuration keys: "
|
288
|
-
"foo, hello"
|
289
|
-
)
|
290
|
-
|
291
|
-
|
292
114
|
def test_build_config__dimension_not_found_in_override():
|
293
115
|
raw_config = """
|
294
116
|
[dimensions]
|
@@ -317,6 +139,78 @@ def test_build_config__dimension_value_not_found_in_override():
|
|
317
139
|
combiner.build_config(config)
|
318
140
|
|
319
141
|
|
142
|
+
def test_extract_keys():
|
143
|
+
config = toml.loads(
|
144
|
+
"""
|
145
|
+
a = 1
|
146
|
+
b.c = 1
|
147
|
+
b.d = 1
|
148
|
+
e.f.g = 1
|
149
|
+
""",
|
150
|
+
)
|
151
|
+
|
152
|
+
result = list(combiner.extract_keys(config))
|
153
|
+
assert result == [
|
154
|
+
("a",),
|
155
|
+
("b", "c"),
|
156
|
+
("b", "d"),
|
157
|
+
("e", "f", "g"),
|
158
|
+
]
|
159
|
+
|
160
|
+
|
161
|
+
@pytest.mark.parametrize(
|
162
|
+
"a, b, expected",
|
163
|
+
[
|
164
|
+
pytest.param(
|
165
|
+
{"env": ["dev"], "region": ["eu"]},
|
166
|
+
{"env": ["dev"]},
|
167
|
+
True,
|
168
|
+
id="subset1",
|
169
|
+
),
|
170
|
+
pytest.param(
|
171
|
+
{"env": ["dev"]},
|
172
|
+
{"env": ["dev"], "region": ["eu"]},
|
173
|
+
True,
|
174
|
+
id="subset2",
|
175
|
+
),
|
176
|
+
pytest.param(
|
177
|
+
{"env": ["prod"], "region": ["eu"]},
|
178
|
+
{"env": ["dev"]},
|
179
|
+
True,
|
180
|
+
id="subset3",
|
181
|
+
),
|
182
|
+
pytest.param(
|
183
|
+
{"env": ["dev"]},
|
184
|
+
{"env": ["prod"], "region": ["eu"]},
|
185
|
+
True,
|
186
|
+
id="subset4",
|
187
|
+
),
|
188
|
+
pytest.param({"env": ["dev"]}, {"region": ["eu"]}, False, id="disjoint"),
|
189
|
+
pytest.param(
|
190
|
+
{"env": ["dev"], "service": ["frontend"]},
|
191
|
+
{"region": ["eu"], "service": ["frontend"]},
|
192
|
+
False,
|
193
|
+
id="overlap",
|
194
|
+
),
|
195
|
+
pytest.param({"env": ["dev"]}, {"env": ["dev"]}, False, id="same_keys1"),
|
196
|
+
pytest.param(
|
197
|
+
{"env": ["dev", "prod"]}, {"env": ["dev"]}, False, id="same_keys1"
|
198
|
+
),
|
199
|
+
pytest.param(
|
200
|
+
{"env": ["prod"]}, {"env": ["dev"]}, True, id="same_keys_disjoint"
|
201
|
+
),
|
202
|
+
pytest.param(
|
203
|
+
{"env": ["prod", "staging"]},
|
204
|
+
{"env": ["dev", "sandbox"]},
|
205
|
+
True,
|
206
|
+
id="multiple_keys_disjoint",
|
207
|
+
),
|
208
|
+
],
|
209
|
+
)
|
210
|
+
def test_are_conditions_compatible(a, b, expected):
|
211
|
+
assert combiner.are_conditions_compatible(a, b) == expected
|
212
|
+
|
213
|
+
|
320
214
|
@pytest.mark.parametrize(
|
321
215
|
"mapping, expected",
|
322
216
|
[
|
@@ -325,23 +219,23 @@ def test_build_config__dimension_value_not_found_in_override():
|
|
325
219
|
{"foo": "bar"},
|
326
220
|
),
|
327
221
|
(
|
328
|
-
{"env": "
|
222
|
+
{"env": "staging"},
|
329
223
|
{"foo": "baz"},
|
330
224
|
),
|
331
225
|
],
|
332
226
|
)
|
333
|
-
def
|
227
|
+
def test_generate_for_mapping__simple_case(mapping, expected):
|
334
228
|
config = combiner.build_config(
|
335
229
|
toml.loads(
|
336
230
|
"""
|
337
231
|
[dimensions]
|
338
|
-
env = ["prod", "
|
232
|
+
env = ["prod", "staging"]
|
339
233
|
|
340
234
|
[default]
|
341
235
|
foo = "bar"
|
342
236
|
|
343
237
|
[[override]]
|
344
|
-
when.env = "
|
238
|
+
when.env = "staging"
|
345
239
|
foo = "baz"
|
346
240
|
""",
|
347
241
|
)
|
@@ -353,43 +247,151 @@ def test_generate_for_mapping__full_chain(mapping, expected):
|
|
353
247
|
assert result == expected
|
354
248
|
|
355
249
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
250
|
+
@pytest.mark.parametrize(
|
251
|
+
"mapping, expected",
|
252
|
+
[
|
253
|
+
pytest.param(
|
254
|
+
{"env": "dev"},
|
255
|
+
{
|
256
|
+
"a": 1,
|
257
|
+
"b": 2,
|
258
|
+
"c": 3,
|
259
|
+
"d": {"e": {"h": {"i": {"j": 4}}}},
|
260
|
+
"g": 6,
|
261
|
+
},
|
262
|
+
id="no_matches",
|
263
|
+
),
|
264
|
+
pytest.param(
|
265
|
+
{"env": "prod"},
|
266
|
+
{
|
267
|
+
"a": 10,
|
268
|
+
"b": 2,
|
269
|
+
"c": 30,
|
270
|
+
"d": {"e": {"h": {"i": {"j": 40}}}},
|
271
|
+
"g": 60,
|
272
|
+
},
|
273
|
+
id="single_match",
|
274
|
+
),
|
275
|
+
pytest.param(
|
276
|
+
{"env": "staging"},
|
277
|
+
{
|
278
|
+
"a": 1,
|
279
|
+
"b": 200,
|
280
|
+
"c": 300,
|
281
|
+
"d": {"e": {"h": {"i": {"j": 400}}}},
|
282
|
+
"f": 500,
|
283
|
+
"g": 6,
|
284
|
+
},
|
285
|
+
id="dont_override_if_match_is_more_specific",
|
286
|
+
),
|
287
|
+
],
|
288
|
+
)
|
289
|
+
def test_generate_for_mapping__complex_case(mapping: dict, expected: dict[str, int]):
|
290
|
+
default = {
|
291
|
+
"a": 1,
|
292
|
+
"b": 2,
|
293
|
+
"c": 3,
|
294
|
+
"d": {"e": {"h": {"i": {"j": 4}}}},
|
295
|
+
"g": 6,
|
296
|
+
}
|
365
297
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
298
|
+
overrides = [
|
299
|
+
combiner.Override(
|
300
|
+
when={"env": ["prod"]},
|
301
|
+
config={
|
302
|
+
"a": 10,
|
303
|
+
"c": 30,
|
304
|
+
"d": {"e": {"h": {"i": {"j": 40}}}},
|
305
|
+
"g": 60,
|
306
|
+
},
|
307
|
+
),
|
308
|
+
combiner.Override(
|
309
|
+
when={"env": ["staging"]},
|
310
|
+
config={
|
311
|
+
"b": 200,
|
312
|
+
"c": 300,
|
313
|
+
"d": {"e": {"h": {"i": {"j": 400}}}},
|
314
|
+
"f": 500,
|
315
|
+
},
|
316
|
+
),
|
317
|
+
combiner.Override(
|
318
|
+
when={"env": ["staging"], "region": ["us"]},
|
319
|
+
config={"f": 5000, "g": 6000},
|
320
|
+
),
|
372
321
|
]
|
322
|
+
config = combiner.Config(
|
323
|
+
dimensions={"env": ["prod", "staging"], "region": ["us"]},
|
324
|
+
default=default,
|
325
|
+
overrides=overrides,
|
326
|
+
)
|
373
327
|
|
328
|
+
result = combiner.generate_for_mapping(config=config, mapping=mapping)
|
329
|
+
assert result == expected
|
374
330
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
331
|
+
|
332
|
+
def test_generate_for_mapping__duplicate_overrides():
|
333
|
+
raw_config = """
|
334
|
+
[dimensions]
|
335
|
+
env = ["prod"]
|
336
|
+
|
337
|
+
[[override]]
|
338
|
+
when.env = "prod"
|
339
|
+
foo = "baz"
|
340
|
+
|
341
|
+
[[override]]
|
342
|
+
when.env = "prod"
|
343
|
+
foo = "qux"
|
344
|
+
"""
|
345
|
+
|
346
|
+
config = combiner.build_config(toml.loads(raw_config))
|
347
|
+
with pytest.raises(exceptions.IncompatibleOverrides):
|
348
|
+
combiner.generate_for_mapping(config=config, mapping={"env": "prod"})
|
349
|
+
|
350
|
+
|
351
|
+
def test_generate_for_mapping__duplicate_overrides_different_vars():
|
352
|
+
raw_config = """
|
353
|
+
[dimensions]
|
354
|
+
env = ["prod"]
|
355
|
+
|
356
|
+
[[override]]
|
357
|
+
when.env = "prod"
|
358
|
+
foo = "baz"
|
359
|
+
|
360
|
+
[[override]]
|
361
|
+
when.env = "prod"
|
362
|
+
baz = "qux"
|
363
|
+
"""
|
364
|
+
|
365
|
+
config = combiner.build_config(toml.loads(raw_config))
|
366
|
+
assert combiner.generate_for_mapping(config=config, mapping={"env": "prod"}) == {
|
367
|
+
"foo": "baz",
|
368
|
+
"baz": "qux",
|
369
|
+
}
|
370
|
+
|
371
|
+
|
372
|
+
def test_generate_for_mapping__duplicate_overrides_list():
|
373
|
+
raw_config = """
|
374
|
+
[dimensions]
|
375
|
+
env = ["prod", "dev"]
|
376
|
+
|
377
|
+
[[override]]
|
378
|
+
when.env = ["prod"]
|
379
|
+
hello.world = 1
|
380
|
+
|
381
|
+
[[override]]
|
382
|
+
when.env = ["prod", "dev"]
|
383
|
+
hello.world = 2
|
384
|
+
"""
|
385
|
+
|
386
|
+
config = combiner.build_config(toml.loads(raw_config))
|
387
|
+
with pytest.raises(exceptions.IncompatibleOverrides) as excinfo:
|
388
|
+
combiner.generate_for_mapping(config=config, mapping={"env": "prod"})
|
389
|
+
|
390
|
+
# Message is a bit complex so we test it too.
|
391
|
+
assert (
|
392
|
+
str(excinfo.value)
|
393
|
+
== "In override {'env': ['prod', 'dev']}: Overrides defining the same "
|
394
|
+
"configuration keys must be included in one another or mutually exclusive.\n"
|
395
|
+
"Key defined multiple times: hello.world\n"
|
396
|
+
"Other override: {'env': ['prod']}"
|
384
397
|
)
|
385
|
-
print(result)
|
386
|
-
assert result == [
|
387
|
-
((("env", "dev"), ("region", "eu")), "a"),
|
388
|
-
((("env", "dev"), ("region", "us")), "a"),
|
389
|
-
((("env", "staging"), ("region", "eu")), "a"),
|
390
|
-
((("env", "staging"), ("region", "us")), "a"),
|
391
|
-
((("env", "dev"), ("region", "eu")), "b.c.d"),
|
392
|
-
((("env", "dev"), ("region", "us")), "b.c.d"),
|
393
|
-
((("env", "staging"), ("region", "eu")), "b.c.d"),
|
394
|
-
((("env", "staging"), ("region", "us")), "b.c.d"),
|
395
|
-
]
|
@@ -16,23 +16,19 @@ def expected():
|
|
16
16
|
return json.loads((pathlib.Path(__file__).parent / "result.json").read_text())
|
17
17
|
|
18
18
|
|
19
|
-
@pytest.mark.parametrize(
|
20
|
-
"kwargs",
|
21
|
-
[
|
22
|
-
{"config_file": config_file},
|
23
|
-
{"config_file": str(config_file)},
|
24
|
-
{"config": config_file.read_text()},
|
25
|
-
{"config": toml.loads(config_file.read_text())},
|
26
|
-
],
|
27
|
-
)
|
28
19
|
@pytest.mark.parametrize(
|
29
20
|
"mapping, expected_key",
|
30
21
|
[
|
31
|
-
(
|
32
|
-
{
|
22
|
+
pytest.param(
|
23
|
+
{
|
24
|
+
"environment": "staging",
|
25
|
+
"type": "service",
|
26
|
+
"stack": "next",
|
27
|
+
},
|
33
28
|
"staging-service-next",
|
29
|
+
id="staging-service-next",
|
34
30
|
),
|
35
|
-
(
|
31
|
+
pytest.param(
|
36
32
|
{
|
37
33
|
"environment": "staging",
|
38
34
|
"type": "service",
|
@@ -40,8 +36,9 @@ def expected():
|
|
40
36
|
"service": "api",
|
41
37
|
},
|
42
38
|
"staging-service-django-api",
|
39
|
+
id="staging-service-django-api",
|
43
40
|
),
|
44
|
-
(
|
41
|
+
pytest.param(
|
45
42
|
{
|
46
43
|
"environment": "staging",
|
47
44
|
"type": "service",
|
@@ -49,8 +46,9 @@ def expected():
|
|
49
46
|
"service": "admin",
|
50
47
|
},
|
51
48
|
"staging-service-django-admin",
|
49
|
+
id="staging-service-django-admin",
|
52
50
|
),
|
53
|
-
(
|
51
|
+
pytest.param(
|
54
52
|
{
|
55
53
|
"environment": "staging",
|
56
54
|
"type": "job",
|
@@ -58,8 +56,9 @@ def expected():
|
|
58
56
|
"job": "manage",
|
59
57
|
},
|
60
58
|
"staging-job-django-manage",
|
59
|
+
id="staging-job-django-manage",
|
61
60
|
),
|
62
|
-
(
|
61
|
+
pytest.param(
|
63
62
|
{
|
64
63
|
"environment": "staging",
|
65
64
|
"type": "job",
|
@@ -67,12 +66,18 @@ def expected():
|
|
67
66
|
"job": "special-command",
|
68
67
|
},
|
69
68
|
"staging-job-django-special-command",
|
69
|
+
id="staging-job-django-special-command",
|
70
70
|
),
|
71
|
-
(
|
72
|
-
{
|
71
|
+
pytest.param(
|
72
|
+
{
|
73
|
+
"environment": "production",
|
74
|
+
"type": "service",
|
75
|
+
"stack": "next",
|
76
|
+
},
|
73
77
|
"production-service-next",
|
78
|
+
id="production-service-next",
|
74
79
|
),
|
75
|
-
(
|
80
|
+
pytest.param(
|
76
81
|
{
|
77
82
|
"environment": "production",
|
78
83
|
"type": "service",
|
@@ -80,8 +85,9 @@ def expected():
|
|
80
85
|
"service": "api",
|
81
86
|
},
|
82
87
|
"production-service-django-api",
|
88
|
+
id="production-service-django-api",
|
83
89
|
),
|
84
|
-
(
|
90
|
+
pytest.param(
|
85
91
|
{
|
86
92
|
"environment": "production",
|
87
93
|
"type": "service",
|
@@ -89,8 +95,9 @@ def expected():
|
|
89
95
|
"service": "admin",
|
90
96
|
},
|
91
97
|
"production-service-django-admin",
|
98
|
+
id="production-service-django-admin",
|
92
99
|
),
|
93
|
-
(
|
100
|
+
pytest.param(
|
94
101
|
{
|
95
102
|
"environment": "production",
|
96
103
|
"type": "job",
|
@@ -98,8 +105,9 @@ def expected():
|
|
98
105
|
"job": "manage",
|
99
106
|
},
|
100
107
|
"production-job-django-manage",
|
108
|
+
id="production-job-django-manage",
|
101
109
|
),
|
102
|
-
(
|
110
|
+
pytest.param(
|
103
111
|
{
|
104
112
|
"environment": "production",
|
105
113
|
"type": "job",
|
@@ -107,9 +115,30 @@ def expected():
|
|
107
115
|
"job": "special-command",
|
108
116
|
},
|
109
117
|
"production-job-django-special-command",
|
118
|
+
id="production-job-django-special-command",
|
110
119
|
),
|
111
120
|
],
|
112
121
|
)
|
113
|
-
def
|
114
|
-
result = toml_combine.combine(
|
122
|
+
def test_full_config(mapping, expected, expected_key):
|
123
|
+
result = toml_combine.combine(config_file=config_file, **mapping)
|
115
124
|
assert result == expected[expected_key]
|
125
|
+
|
126
|
+
|
127
|
+
@pytest.mark.parametrize(
|
128
|
+
"kwargs",
|
129
|
+
[
|
130
|
+
pytest.param({"config_file": config_file}, id="path"),
|
131
|
+
pytest.param({"config_file": str(config_file)}, id="path_str"),
|
132
|
+
pytest.param({"config": config_file.read_text()}, id="text"),
|
133
|
+
pytest.param({"config": toml.loads(config_file.read_text())}, id="parsed"),
|
134
|
+
],
|
135
|
+
)
|
136
|
+
def test_full_load_kwargs(kwargs, expected):
|
137
|
+
result = toml_combine.combine(
|
138
|
+
**kwargs,
|
139
|
+
environment="production",
|
140
|
+
type="service",
|
141
|
+
stack="django",
|
142
|
+
service="api",
|
143
|
+
)
|
144
|
+
assert result == expected["production-service-django-api"]
|
@@ -2,9 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import copy
|
4
4
|
import dataclasses
|
5
|
-
import itertools
|
6
5
|
from collections.abc import Iterable, Mapping, Sequence
|
7
|
-
from functools import partial
|
8
6
|
from typing import Any, TypeVar
|
9
7
|
|
10
8
|
from . import exceptions
|
@@ -60,48 +58,6 @@ def clean_dimensions_dict(
|
|
60
58
|
return result
|
61
59
|
|
62
60
|
|
63
|
-
def override_sort_key(
|
64
|
-
override: Override, dimensions: dict[str, list[str]]
|
65
|
-
) -> tuple[int, ...]:
|
66
|
-
"""
|
67
|
-
We sort overrides before applying them, and they are applied in the order of the
|
68
|
-
sorted list, each override replacing the common values of the previous overrides.
|
69
|
-
|
70
|
-
override_sort_key defines the sort key for overrides that ensures less specific
|
71
|
-
overrides come first:
|
72
|
-
- Overrides with fewer dimensions come first (will be overridden
|
73
|
-
by more specific ones)
|
74
|
-
- If two overrides have the same number of dimensions but define different
|
75
|
-
dimensions, we sort by the definition order of the dimensions.
|
76
|
-
|
77
|
-
Example:
|
78
|
-
dimensions = {"env": ["dev", "prod"], "region": ["us", "eu"]}
|
79
|
-
|
80
|
-
- Override with {"env": "dev"} comes before override with
|
81
|
-
{"env": "dev", "region": "us"} (less specific)
|
82
|
-
- Override with {"env": "dev"} comes before override with {"region": "us"} ("env"
|
83
|
-
is defined before "region" in the dimensions list)
|
84
|
-
|
85
|
-
Parameters:
|
86
|
-
-----------
|
87
|
-
override: An Override object that defines the condition when it applies
|
88
|
-
(override.when)
|
89
|
-
dimensions: The dict of all existing dimensions and their values, in order of
|
90
|
-
definition
|
91
|
-
|
92
|
-
Returns:
|
93
|
-
--------
|
94
|
-
A tuple that supports comparisons. Less specific Overrides should return smaller
|
95
|
-
values and vice versa.
|
96
|
-
"""
|
97
|
-
result = [len(override.when)]
|
98
|
-
for i, dimension in enumerate(dimensions):
|
99
|
-
if dimension in override.when:
|
100
|
-
result.append(i)
|
101
|
-
|
102
|
-
return tuple(result)
|
103
|
-
|
104
|
-
|
105
61
|
T = TypeVar("T", dict, list, str, int, float, bool)
|
106
62
|
|
107
63
|
|
@@ -136,21 +92,27 @@ def extract_keys(config: Any) -> Iterable[tuple[str, ...]]:
|
|
136
92
|
yield tuple()
|
137
93
|
|
138
94
|
|
139
|
-
def
|
140
|
-
|
141
|
-
) ->
|
95
|
+
def are_conditions_compatible(
|
96
|
+
a: Mapping[str, list[str]], b: Mapping[str, list[str]], /
|
97
|
+
) -> bool:
|
142
98
|
"""
|
143
|
-
|
99
|
+
`a` and `b` are dictionaries representing override conditions (`when`). Return
|
100
|
+
`True` if the conditions represented by `a` are compatible with `b`. Conditions are
|
101
|
+
compatible if one is stricly more specific than the other or if they're mutually
|
102
|
+
exclusive.
|
144
103
|
"""
|
145
|
-
|
146
|
-
|
147
|
-
|
104
|
+
# Subset
|
105
|
+
if set(a) < set(b) or set(b) < set(a):
|
106
|
+
return True
|
107
|
+
|
108
|
+
# Disjoint or overlapping sets
|
109
|
+
if set(a) != set(b):
|
110
|
+
return False
|
148
111
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
yield (when_definition, *config_key)
|
112
|
+
# Equal sets: it's only compatible if the values are disjoint
|
113
|
+
if any(set(a[key]) & set(b[key]) for key in a.keys()):
|
114
|
+
return False
|
115
|
+
return True
|
154
116
|
|
155
117
|
|
156
118
|
def build_config(config: dict[str, Any]) -> Config:
|
@@ -161,9 +123,6 @@ def build_config(config: dict[str, Any]) -> Config:
|
|
161
123
|
# Parse template
|
162
124
|
default = config.pop("default", {})
|
163
125
|
|
164
|
-
# The rule is: the same exact set of conditions cannot be used twice to define
|
165
|
-
# the same values (on the same or different overrides)
|
166
|
-
seen_conditions_and_keys = set()
|
167
126
|
overrides = []
|
168
127
|
for override in config.pop("override", []):
|
169
128
|
try:
|
@@ -176,22 +135,10 @@ def build_config(config: dict[str, Any]) -> Config:
|
|
176
135
|
type="override",
|
177
136
|
)
|
178
137
|
|
179
|
-
conditions_and_keys = set(
|
180
|
-
extract_conditions_and_keys(when=when, config=override)
|
181
|
-
)
|
182
|
-
if duplicates := (conditions_and_keys & seen_conditions_and_keys):
|
183
|
-
duplicate_str = ", ".join(sorted(key for *_, key in duplicates))
|
184
|
-
raise exceptions.DuplicateError(id=when, details=duplicate_str)
|
185
|
-
|
186
|
-
seen_conditions_and_keys |= conditions_and_keys
|
187
|
-
|
188
138
|
overrides.append(Override(when=when, config=override))
|
189
139
|
|
190
140
|
# Sort overrides by increasing specificity
|
191
|
-
overrides = sorted(
|
192
|
-
overrides,
|
193
|
-
key=partial(override_sort_key, dimensions=dimensions),
|
194
|
-
)
|
141
|
+
overrides = sorted(overrides, key=lambda override: len(override.when))
|
195
142
|
|
196
143
|
return Config(
|
197
144
|
dimensions=dimensions,
|
@@ -219,11 +166,28 @@ def generate_for_mapping(
|
|
219
166
|
mapping: Mapping[str, str],
|
220
167
|
) -> Mapping[str, Any]:
|
221
168
|
result = copy.deepcopy(config.default)
|
169
|
+
keys_to_conditions: dict[tuple[str, ...], list[Mapping[str, list[str]]]] = {}
|
222
170
|
# Apply each matching override
|
223
171
|
for override in config.overrides:
|
224
172
|
# Check if all dimension values in the override match
|
225
173
|
|
226
174
|
if mapping_matches_override(mapping=mapping, override=override):
|
175
|
+
# Check that all applicableoverrides are compatible
|
176
|
+
keys = extract_keys(override.config)
|
177
|
+
|
178
|
+
for key in keys:
|
179
|
+
previous_conditions = keys_to_conditions.setdefault(key, [])
|
180
|
+
|
181
|
+
for previous_condition in previous_conditions:
|
182
|
+
if not are_conditions_compatible(previous_condition, override.when):
|
183
|
+
raise exceptions.IncompatibleOverrides(
|
184
|
+
id=override.when,
|
185
|
+
key=".".join(key),
|
186
|
+
other_override=previous_condition,
|
187
|
+
)
|
188
|
+
|
189
|
+
keys_to_conditions[key].append(override.when)
|
190
|
+
|
227
191
|
result = merge_configs(result, override.config)
|
228
192
|
|
229
193
|
return result
|
@@ -19,8 +19,8 @@ class TomlEncodeError(TomlCombineError):
|
|
19
19
|
"""Error while encoding configuration file."""
|
20
20
|
|
21
21
|
|
22
|
-
class
|
23
|
-
"""In override {id}: Overrides
|
22
|
+
class IncompatibleOverrides(TomlCombineError):
|
23
|
+
"""In override {id}: Overrides defining the same configuration keys must be included in one another or mutually exclusive.\nKey defined multiple times: {key}\nOther override: {other_override}"""
|
24
24
|
|
25
25
|
|
26
26
|
class DimensionNotFound(TomlCombineError):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|