toml-combine 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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/combiner.py CHANGED
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import copy
4
4
  import dataclasses
5
- from collections.abc import Mapping, Sequence
5
+ import itertools
6
+ from collections.abc import Iterable, Mapping, Sequence
6
7
  from functools import partial
7
8
  from typing import Any, TypeVar
8
9
 
@@ -80,6 +81,18 @@ def override_sort_key(
80
81
  {"env": "dev", "region": "us"} (less specific)
81
82
  - Override with {"env": "dev"} comes before override with {"region": "us"} ("env"
82
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.
83
96
  """
84
97
  result = [len(override.when)]
85
98
  for i, dimension in enumerate(dimensions):
@@ -111,6 +124,35 @@ def merge_configs(a: T, b: T, /) -> T:
111
124
  return result
112
125
 
113
126
 
127
+ def extract_keys(config: Any) -> Iterable[tuple[str, ...]]:
128
+ """
129
+ Extract the keys from a config.
130
+ """
131
+ if isinstance(config, dict):
132
+ for key, value in config.items():
133
+ for sub_key in extract_keys(value):
134
+ yield (key, *sub_key)
135
+ else:
136
+ yield tuple()
137
+
138
+
139
+ def extract_conditions_and_keys(
140
+ when: dict[str, list[str]], config: dict[str, Any]
141
+ ) -> Iterable[tuple[Any, ...]]:
142
+ """
143
+ Extract the definitions from an override.
144
+ """
145
+ when_definitions = []
146
+ for key, values in when.items():
147
+ when_definitions.append([(key, value) for value in values])
148
+
149
+ when_combined_definitions = list(itertools.product(*when_definitions))
150
+ config_keys = extract_keys(config)
151
+ for config_key in config_keys:
152
+ for when_definition in when_combined_definitions:
153
+ yield (when_definition, *config_key)
154
+
155
+
114
156
  def build_config(config: dict[str, Any]) -> Config:
115
157
  config = copy.deepcopy(config)
116
158
  # Parse dimensions
@@ -119,7 +161,9 @@ def build_config(config: dict[str, Any]) -> Config:
119
161
  # Parse template
120
162
  default = config.pop("default", {})
121
163
 
122
- seen_conditions = set()
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()
123
167
  overrides = []
124
168
  for override in config.pop("override", []):
125
169
  try:
@@ -132,20 +176,17 @@ def build_config(config: dict[str, Any]) -> Config:
132
176
  type="override",
133
177
  )
134
178
 
135
- conditions = tuple((k, tuple(v)) for k, v in when.items())
136
- if conditions in seen_conditions:
137
- raise exceptions.DuplicateError(type="override", id=when)
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)
138
185
 
139
- seen_conditions.add(conditions)
186
+ seen_conditions_and_keys |= conditions_and_keys
187
+
188
+ overrides.append(Override(when=when, config=override))
140
189
 
141
- overrides.append(
142
- Override(
143
- when=clean_dimensions_dict(
144
- to_sort=when, clean=dimensions, type="override"
145
- ),
146
- config=override,
147
- )
148
- )
149
190
  # Sort overrides by increasing specificity
150
191
  overrides = sorted(
151
192
  overrides,
@@ -159,7 +200,7 @@ def build_config(config: dict[str, Any]) -> Config:
159
200
  )
160
201
 
161
202
 
162
- def mapping_matches_override(mapping: dict[str, str], override: Override) -> bool:
203
+ def mapping_matches_override(mapping: Mapping[str, str], override: Override) -> bool:
163
204
  """
164
205
  Check if the values in the override match the given dimensions.
165
206
  """
@@ -174,13 +215,12 @@ def mapping_matches_override(mapping: dict[str, str], override: Override) -> boo
174
215
 
175
216
 
176
217
  def generate_for_mapping(
177
- default: Mapping[str, Any],
178
- overrides: Sequence[Override],
179
- mapping: dict[str, str],
180
- ) -> dict[str, Any]:
181
- result = copy.deepcopy(default)
218
+ config: Config,
219
+ mapping: Mapping[str, str],
220
+ ) -> Mapping[str, Any]:
221
+ result = copy.deepcopy(config.default)
182
222
  # Apply each matching override
183
- for override in overrides:
223
+ for override in config.overrides:
184
224
  # Check if all dimension values in the override match
185
225
 
186
226
  if mapping_matches_override(mapping=mapping, override=override):
@@ -20,7 +20,7 @@ class TomlEncodeError(TomlCombineError):
20
20
 
21
21
 
22
22
  class DuplicateError(TomlCombineError):
23
- """In {type} {id}: Cannot have multiple {type}s with the same dimensions."""
23
+ """In override {id}: Overrides with the same dimensions cannot define the same configuration keys: {details}"""
24
24
 
25
25
 
26
26
  class DimensionNotFound(TomlCombineError):
toml_combine/lib.py CHANGED
@@ -52,7 +52,6 @@ def combine(*, config=None, config_file=None, **mapping):
52
52
  config_obj = combiner.build_config(dict_config)
53
53
 
54
54
  return combiner.generate_for_mapping(
55
- default=config_obj.default,
56
- overrides=config_obj.overrides,
55
+ config=config_obj,
57
56
  mapping=mapping,
58
57
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toml-combine
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -29,7 +29,7 @@ parts that are common to everyone.
29
29
 
30
30
  ### The config file
31
31
 
32
- The configuration file is usually a TOML file. Here's a small example:
32
+ The configuration file is (usually) a TOML file. Here's a small example:
33
33
 
34
34
  ```toml
35
35
  [dimensions]
@@ -65,11 +65,14 @@ Overrides define a set of condition where they apply (`when.<dimension> =
65
65
  "<value>"`) and the values that are overriden. Overrides are applied in order from less
66
66
  specific to more specific, each one overriding the values of the previous ones:
67
67
 
68
- - If an override contains conditions on more dimensions than another one, it's applied
69
- later
70
- - In case 2 overrides contain the same number of dimensions and they're a disjoint set,
71
- then it depends on how the dimensions are defined at the top of the file: dimensions
72
- defined last have a greater priority
68
+ - In case 2 overrides are applicable, the more specific one (the one with more
69
+ dimensions defined) has greater priority
70
+ - In case 2 overrides use the same number of dimensions, then it depends on how the
71
+ dimensions are defined at the top of the file: dimensions defined last have a greater
72
+ priority
73
+ - In case 2 overrides use the same dimensions, if they define the same configuration
74
+ values, an error will be raised. If they define different configuation values, then
75
+ the priority is irrelevant.
73
76
 
74
77
  > [!Note]
75
78
  > Defining a list as the value of one or more conditions in an override
@@ -141,9 +144,9 @@ Example with the config from the previous section:
141
144
 
142
145
  ```console
143
146
  $ toml-combine path/to/config.toml --environment=staging
144
- [dimensions]
145
- environment = "staging"
147
+ ```
146
148
 
149
+ ```toml
147
150
  [fruits]
148
151
  apple.color = "red"
149
152
  orange.color = "orange"
@@ -202,6 +205,9 @@ This produces the following configs:
202
205
 
203
206
  ```console
204
207
  $ toml-combine example.toml --environment=production --service=frontend
208
+ ```
209
+
210
+ ```toml
205
211
  registry = "gcr.io/my-project/"
206
212
  service_account = "my-service-account"
207
213
  name = "service-frontend"
@@ -212,6 +218,9 @@ image_name = "my-image-frontend"
212
218
 
213
219
  ```console
214
220
  $ toml-combine example.toml --environment=production --service=backend
221
+ ```
222
+
223
+ ```toml
215
224
  registry = "gcr.io/my-project/"
216
225
  service_account = "my-service-account"
217
226
  name = "service-backend"
@@ -223,6 +232,9 @@ port = 8080
223
232
 
224
233
  ```console
225
234
  $ toml-combine example.toml --environment=staging --service=frontend
235
+ ```
236
+
237
+ ```toml
226
238
  registry = "gcr.io/my-project/"
227
239
  service_account = "my-service-account"
228
240
  name = "service-frontend"
@@ -233,6 +245,9 @@ image_name = "my-image-frontend"
233
245
 
234
246
  ```console
235
247
  $ toml-combine example.toml --environment=staging --service=backend
248
+ ```
249
+
250
+ ```toml
236
251
  registry = "gcr.io/my-project/"
237
252
  service_account = "my-service-account"
238
253
  name = "service-backend"
@@ -247,6 +262,9 @@ ENABLE_EXPENSIVE_MONITORING = false
247
262
 
248
263
  ```console
249
264
  $ toml-combine example.toml --environment=dev --service=backend
265
+ ```
266
+
267
+ ```toml
250
268
  registry = "gcr.io/my-project/"
251
269
  service_account = "my-service-account"
252
270
  name = "service-backend"
@@ -257,5 +275,4 @@ port = 8080
257
275
  [container.env]
258
276
  DEBUG = true
259
277
  ENABLE_EXPENSIVE_MONITORING = false
260
-
261
278
  ```
@@ -0,0 +1,11 @@
1
+ toml_combine/__init__.py,sha256=TDkOwwEM-nS6hOh79u9Qae6g2Q6VfANpPpnKGfSgu80,84
2
+ toml_combine/__main__.py,sha256=hmF8N8xX6UEApzbKTVZ-4E1HU5-rjgUkdXNLO-mF6vo,100
3
+ toml_combine/cli.py,sha256=hG03eDKz7xU-ydJIa1kDuu6WlFzNS3GTMJ6zals9M9c,2843
4
+ toml_combine/combiner.py,sha256=BCsZOm7Cr2_JxttsKzjJH_HYQkD2XPjylXKlkxvi1EY,6974
5
+ toml_combine/exceptions.py,sha256=Qg_gGIdXcwTmWDlIfJOidXkViBOVSPdLx0WOELxFPp0,1026
6
+ toml_combine/lib.py,sha256=jh6OG57JefpGa-WE-mLSIK6KjyJ0-1yGBynr_kiVTww,1634
7
+ toml_combine/toml.py,sha256=iBV8xj0qWcvGp2AZaML8FCT3i2X9DL7iA6jd-wcP5Bc,814
8
+ toml_combine-0.5.0.dist-info/METADATA,sha256=XRrnb4YkB_wwoGdoZBDQBGoXi4qY9FmjBhKGQVAJjc4,7585
9
+ toml_combine-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ toml_combine-0.5.0.dist-info/entry_points.txt,sha256=dXUQNom54uZt_7ylEG81iNYMamYpaFo9-ItcZJU6Uzc,58
11
+ toml_combine-0.5.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- toml_combine/__init__.py,sha256=TDkOwwEM-nS6hOh79u9Qae6g2Q6VfANpPpnKGfSgu80,84
2
- toml_combine/__main__.py,sha256=hmF8N8xX6UEApzbKTVZ-4E1HU5-rjgUkdXNLO-mF6vo,100
3
- toml_combine/cli.py,sha256=hG03eDKz7xU-ydJIa1kDuu6WlFzNS3GTMJ6zals9M9c,2843
4
- toml_combine/combiner.py,sha256=RhhCevncnVvxFYNywvtVWkVMpiqtF0mq_APjg76Tg4Q,5546
5
- toml_combine/exceptions.py,sha256=tAFTDRSg6d10bBruBhsasZXrNNgLTmr_nKfvIsRR_yU,991
6
- toml_combine/lib.py,sha256=Iw7F8SCyQMlhaqSD2vtnmM6jbnrgzCZeX0d-LTM3VVg,1683
7
- toml_combine/toml.py,sha256=iBV8xj0qWcvGp2AZaML8FCT3i2X9DL7iA6jd-wcP5Bc,814
8
- toml_combine-0.4.0.dist-info/METADATA,sha256=WKLq2pGpPRwfLhdpBuqxoAtCwVKXipgcega_4GTl_r4,7347
9
- toml_combine-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- toml_combine-0.4.0.dist-info/entry_points.txt,sha256=dXUQNom54uZt_7ylEG81iNYMamYpaFo9-ItcZJU6Uzc,58
11
- toml_combine-0.4.0.dist-info/RECORD,,