toml-combine 1.0.1__tar.gz → 1.0.3__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.
Files changed (23) hide show
  1. toml_combine-1.0.3/.github/release.yml +6 -0
  2. {toml_combine-1.0.1 → toml_combine-1.0.3}/.github/workflows/ci.yml +2 -2
  3. {toml_combine-1.0.1 → toml_combine-1.0.3}/.pre-commit-config.yaml +2 -2
  4. {toml_combine-1.0.1 → toml_combine-1.0.3}/CONTRIBUTING.md +23 -4
  5. {toml_combine-1.0.1 → toml_combine-1.0.3}/PKG-INFO +112 -23
  6. {toml_combine-1.0.1 → toml_combine-1.0.3}/README.md +111 -22
  7. {toml_combine-1.0.1 → toml_combine-1.0.3}/tests/test_combiner.py +3 -4
  8. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/cli.py +2 -2
  9. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/combiner.py +13 -1
  10. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/exceptions.py +1 -1
  11. {toml_combine-1.0.1 → toml_combine-1.0.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  12. {toml_combine-1.0.1 → toml_combine-1.0.3}/.github/renovate.json5 +0 -0
  13. {toml_combine-1.0.1 → toml_combine-1.0.3}/LICENSE +0 -0
  14. {toml_combine-1.0.1 → toml_combine-1.0.3}/pyproject.toml +0 -0
  15. {toml_combine-1.0.1 → toml_combine-1.0.3}/tests/result.json +0 -0
  16. {toml_combine-1.0.1 → toml_combine-1.0.3}/tests/test.toml +0 -0
  17. {toml_combine-1.0.1 → toml_combine-1.0.3}/tests/test_cli.py +0 -0
  18. {toml_combine-1.0.1 → toml_combine-1.0.3}/tests/test_lib.py +0 -0
  19. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/__init__.py +0 -0
  20. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/__main__.py +0 -0
  21. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/lib.py +0 -0
  22. {toml_combine-1.0.1 → toml_combine-1.0.3}/toml_combine/toml.py +0 -0
  23. {toml_combine-1.0.1 → toml_combine-1.0.3}/uv.lock +0 -0
@@ -0,0 +1,6 @@
1
+ changelog:
2
+ exclude:
3
+ authors:
4
+ - pre-commit-ci
5
+ - renovate
6
+ - dependabot
@@ -29,7 +29,7 @@ jobs:
29
29
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
30
30
 
31
31
  - name: Install the latest version of uv
32
- uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6
32
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6
33
33
  with:
34
34
  python-version: ${{ matrix.python-version }}
35
35
 
@@ -53,7 +53,7 @@ jobs:
53
53
  ref: ${{ github.ref }}
54
54
 
55
55
  - name: Install the latest version of uv
56
- uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6
56
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6
57
57
 
58
58
  - name: Build wheel & sdist
59
59
  run: uv build
@@ -22,11 +22,11 @@ repos:
22
22
  - id: mixed-line-ending
23
23
  - repo: https://github.com/astral-sh/uv-pre-commit
24
24
  # uv version.
25
- rev: 0.7.4
25
+ rev: 0.7.7
26
26
  hooks:
27
27
  - id: uv-lock
28
28
  - repo: https://github.com/astral-sh/ruff-pre-commit
29
- rev: v0.11.10
29
+ rev: v0.11.11
30
30
  hooks:
31
31
  - id: ruff
32
32
  args: [--fix, --unsafe-fixes, --show-fixes]
@@ -14,21 +14,40 @@ or using a package manager, such as [brew](https://brew.sh/):
14
14
  $ brew install uv
15
15
  ```
16
16
 
17
- Then you can directly launch the tests with:
17
+ ### Work with UV (will handle the venv for you)
18
+
19
+ **Run tests**
18
20
 
19
21
  ```console
20
22
  $ uv run pytest
21
23
  ```
22
24
 
23
- or create a virtual environment to work on the package and activate it:
25
+ **Run the command**
26
+
27
+ ```console
28
+ $ uv run toml-combine --help
29
+ ```
30
+
31
+ ### Work with a classic virtual environment
32
+
33
+ **Create and activate a virtual environment**
24
34
 
25
35
  ```console
26
36
  $ uv sync
27
37
  $ source .venv/bin/activate
28
38
  ```
29
39
 
30
- or launch the command itself with uv (no need to activate the venv):
40
+ **Run tests**
31
41
 
32
42
  ```console
33
- $ uv run toml-combine --help
43
+ $ pytest
44
+ ```
45
+
46
+ **Run the command**
47
+
48
+ ```console
49
+ $ toml-combine --help
34
50
  ```
51
+
52
+ > [!NOTE]
53
+ > In this venv, if you need `pip` you can use `uv pip` instead, it works the same.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toml-combine
3
- Version: 1.0.1
3
+ Version: 1.0.3
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
@@ -47,7 +47,10 @@ name = "my-service"
47
47
  registry = "gcr.io/my-project/"
48
48
  container.image_name = "my-image"
49
49
  container.port = 8080
50
- service_account = "my-service-account"
50
+
51
+ [[override]]
52
+ when.environment = "production"
53
+ service_account = "my-production-service-account"
51
54
 
52
55
  [[override]]
53
56
  when.environment = "staging"
@@ -68,29 +71,115 @@ The common configuration to start from, before we start overlaying overrides on
68
71
 
69
72
  ### Overrides
70
73
 
71
- Overrides define a set of condition where they apply (`when.<dimension> =
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 2 applicable overrides both define a dimension that the other one doesn't, they're
78
- incompatible, and running the tool with a configuration that would select both of them
79
- will yield an error.
80
-
81
- Examples:
82
- - Override 1: `env=staging` & Override 2: `region=eu` are incompatible (1 defines
83
- `env` not in 2, 2 defines `region` not in 1).
84
- - Override 1: `env=staging` & Override 2: `env=staging, region=eu` are compatible
85
- (all dimensions defined in 1 are also in 2)
86
- - Override 1: `env=staging` & Override 2: `env=prod` are compatible
87
- (they define the same dimensions)
88
- - Override 1: `env=staging, service=frontend` & Override 2: `region=eu, service=frontend`
89
- are incompatible (1 defines `env` not in 2, 2 defines `region` not in 1)
90
-
91
- > [!Note]
74
+ Each override defines a set of condition where it applies (`when.<dimension> =
75
+ "<dimension_value>"`) and a set of overridden key/values.
76
+
77
+ ```toml
78
+ [[override]]
79
+ # Keys starting with `when.` are "conditions"
80
+ when.environment = "staging"
81
+ when.region = "us"
82
+
83
+ # Other keys in an override are "overridden keys" / "overridden values"
84
+ service_account = "my-us-staging-service-account"
85
+ ```
86
+
87
+ If you run `toml-combine` with a given mapping that selects multiple overrides, they
88
+ will be checked for _compatibility_ with one another, and an error will be raised if
89
+ they're _not compatible_.
90
+
91
+ Compatibility rules:
92
+
93
+ - If the two overrides don't share any _overridden key_, then they're always compatible.
94
+
95
+ <details>
96
+ <summary>Example (click to expand)</summary>
97
+
98
+ ```toml
99
+ [dimensions]
100
+ environment = ["staging"]
101
+ region = ["eu"]
102
+
103
+ [[override]]
104
+ when.environment = "staging"
105
+ service_account = "my-staging-service-account"
106
+
107
+ [[override]]
108
+ when.region = "eu"
109
+ env.CURRENCY = "EUR"
110
+ ```
111
+
112
+ </details>
113
+
114
+ - If an override defines a set of conditions (say `env=prod`) and the other one defines
115
+ strictly more conditions (say `env=prod, region=eu`, in other words, it defines all
116
+ the conditions of the first override and then some more), then they're compatible.
117
+ Also, in that case, **the override with more conditions will have precedence**.
118
+
119
+ <details>
120
+ <summary>Example</summary>
121
+
122
+ ```toml
123
+ [dimensions]
124
+ environment = ["staging"]
125
+ region = ["eu"]
126
+
127
+ [[override]]
128
+ when.environment = "staging"
129
+ service_account = "my-staging-service-account"
130
+
131
+ [[override]]
132
+ when.environment = "staging"
133
+ when.region = "eu"
134
+ service_account = "my-staging-eu-service-account"
135
+ ```
136
+
137
+ </details>
138
+
139
+ - If they both define a dimension that the other one doesn't, they're incompatible.
140
+
141
+ <details>
142
+ <summary>Example (click to expand)</summary>
143
+
144
+ Incompatible overrides: neither is a subset of the other one and they both
145
+ define a value for `service_account`:
146
+
147
+ ```toml
148
+ [dimensions]
149
+ environment = ["staging"]
150
+ region = ["eu"]
151
+
152
+ [default]
153
+ service_account = "my-service-account"
154
+
155
+ [[override]]
156
+ when.environment = "staging"
157
+ service_account = "my-staging-service-account"
158
+
159
+ [[override]]
160
+ when.region = "eu"
161
+ service_account = "my-eu-service-account"
162
+ ```
163
+
164
+ ```console
165
+ $ toml-combine config.toml --environment=staging --region=eu
166
+ Error: Incompatible overrides `{'region': ['eu']}` and `{'environment': ['staging']}`:
167
+ When they're both applicable, overrides defining a common overridden key (foo) must be
168
+ a subset of one another
169
+ ```
170
+
171
+ > [!NOTE]
172
+ > It's ok to have incompatible overrides in your config as long as you don't
173
+ > run `toml-combine` with a mapping that would select both of them. In the example
174
+ > above, if you run `toml-combine --environment=staging --region=eu`, the error
175
+ > will be triggered, but you can run `toml-combine --environment=staging`.
176
+
177
+ </details>
178
+
179
+ > [!NOTE]
92
180
  > Instead of defining a single value for the override dimensions, you can define a list.
93
181
  > This is a shortcut to duplicating the override with each individual value:
182
+ >
94
183
  > ```
95
184
  > [[override]]
96
185
  > when.environment = ["staging", "prod"]
@@ -27,7 +27,10 @@ name = "my-service"
27
27
  registry = "gcr.io/my-project/"
28
28
  container.image_name = "my-image"
29
29
  container.port = 8080
30
- service_account = "my-service-account"
30
+
31
+ [[override]]
32
+ when.environment = "production"
33
+ service_account = "my-production-service-account"
31
34
 
32
35
  [[override]]
33
36
  when.environment = "staging"
@@ -48,29 +51,115 @@ The common configuration to start from, before we start overlaying overrides on
48
51
 
49
52
  ### Overrides
50
53
 
51
- Overrides define a set of condition where they apply (`when.<dimension> =
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 2 applicable overrides both define a dimension that the other one doesn't, they're
58
- incompatible, and running the tool with a configuration that would select both of them
59
- will yield an error.
60
-
61
- Examples:
62
- - Override 1: `env=staging` & Override 2: `region=eu` are incompatible (1 defines
63
- `env` not in 2, 2 defines `region` not in 1).
64
- - Override 1: `env=staging` & Override 2: `env=staging, region=eu` are compatible
65
- (all dimensions defined in 1 are also in 2)
66
- - Override 1: `env=staging` & Override 2: `env=prod` are compatible
67
- (they define the same dimensions)
68
- - Override 1: `env=staging, service=frontend` & Override 2: `region=eu, service=frontend`
69
- are incompatible (1 defines `env` not in 2, 2 defines `region` not in 1)
70
-
71
- > [!Note]
54
+ Each override defines a set of condition where it applies (`when.<dimension> =
55
+ "<dimension_value>"`) and a set of overridden key/values.
56
+
57
+ ```toml
58
+ [[override]]
59
+ # Keys starting with `when.` are "conditions"
60
+ when.environment = "staging"
61
+ when.region = "us"
62
+
63
+ # Other keys in an override are "overridden keys" / "overridden values"
64
+ service_account = "my-us-staging-service-account"
65
+ ```
66
+
67
+ If you run `toml-combine` with a given mapping that selects multiple overrides, they
68
+ will be checked for _compatibility_ with one another, and an error will be raised if
69
+ they're _not compatible_.
70
+
71
+ Compatibility rules:
72
+
73
+ - If the two overrides don't share any _overridden key_, then they're always compatible.
74
+
75
+ <details>
76
+ <summary>Example (click to expand)</summary>
77
+
78
+ ```toml
79
+ [dimensions]
80
+ environment = ["staging"]
81
+ region = ["eu"]
82
+
83
+ [[override]]
84
+ when.environment = "staging"
85
+ service_account = "my-staging-service-account"
86
+
87
+ [[override]]
88
+ when.region = "eu"
89
+ env.CURRENCY = "EUR"
90
+ ```
91
+
92
+ </details>
93
+
94
+ - If an override defines a set of conditions (say `env=prod`) and the other one defines
95
+ strictly more conditions (say `env=prod, region=eu`, in other words, it defines all
96
+ the conditions of the first override and then some more), then they're compatible.
97
+ Also, in that case, **the override with more conditions will have precedence**.
98
+
99
+ <details>
100
+ <summary>Example</summary>
101
+
102
+ ```toml
103
+ [dimensions]
104
+ environment = ["staging"]
105
+ region = ["eu"]
106
+
107
+ [[override]]
108
+ when.environment = "staging"
109
+ service_account = "my-staging-service-account"
110
+
111
+ [[override]]
112
+ when.environment = "staging"
113
+ when.region = "eu"
114
+ service_account = "my-staging-eu-service-account"
115
+ ```
116
+
117
+ </details>
118
+
119
+ - If they both define a dimension that the other one doesn't, they're incompatible.
120
+
121
+ <details>
122
+ <summary>Example (click to expand)</summary>
123
+
124
+ Incompatible overrides: neither is a subset of the other one and they both
125
+ define a value for `service_account`:
126
+
127
+ ```toml
128
+ [dimensions]
129
+ environment = ["staging"]
130
+ region = ["eu"]
131
+
132
+ [default]
133
+ service_account = "my-service-account"
134
+
135
+ [[override]]
136
+ when.environment = "staging"
137
+ service_account = "my-staging-service-account"
138
+
139
+ [[override]]
140
+ when.region = "eu"
141
+ service_account = "my-eu-service-account"
142
+ ```
143
+
144
+ ```console
145
+ $ toml-combine config.toml --environment=staging --region=eu
146
+ Error: Incompatible overrides `{'region': ['eu']}` and `{'environment': ['staging']}`:
147
+ When they're both applicable, overrides defining a common overridden key (foo) must be
148
+ a subset of one another
149
+ ```
150
+
151
+ > [!NOTE]
152
+ > It's ok to have incompatible overrides in your config as long as you don't
153
+ > run `toml-combine` with a mapping that would select both of them. In the example
154
+ > above, if you run `toml-combine --environment=staging --region=eu`, the error
155
+ > will be triggered, but you can run `toml-combine --environment=staging`.
156
+
157
+ </details>
158
+
159
+ > [!NOTE]
72
160
  > Instead of defining a single value for the override dimensions, you can define a list.
73
161
  > This is a shortcut to duplicating the override with each individual value:
162
+ >
74
163
  > ```
75
164
  > [[override]]
76
165
  > when.environment = ["staging", "prod"]
@@ -390,8 +390,7 @@ def test_generate_for_mapping__duplicate_overrides_list():
390
390
  # Message is a bit complex so we test it too.
391
391
  assert (
392
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']}"
393
+ == "Incompatible overrides `{'env': ['prod', 'dev']}` and `{'env': ['prod']}`: "
394
+ "When they're both applicable, overrides defining a common overridden key (hello.world) "
395
+ "must be a subset of one another"
397
396
  )
@@ -70,7 +70,7 @@ def cli(argv) -> int:
70
70
  try:
71
71
  config = combiner.build_config(dict_config)
72
72
  except exceptions.TomlCombineError as exc:
73
- print(exc, file=sys.stderr)
73
+ print(f"Error: {exc}", file=sys.stderr)
74
74
  return 1
75
75
 
76
76
  # Parse all arguments
@@ -86,7 +86,7 @@ def cli(argv) -> int:
86
86
  try:
87
87
  result = lib.combine(config=dict_config, **mapping)
88
88
  except exceptions.TomlCombineError as exc:
89
- print(exc, file=sys.stderr)
89
+ print(f"Error: {exc}", file=sys.stderr)
90
90
  return 1
91
91
 
92
92
  if args.format == "toml":
@@ -21,6 +21,7 @@ class Override:
21
21
  class Config:
22
22
  dimensions: Mapping[str, list[str]]
23
23
  default: Mapping[str, Any]
24
+ # List of overrides, in order of increasing specificity
24
25
  overrides: Sequence[Override]
25
26
 
26
27
 
@@ -30,6 +31,7 @@ def clean_dimensions_dict(
30
31
  """
31
32
  Recreate a dictionary of dimension values with the same order as the
32
33
  dimensions list.
34
+ Also check that the values are valid.
33
35
  """
34
36
  result = {}
35
37
  if invalid_dimensions := set(to_sort) - set(clean):
@@ -63,7 +65,7 @@ T = TypeVar("T", dict, list, str, int, float, bool)
63
65
 
64
66
  def merge_configs(a: T, b: T, /) -> T:
65
67
  """
66
- Recursively merge two configuration dictionaries, with b taking precedence.
68
+ Recursively merge two configuration dictionaries a and b, with b taking precedence.
67
69
  """
68
70
  if isinstance(a, dict) != isinstance(b, dict):
69
71
  raise ValueError(f"Cannot merge {type(a)} with {type(b)}")
@@ -116,6 +118,9 @@ def are_conditions_compatible(
116
118
 
117
119
 
118
120
  def build_config(config: dict[str, Any]) -> Config:
121
+ """
122
+ Build a finalized Config object from the given configuration dictionary.
123
+ """
119
124
  config = copy.deepcopy(config)
120
125
  # Parse dimensions
121
126
  dimensions = config.pop("dimensions")
@@ -165,6 +170,13 @@ def generate_for_mapping(
165
170
  config: Config,
166
171
  mapping: Mapping[str, str],
167
172
  ) -> Mapping[str, Any]:
173
+ """
174
+ Generate a configuration based on the provided mapping of dimension values.
175
+ The mapping should contain only the dimensions defined in the config.
176
+ If a dimension is not defined in the mapping, the default value for that
177
+ dimension will be used.
178
+ """
179
+
168
180
  result = copy.deepcopy(config.default)
169
181
  keys_to_conditions: dict[tuple[str, ...], list[Mapping[str, list[str]]]] = {}
170
182
  # Apply each matching override
@@ -20,7 +20,7 @@ class TomlEncodeError(TomlCombineError):
20
20
 
21
21
 
22
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}"""
23
+ """Incompatible overrides `{id}` and `{other_override}`: When they're both applicable, overrides defining a common overridden key ({key}) must be a subset of one another"""
24
24
 
25
25
 
26
26
  class DimensionNotFound(TomlCombineError):
File without changes
File without changes