confarg 0.0.1.dev5__tar.gz → 0.0.1.dev6__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 (34) hide show
  1. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/PKG-INFO +47 -42
  2. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/README.md +46 -41
  3. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/pyproject.toml +3 -2
  4. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/__init__.py +27 -0
  5. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_api.py +10 -10
  6. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_parse_cli.py +675 -669
  7. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_parse_env.py +65 -5
  8. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_serialize.py +22 -1
  9. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_types.py +32 -1
  10. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/_build.py +107 -24
  11. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/_completion.py +12 -12
  12. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/_namespace.py +91 -9
  13. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/_register.py +4 -4
  14. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/click/_completion.py +4 -4
  15. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/click/_context.py +6 -6
  16. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/click/_register.py +4 -4
  17. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/cyclopts/_context.py +11 -11
  18. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/cyclopts/_register.py +7 -4
  19. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/typedload/_coerce.py +17 -9
  20. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/typedload/_construct.py +92 -4
  21. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_callable.py +0 -0
  22. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_defaults.py +0 -0
  23. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_files.py +0 -0
  24. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_import.py +0 -0
  25. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/_merge.py +0 -0
  26. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/__init__.py +0 -0
  27. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/__init__.py +0 -0
  28. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/argparse/_spec.py +0 -0
  29. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/click/__init__.py +0 -0
  30. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/cli/cyclopts/__init__.py +0 -0
  31. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/dictexpr/__init__.py +0 -0
  32. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/dictexpr/_expressions.py +0 -0
  33. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/exceptions.py +0 -0
  34. {confarg-0.0.1.dev5 → confarg-0.0.1.dev6}/src/confarg/typedload/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confarg
3
- Version: 0.0.1.dev5
3
+ Version: 0.0.1.dev6
4
4
  Summary: Load and resolve complex configurations from files, environment variables and command line arguments. Keep your favorite CLI library.
5
5
  Author: confarg
6
6
  Author-email: confarg <280620574+confarg@users.noreply.github.com>
@@ -12,6 +12,11 @@ Project-URL: Repository, https://github.com/confarg/confarg
12
12
  Provides-Extra: completion
13
13
  Description-Content-Type: text/markdown
14
14
 
15
+ <!-- pytest-markdown-console-file: notest -->
16
+ <p align="center">
17
+ <img src="docs/assets/banner.svg" alt="confarg" />
18
+ </p>
19
+
15
20
  # A tool to manage complex configurations
16
21
 
17
22
  > Load and resolve complex configurations from files, environment variables and command line arguments. Keep your data structures and favorite CLI library.
@@ -20,7 +25,7 @@ Description-Content-Type: text/markdown
20
25
 
21
26
  It can handle deeply nested configurations, type unions, derived classes, expressions and variable interpolation, configuration compositions and more, and can integrate with your favorite argument parser library such as `argparse`, `click` or `typer`.
22
27
 
23
- `confarg` is not a framework. No decorator, base class or special annotation type required: none are provided. It is just a tool for the deserialization and serialization of complex configurations. Its footprint in your code is typically a few lines of code, making it easy to switch to it, or away from it.
28
+ `confarg` is not a framework. No decorator, base class or special annotation type required: none are provided. It is just a tool for the deserialization and serialization of complex configurations. Its footprint in your code is typically a few lines of code.
24
29
 
25
30
  ## Install
26
31
 
@@ -95,46 +100,46 @@ This allows you to construct a `DBConfig` object by collecting data from three p
95
100
 
96
101
  1. From a configuration file. By passing `--config <config_file>` to your app, `confarg` will load the content of the file and fill the `DBConfig` object. For example, a config file could look like so:
97
102
 
98
- ```yaml
99
- # config.yaml
100
- host: example.com
101
- port: 1234
102
- name: mydb
103
- ```
103
+ ```yaml
104
+ # config.yaml
105
+ host: example.com
106
+ port: 1234
107
+ name: mydb
108
+ ```
104
109
 
105
- You would then call your application as
110
+ You would then call your application as
106
111
 
107
- ```console notest
108
- $ myapp.py --config config.yaml
109
- DBConfig(host='example.com', port=1234, name='mydb')
110
- ```
112
+ ```console
113
+ $ myapp.py --config config.yaml
114
+ DBConfig(host='example.com', port=1234, name='mydb')
115
+ ```
111
116
 
112
- Configuration files in TOML and JSON formats are also supported.
117
+ Configuration files in TOML and JSON formats are also supported.
113
118
 
114
- > You can change the default `config` flag to something else using the `config_flag` parameter.
119
+ > You can change the default `config` flag to something else using the `config_flag` parameter.
115
120
 
116
121
  2. From environment variables. You can declare
117
122
 
118
- ```properties
119
- MYAPP_HOST=example.com
120
- MYAPP_PORT=1234
121
- MYAPP_NAME=mydb
122
- ```
123
+ ```properties
124
+ MYAPP_HOST=example.com
125
+ MYAPP_PORT=1234
126
+ MYAPP_NAME=mydb
127
+ ```
123
128
 
124
- for the same effect.
129
+ for the same effect.
125
130
 
126
- > Note that the environment variable prefix of your app should actually be passed to `confarg.load` like so:
127
- >
128
- > ```python
129
- > db_config = confarg.load(DBConfig, env_prefix="MYAPP_")
130
- > ```
131
+ > Note that the environment variable prefix of your app should actually be passed to `confarg.load` like so:
132
+ >
133
+ > ```python
134
+ > db_config = confarg.load(DBConfig, env_prefix="MYAPP_")
135
+ > ```
131
136
 
132
137
  3. From command line arguments.
133
138
 
134
- ```console notest
135
- $ my_app --host example.com --port 1234 --name mydb
136
- DBConfig(host='example.com', port=1234, name='mydb')
137
- ```
139
+ ```console
140
+ $ my_app --host example.com --port 1234 --name mydb
141
+ DBConfig(host='example.com', port=1234, name='mydb')
142
+ ```
138
143
 
139
144
  ### Progressive build-up
140
145
 
@@ -152,7 +157,7 @@ port: 1234
152
157
 
153
158
  and provide the schema name from the command line:
154
159
 
155
- ```console notest
160
+ ```console
156
161
  $ myapp.py --config partial_config.yaml --name mydb
157
162
  DBConfig(host='example.com', port=1234, name='mydb')
158
163
  ```
@@ -167,7 +172,7 @@ Configuration data is read in the following order, later read overwriting existi
167
172
 
168
173
  This allows for surgical modifications of configuration files. For example, one could overwrite the schema configuration from our existing `full_config` from the command line like so:
169
174
 
170
- ```console notest
175
+ ```console
171
176
  $ # Overwrite the schema name defined in the config file from the command line
172
177
  $ myapp.py --config config.yaml --name otherdb
173
178
  DBConfig(host='example.com', port=1234, name='otherdb')
@@ -197,7 +202,7 @@ type DBConfig = SQLiteConfig | DBServerConfig
197
202
 
198
203
  `confarg` can handle this new union type and figure out which configuration is desired based on the arguments it got:
199
204
 
200
- ```console notest
205
+ ```console
201
206
  $ # Pass DBServerConfig parameters, and you get a DBServerConfig
202
207
  $ myapp.py --host example.com --port 1234 --name mydb
203
208
  DBServerConfig(host='example.com', port=1234, name='mydb')
@@ -216,7 +221,7 @@ Even when disambiguation is possible, it may not be obvious to the human eye whi
216
221
 
217
222
  Therefore, by necessity or for the sake of clarity, you can provide the class path of the required configuration by using the `class` tag, like so
218
223
 
219
- ```console notest
224
+ ```console
220
225
  $ # Explicitly ask for a SQLiteConfig
221
226
  $ myapp.py --class myapp.SQLiteConfig --dbpath db.sqlite
222
227
  SQLiteConfig(dbpath='db.sqlite')
@@ -224,7 +229,7 @@ SQLiteConfig(dbpath='db.sqlite')
224
229
 
225
230
  One example where it is necessary to provide the `class` path is to overwrite the configuration with a new class. Without it, command line arguments are added to the configuration, resulting in an invalid input.
226
231
 
227
- ```console notest
232
+ ```console
228
233
  $ # Config file contains a DBServerConfig
229
234
  $ myapp.py --config db_server.yaml
230
235
  DBServerConfig(host='example.com', port=1234, name='mydb')
@@ -260,7 +265,7 @@ This allows configurations to be easily extensible. Contrast with unions, where
260
265
 
261
266
  The downside is that the concrete class must be tagged, as `confarg` cannot discover classes derived from a given class.
262
267
 
263
- ```console notest
268
+ ```console
264
269
  $ # Fails: derived class not specified
265
270
  $ uv run myapp.py --dbpath db.sqlite
266
271
  ...
@@ -292,7 +297,7 @@ Our DB configuration, which used to be the root configuration, is now located un
292
297
 
293
298
  For command line arguments, we follow the common convention of using dot-separated paths to address nested fields. Previous command line arguments for `DBConfig` are now prefixed by `db.`, like so:
294
299
 
295
- ```console notest
300
+ ```console
296
301
  $ myapp.py --db.class myapp.SQLiteConfig --db.dbpath db.sqlite
297
302
  Config(db=SQLiteConfig(dbpath='db.sqlite'), log_level='INFO')
298
303
  ```
@@ -310,7 +315,7 @@ db:
310
315
 
311
316
  and is used just like before:
312
317
 
313
- ```console notest
318
+ ```console
314
319
  $ myapp.py --config config.yaml
315
320
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'),
316
321
  log_level='DEBUG')
@@ -378,7 +383,7 @@ resources:
378
383
  max_heap_size_mb: ${int(resources.memory_gb * 1024 * 0.8)}
379
384
  ```
380
385
 
381
- ```console notest
386
+ ```console
382
387
  $ myapp.py --config expression_config.yaml
383
388
  Config(db=SQLiteConfig(dbpath='db.sqlite'),
384
389
  resources=Resources(cpu_count=4, memory_gb=16, max_heap_size_mb=13107),
@@ -387,7 +392,7 @@ Config(db=SQLiteConfig(dbpath='db.sqlite'),
387
392
 
388
393
  Note that variable interpolation occurs after all configuration data is read. This means here that you can override `memory_gb` from the command line, and `max_heap_size_mb` will be adjusted accordingly, even though the expression is defined in the configuration file.
389
394
 
390
- ```console notest
395
+ ```console
391
396
  $ # Max heap is recomputed according to the expression in the config file
392
397
  $ myapp.py --config expression_config.yaml --resources.memory_gb 8
393
398
  Config(db=SQLiteConfig(dbpath='db.sqlite'),
@@ -405,7 +410,7 @@ Some configuration components may even be generated automatically, in which case
405
410
 
406
411
  From the command line, the `--config` flag can be suffixed with a key path to load configurations there. For example,
407
412
 
408
- ```console notest
413
+ ```console
409
414
  # Load a config file specific to the `db` key
410
415
  $ myapp.py --config.db db_config.yaml
411
416
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
@@ -413,7 +418,7 @@ Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level=
413
418
 
414
419
  A similar pattern applies to environment variables:
415
420
 
416
- ```console notest
421
+ ```console
417
422
  $ MYAPP_CONFIG_DB=db_config.py myapp.py
418
423
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
419
424
  ```
@@ -1,3 +1,8 @@
1
+ <!-- pytest-markdown-console-file: notest -->
2
+ <p align="center">
3
+ <img src="docs/assets/banner.svg" alt="confarg" />
4
+ </p>
5
+
1
6
  # A tool to manage complex configurations
2
7
 
3
8
  > Load and resolve complex configurations from files, environment variables and command line arguments. Keep your data structures and favorite CLI library.
@@ -6,7 +11,7 @@
6
11
 
7
12
  It can handle deeply nested configurations, type unions, derived classes, expressions and variable interpolation, configuration compositions and more, and can integrate with your favorite argument parser library such as `argparse`, `click` or `typer`.
8
13
 
9
- `confarg` is not a framework. No decorator, base class or special annotation type required: none are provided. It is just a tool for the deserialization and serialization of complex configurations. Its footprint in your code is typically a few lines of code, making it easy to switch to it, or away from it.
14
+ `confarg` is not a framework. No decorator, base class or special annotation type required: none are provided. It is just a tool for the deserialization and serialization of complex configurations. Its footprint in your code is typically a few lines of code.
10
15
 
11
16
  ## Install
12
17
 
@@ -81,46 +86,46 @@ This allows you to construct a `DBConfig` object by collecting data from three p
81
86
 
82
87
  1. From a configuration file. By passing `--config <config_file>` to your app, `confarg` will load the content of the file and fill the `DBConfig` object. For example, a config file could look like so:
83
88
 
84
- ```yaml
85
- # config.yaml
86
- host: example.com
87
- port: 1234
88
- name: mydb
89
- ```
89
+ ```yaml
90
+ # config.yaml
91
+ host: example.com
92
+ port: 1234
93
+ name: mydb
94
+ ```
90
95
 
91
- You would then call your application as
96
+ You would then call your application as
92
97
 
93
- ```console notest
94
- $ myapp.py --config config.yaml
95
- DBConfig(host='example.com', port=1234, name='mydb')
96
- ```
98
+ ```console
99
+ $ myapp.py --config config.yaml
100
+ DBConfig(host='example.com', port=1234, name='mydb')
101
+ ```
97
102
 
98
- Configuration files in TOML and JSON formats are also supported.
103
+ Configuration files in TOML and JSON formats are also supported.
99
104
 
100
- > You can change the default `config` flag to something else using the `config_flag` parameter.
105
+ > You can change the default `config` flag to something else using the `config_flag` parameter.
101
106
 
102
107
  2. From environment variables. You can declare
103
108
 
104
- ```properties
105
- MYAPP_HOST=example.com
106
- MYAPP_PORT=1234
107
- MYAPP_NAME=mydb
108
- ```
109
+ ```properties
110
+ MYAPP_HOST=example.com
111
+ MYAPP_PORT=1234
112
+ MYAPP_NAME=mydb
113
+ ```
109
114
 
110
- for the same effect.
115
+ for the same effect.
111
116
 
112
- > Note that the environment variable prefix of your app should actually be passed to `confarg.load` like so:
113
- >
114
- > ```python
115
- > db_config = confarg.load(DBConfig, env_prefix="MYAPP_")
116
- > ```
117
+ > Note that the environment variable prefix of your app should actually be passed to `confarg.load` like so:
118
+ >
119
+ > ```python
120
+ > db_config = confarg.load(DBConfig, env_prefix="MYAPP_")
121
+ > ```
117
122
 
118
123
  3. From command line arguments.
119
124
 
120
- ```console notest
121
- $ my_app --host example.com --port 1234 --name mydb
122
- DBConfig(host='example.com', port=1234, name='mydb')
123
- ```
125
+ ```console
126
+ $ my_app --host example.com --port 1234 --name mydb
127
+ DBConfig(host='example.com', port=1234, name='mydb')
128
+ ```
124
129
 
125
130
  ### Progressive build-up
126
131
 
@@ -138,7 +143,7 @@ port: 1234
138
143
 
139
144
  and provide the schema name from the command line:
140
145
 
141
- ```console notest
146
+ ```console
142
147
  $ myapp.py --config partial_config.yaml --name mydb
143
148
  DBConfig(host='example.com', port=1234, name='mydb')
144
149
  ```
@@ -153,7 +158,7 @@ Configuration data is read in the following order, later read overwriting existi
153
158
 
154
159
  This allows for surgical modifications of configuration files. For example, one could overwrite the schema configuration from our existing `full_config` from the command line like so:
155
160
 
156
- ```console notest
161
+ ```console
157
162
  $ # Overwrite the schema name defined in the config file from the command line
158
163
  $ myapp.py --config config.yaml --name otherdb
159
164
  DBConfig(host='example.com', port=1234, name='otherdb')
@@ -183,7 +188,7 @@ type DBConfig = SQLiteConfig | DBServerConfig
183
188
 
184
189
  `confarg` can handle this new union type and figure out which configuration is desired based on the arguments it got:
185
190
 
186
- ```console notest
191
+ ```console
187
192
  $ # Pass DBServerConfig parameters, and you get a DBServerConfig
188
193
  $ myapp.py --host example.com --port 1234 --name mydb
189
194
  DBServerConfig(host='example.com', port=1234, name='mydb')
@@ -202,7 +207,7 @@ Even when disambiguation is possible, it may not be obvious to the human eye whi
202
207
 
203
208
  Therefore, by necessity or for the sake of clarity, you can provide the class path of the required configuration by using the `class` tag, like so
204
209
 
205
- ```console notest
210
+ ```console
206
211
  $ # Explicitly ask for a SQLiteConfig
207
212
  $ myapp.py --class myapp.SQLiteConfig --dbpath db.sqlite
208
213
  SQLiteConfig(dbpath='db.sqlite')
@@ -210,7 +215,7 @@ SQLiteConfig(dbpath='db.sqlite')
210
215
 
211
216
  One example where it is necessary to provide the `class` path is to overwrite the configuration with a new class. Without it, command line arguments are added to the configuration, resulting in an invalid input.
212
217
 
213
- ```console notest
218
+ ```console
214
219
  $ # Config file contains a DBServerConfig
215
220
  $ myapp.py --config db_server.yaml
216
221
  DBServerConfig(host='example.com', port=1234, name='mydb')
@@ -246,7 +251,7 @@ This allows configurations to be easily extensible. Contrast with unions, where
246
251
 
247
252
  The downside is that the concrete class must be tagged, as `confarg` cannot discover classes derived from a given class.
248
253
 
249
- ```console notest
254
+ ```console
250
255
  $ # Fails: derived class not specified
251
256
  $ uv run myapp.py --dbpath db.sqlite
252
257
  ...
@@ -278,7 +283,7 @@ Our DB configuration, which used to be the root configuration, is now located un
278
283
 
279
284
  For command line arguments, we follow the common convention of using dot-separated paths to address nested fields. Previous command line arguments for `DBConfig` are now prefixed by `db.`, like so:
280
285
 
281
- ```console notest
286
+ ```console
282
287
  $ myapp.py --db.class myapp.SQLiteConfig --db.dbpath db.sqlite
283
288
  Config(db=SQLiteConfig(dbpath='db.sqlite'), log_level='INFO')
284
289
  ```
@@ -296,7 +301,7 @@ db:
296
301
 
297
302
  and is used just like before:
298
303
 
299
- ```console notest
304
+ ```console
300
305
  $ myapp.py --config config.yaml
301
306
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'),
302
307
  log_level='DEBUG')
@@ -364,7 +369,7 @@ resources:
364
369
  max_heap_size_mb: ${int(resources.memory_gb * 1024 * 0.8)}
365
370
  ```
366
371
 
367
- ```console notest
372
+ ```console
368
373
  $ myapp.py --config expression_config.yaml
369
374
  Config(db=SQLiteConfig(dbpath='db.sqlite'),
370
375
  resources=Resources(cpu_count=4, memory_gb=16, max_heap_size_mb=13107),
@@ -373,7 +378,7 @@ Config(db=SQLiteConfig(dbpath='db.sqlite'),
373
378
 
374
379
  Note that variable interpolation occurs after all configuration data is read. This means here that you can override `memory_gb` from the command line, and `max_heap_size_mb` will be adjusted accordingly, even though the expression is defined in the configuration file.
375
380
 
376
- ```console notest
381
+ ```console
377
382
  $ # Max heap is recomputed according to the expression in the config file
378
383
  $ myapp.py --config expression_config.yaml --resources.memory_gb 8
379
384
  Config(db=SQLiteConfig(dbpath='db.sqlite'),
@@ -391,7 +396,7 @@ Some configuration components may even be generated automatically, in which case
391
396
 
392
397
  From the command line, the `--config` flag can be suffixed with a key path to load configurations there. For example,
393
398
 
394
- ```console notest
399
+ ```console
395
400
  # Load a config file specific to the `db` key
396
401
  $ myapp.py --config.db db_config.yaml
397
402
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
@@ -399,7 +404,7 @@ Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level=
399
404
 
400
405
  A similar pattern applies to environment variables:
401
406
 
402
- ```console notest
407
+ ```console
403
408
  $ MYAPP_CONFIG_DB=db_config.py myapp.py
404
409
  Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
405
410
  ```
@@ -7,7 +7,7 @@ license = "MPL-2.0"
7
7
  name = "confarg"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
- version = "0.0.1.dev5"
10
+ version = "0.0.1.dev6"
11
11
 
12
12
  [project.urls]
13
13
  Documentation = "https://confarg.github.io/confarg"
@@ -23,11 +23,12 @@ dev = [
23
23
  "hypothesis>=6.100",
24
24
  "ipython>=9.12.0",
25
25
  "jsonargparse>=4.48.0",
26
+ "just>=0.8.165",
26
27
  "mkdocstrings-python>=2.0.3",
27
28
  "mkdocstrings>=1.0.4",
28
29
  "pre-commit>=4.5.1",
29
30
  "pytest-cov>=7.0.0",
30
- "pytest-markdown-docs>=0.9.2",
31
+ "pytest-markdown-console>=0.0.1.dev2",
31
32
  "pytest>=8.0",
32
33
  "pyyaml>=6.0",
33
34
  "tomli-w>=1.0",
@@ -10,9 +10,34 @@
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Callable
17
+
13
18
  from confarg import exceptions
14
19
  from confarg._api import build, dump, dump_file, from_dict, load, merge, resolve
15
20
  from confarg._types import TagPolicy
21
+ from confarg.typedload._coerce import _LEAF_COERCIONS
22
+
23
+
24
+ def register_leaf_type(tp: type, coerce: Callable[[Any], Any]) -> None:
25
+ """Register a custom leaf type.
26
+
27
+ After registration, ``tp`` is treated as a leaf: values of that type can
28
+ appear as scalars in config files, env vars, and CLI args. ``coerce`` is
29
+ called with the raw input value — either a ``_StrToken`` (a ``str``
30
+ subclass) from CLI/env sources, or the natively-parsed Python object from
31
+ config files — and must return an instance of ``tp``, raising
32
+ ``ValueError`` or ``TypeError`` on failure.
33
+
34
+ Example::
35
+
36
+ from uuid import UUID
37
+ confarg.register_leaf_type(UUID, UUID)
38
+ """
39
+ _LEAF_COERCIONS[tp] = coerce
40
+
16
41
 
17
42
  __all__ = [ # noqa: RUF022
18
43
  # Two-step API
@@ -28,6 +53,8 @@ __all__ = [ # noqa: RUF022
28
53
  "dump_file",
29
54
  # Types
30
55
  "TagPolicy",
56
+ # Leaf-type extension
57
+ "register_leaf_type",
31
58
  # Exceptions / warnings
32
59
  "exceptions",
33
60
  ]
@@ -60,7 +60,7 @@ def _load_cli_config(fpath: Path, subpath: str, config_flag: str) -> dict[str, A
60
60
  def merge( # noqa: PLR0913
61
61
  target: type | TypeAliasType | UnionType,
62
62
  *,
63
- args: Sequence[str] | None = None,
63
+ argv: Sequence[str] | None = None,
64
64
  env: Mapping[str, str] | None = None,
65
65
  env_prefix: str | None = _defaults.ENV_PREFIX,
66
66
  env_separator: str = "__",
@@ -80,7 +80,7 @@ def merge( # noqa: PLR0913
80
80
 
81
81
  Args:
82
82
  target: The dataclass type (or scalar type) used to guide CLI parsing.
83
- args: CLI arguments to parse. Defaults to sys.argv[1:].
83
+ argv: CLI arguments to parse. Defaults to sys.argv[1:].
84
84
  env: Environment variable mapping to scan. Defaults to os.environ.
85
85
  env_prefix: Prefix that env vars must start with. Defaults to ``None``,
86
86
  which disables environment variable parsing entirely. Set to ``""``
@@ -120,13 +120,13 @@ def merge( # noqa: PLR0913
120
120
  InvalidConfigFileError: If a config file cannot be loaded.
121
121
  UnknownArgumentError: If an unrecognized CLI argument is encountered.
122
122
  """
123
- if args is None:
124
- args = sys.argv[1:]
123
+ if argv is None:
124
+ argv = sys.argv[1:]
125
125
  if env is None:
126
126
  env = os.environ
127
127
 
128
128
  # 1. Parse CLI
129
- cli_data, cli_configs = _parse_cli(args, target, cli_prefix, config_flag, union_tag)
129
+ cli_data, cli_configs = _parse_cli(argv, target, cli_prefix, config_flag, union_tag)
130
130
 
131
131
  # 2. Parse env vars (done here so env-specified config files are loaded in order)
132
132
  if env_prefix is None:
@@ -263,7 +263,7 @@ def from_dict[T](
263
263
  def load[T](
264
264
  target: type[T],
265
265
  *,
266
- args: Sequence[str] | None = ...,
266
+ argv: Sequence[str] | None = ...,
267
267
  env: Mapping[str, str] | None = ...,
268
268
  env_prefix: str | None = ...,
269
269
  env_separator: str = ...,
@@ -277,7 +277,7 @@ def load[T](
277
277
  def load(
278
278
  target: object,
279
279
  *,
280
- args: Sequence[str] | None = ...,
280
+ argv: Sequence[str] | None = ...,
281
281
  env: Mapping[str, str] | None = ...,
282
282
  env_prefix: str | None = ...,
283
283
  env_separator: str = ...,
@@ -290,7 +290,7 @@ def load(
290
290
  def load[T]( # noqa: PLR0913
291
291
  target: type[T] | TypeAliasType | UnionType,
292
292
  *,
293
- args: Sequence[str] | None = None,
293
+ argv: Sequence[str] | None = None,
294
294
  env: Mapping[str, str] | None = None,
295
295
  env_prefix: str | None = _defaults.ENV_PREFIX,
296
296
  env_separator: str = "__",
@@ -308,7 +308,7 @@ def load[T]( # noqa: PLR0913
308
308
 
309
309
  Args:
310
310
  target: The dataclass type (or scalar type) to load configuration into.
311
- args: CLI arguments to parse. Defaults to sys.argv[1:].
311
+ argv: CLI arguments to parse. Defaults to sys.argv[1:].
312
312
  env: Environment variable mapping to scan. Defaults to os.environ.
313
313
  env_prefix: Prefix that env vars must start with. Defaults to ``None``,
314
314
  which disables environment variable parsing entirely. Set to ``""``
@@ -344,7 +344,7 @@ def load[T]( # noqa: PLR0913
344
344
  """
345
345
  data = merge(
346
346
  target,
347
- args=args,
347
+ argv=argv,
348
348
  env=env,
349
349
  env_prefix=env_prefix,
350
350
  env_separator=env_separator,