psyplus 1.0.1__tar.gz → 1.2.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.
Files changed (35) hide show
  1. {psyplus-1.0.1 → psyplus-1.2.0}/PKG-INFO +164 -32
  2. {psyplus-1.0.1 → psyplus-1.2.0}/README.md +157 -26
  3. psyplus-1.2.0/psyplus/field_info.py +532 -0
  4. psyplus-1.2.0/psyplus/main.py +244 -0
  5. psyplus-1.2.0/psyplus/markdown_generator.py +242 -0
  6. psyplus-1.2.0/psyplus/sources.py +641 -0
  7. psyplus-1.2.0/psyplus/utils.py +332 -0
  8. psyplus-1.2.0/psyplus/yaml_generator.py +380 -0
  9. {psyplus-1.0.1 → psyplus-1.2.0}/pyproject.toml +7 -6
  10. psyplus-1.2.0/tests/models/env_cases.py +247 -0
  11. {psyplus-1.0.1 → psyplus-1.2.0}/tests/models/nested.py +1 -1
  12. {psyplus-1.0.1 → psyplus-1.2.0}/tests/models/readme_example.py +1 -1
  13. {psyplus-1.0.1 → psyplus-1.2.0}/tests/models/simple.py +3 -3
  14. psyplus-1.2.0/tests/test_aliases.py +550 -0
  15. psyplus-1.2.0/tests/test_env_roundtrip.py +325 -0
  16. {psyplus-1.0.1 → psyplus-1.2.0}/tests/test_field_info.py +160 -4
  17. psyplus-1.2.0/tests/test_list_index_env.py +755 -0
  18. psyplus-1.2.0/tests/test_main.py +509 -0
  19. psyplus-1.2.0/tests/test_markdown.py +383 -0
  20. psyplus-1.2.0/tests/test_required_fields.py +269 -0
  21. {psyplus-1.0.1 → psyplus-1.2.0}/tests/test_utils.py +157 -3
  22. {psyplus-1.0.1 → psyplus-1.2.0}/tests/test_yaml_generation.py +195 -89
  23. psyplus-1.0.1/psyplus/field_info.py +0 -202
  24. psyplus-1.0.1/psyplus/main.py +0 -143
  25. psyplus-1.0.1/psyplus/markdown_generator.py +0 -195
  26. psyplus-1.0.1/psyplus/utils.py +0 -174
  27. psyplus-1.0.1/psyplus/yaml_generator.py +0 -231
  28. psyplus-1.0.1/tests/test_main.py +0 -229
  29. psyplus-1.0.1/tests/test_markdown.py +0 -166
  30. {psyplus-1.0.1 → psyplus-1.2.0}/LICENSE +0 -0
  31. {psyplus-1.0.1 → psyplus-1.2.0}/psyplus/__init__.py +0 -0
  32. {psyplus-1.0.1 → psyplus-1.2.0}/tests/__init__.py +0 -0
  33. {psyplus-1.0.1 → psyplus-1.2.0}/tests/models/__init__.py +0 -0
  34. {psyplus-1.0.1 → psyplus-1.2.0}/tests/models/nullable.py +0 -0
  35. {psyplus-1.0.1 → psyplus-1.2.0}/tests/test_nullable.py +0 -0
@@ -1,21 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: psyplus
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Generate, read and document pydantic-settings config files in YAML
5
5
  Keywords: DZD,pydantic,pydantic-settings,yaml,config
6
6
  Author-Email: Tim Bleimehl <bleimehl@helmholtz-munich.de>
7
7
  License: MIT
8
8
  Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
9
10
  Classifier: Programming Language :: Python :: 3.12
10
11
  Classifier: Programming Language :: Python :: 3.13
11
12
  Classifier: Programming Language :: Python :: 3.14
12
13
  Project-URL: Source, https://github.com/DZD-eV-Diabetes-Research/pydantic-settings-yaml-plus
13
14
  Project-URL: Issues, https://github.com/DZD-eV-Diabetes-Research/pydantic-settings-yaml-plus/issues
14
- Requires-Python: >=3.12
15
- Requires-Dist: pydantic>=2.0
16
- Requires-Dist: pydantic-settings>=2.3
17
- Requires-Dist: ruamel.yaml>=0.17
18
- Requires-Dist: pyyaml>=6.0
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: pydantic>=2.10
17
+ Requires-Dist: pydantic-settings>=2.13.1
18
+ Requires-Dist: ruamel.yaml>=0.17.5
19
+ Requires-Dist: pyyaml>=6.0.1
19
20
  Provides-Extra: test
20
21
  Requires-Dist: pytest>=8.0; extra == "test"
21
22
  Requires-Dist: pytest-cov>=5.0; extra == "test"
@@ -23,14 +24,14 @@ Description-Content-Type: text/markdown
23
24
 
24
25
  # pydantic-settings-yaml-plus
25
26
 
26
- A helper library that builds upon [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) to **generate**, **read**, and **document** YAML config files — with comment headers that explain every setting's type, default value, allowed values, environment variable override, and description.
27
+ A helper library that builds upon [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) to **generate**, **read**, and **document** YAML config files, with comment headers that explain every setting's type, default value, allowed values, environment variable override, and description.
27
28
 
28
29
  - [pydantic-settings-yaml-plus](#pydantic-settings-yaml-plus)
29
30
  - [Features](#features)
30
31
  - [Installation](#installation)
31
32
  - [Quick start](#quick-start)
32
- - [1 — Define your settings model](#1--define-your-settings-model)
33
- - [2 — Generate the YAML config file](#2--generate-the-yaml-config-file)
33
+ - [1. Define your settings model](#1-define-your-settings-model)
34
+ - [2. Generate the YAML config file](#2-generate-the-yaml-config-file)
34
35
  - [Generated YAML output](#generated-yaml-output)
35
36
  - [Reading the config](#reading-the-config)
36
37
  - [Generating Markdown documentation](#generating-markdown-documentation)
@@ -44,6 +45,7 @@ A helper library that builds upon [pydantic-settings](https://docs.pydantic.dev/
44
45
  - [Useful `Field()` kwargs](#useful-field-kwargs)
45
46
  - [Configuring env vars](#configuring-env-vars)
46
47
  - [Environment variable overrides](#environment-variable-overrides)
48
+ - [Single list elements](#single-list-elements)
47
49
  - [Development](#development)
48
50
  - [Smoke test](#smoke-test)
49
51
  - [Running tests with coverage](#running-tests-with-coverage)
@@ -53,11 +55,12 @@ A helper library that builds upon [pydantic-settings](https://docs.pydantic.dev/
53
55
 
54
56
  ## Features
55
57
 
56
- - **Generate documented YAML** — each field gets a comment block with its type, required flag, default value, allowed enum values, corresponding environment variable, description, and examples.
57
- - **Nested models** — `BaseModel` sub-models are rendered as indented YAML sections with full comment headers.
58
- - **Read back with env-var override** — `load()` loads the YAML file and still applies environment variable overrides using pydantic-settings' standard machinery.
59
- - **Markdown docs** — generate a Markdown reference document straight from your settings class.
60
- - **Zero boilerplate** — define your settings in a standard `pydantic_settings.BaseSettings` subclass; psyplus does the rest.
58
+ - **Generate documented YAML:** each field gets a comment block with its type, required flag, default value, allowed enum values, corresponding environment variable, description, and examples.
59
+ - **Nested models:** `BaseModel` sub-models are rendered as indented YAML sections with full comment headers.
60
+ - **Read back with env-var override:** `load()` loads the YAML file and still applies environment variable overrides using pydantic-settings' standard machinery.
61
+ - **Single list elements from env vars:** `load()` also reads env vars like `APP_SERVERS__0__HOST` that change one element of a list from the YAML file.
62
+ - **Markdown docs:** generate a Markdown reference document straight from your settings class.
63
+ - **Zero boilerplate:** define your settings in a standard `pydantic_settings.BaseSettings` subclass; psyplus does the rest.
61
64
 
62
65
  ---
63
66
 
@@ -67,13 +70,13 @@ A helper library that builds upon [pydantic-settings](https://docs.pydantic.dev/
67
70
  pip install psyplus
68
71
  ```
69
72
 
70
- **Requirements:** Python ≥ 3.12, pydantic ≥ 2.0, pydantic-settings ≥ 2.3
73
+ **Requirements:** Python ≥ 3.11, pydantic ≥ 2.10, pydantic-settings ≥ 2.13.1, ruamel.yaml ≥ 0.17.5, PyYAML ≥ 6.0.1
71
74
 
72
75
  ---
73
76
 
74
77
  ## Quick start
75
78
 
76
- ### 1 — Define your settings model
79
+ ### 1. Define your settings model
77
80
 
78
81
  ```python
79
82
  # config.py
@@ -128,7 +131,7 @@ class MyAppConfig(BaseSettings):
128
131
 
129
132
  > **Note on `model_config`:** use `SettingsConfigDict` (pydantic-settings v2 style) rather than the old inner `class Config`. Both work, but `SettingsConfigDict` is preferred.
130
133
 
131
- ### 2 — Generate the YAML config file
134
+ ### 2. Generate the YAML config file
132
135
 
133
136
  ```python
134
137
  from psyplus import YamlSettingsPlus
@@ -179,7 +182,7 @@ storage_dir: /home/user/.config/myapp
179
182
  # Required: True
180
183
  # Env-var: 'APP_ADMIN_PW'
181
184
  # Description: The init password for the admin account
182
- admin_pw: ''
185
+ admin_pw:
183
186
 
184
187
  # ## database_server ###
185
188
  # Type: Object (DatabaseServerSettings)
@@ -218,22 +221,28 @@ database_server:
218
221
  # Type: List of str
219
222
  # Required: True
220
223
  # Env-var: 'APP_DATABASE_SERVER__DATABASE_NAMES'
224
+ # 'APP_DATABASE_SERVER__DATABASE_NAMES__<list-index>' (one element, 0 is the first; psyplus load() only)
221
225
  # Description: The names of the databases to use
222
226
  # Example:
223
227
  # >database_names:
224
228
  # >- mydb
225
229
  # >- theotherdb
226
- database_names: []
230
+ database_names:
227
231
 
228
232
  # ## init_values ###
229
233
  # Type: Dictionary of (str, str)
230
234
  # Required: True
231
235
  # Env-var: 'APP_INIT_VALUES'
232
- init_values: {}
236
+ # 'APP_INIT_VALUES__<dict-key>' sets one entry, its key arrives in lower case
237
+ init_values:
233
238
  ```
234
239
 
235
- Required fields without defaults are written with an empty placeholder (`''`, `[]`, `{}`).
236
- The `Required: True` comment tells the user they must fill these in before the app will validate.
240
+ Required fields without a default are written with an empty value (`admin_pw:`, which YAML reads as null),
241
+ whatever their type. A required sub-model without a default shows its fields, so the structure is
242
+ visible, with its own required fields empty. The `Required: True` comment marks every value to fill in.
243
+ Until that is done, `load()` raises a pydantic `ValidationError` that names each empty required field,
244
+ so an app does not start with an unfilled config. (A required `Optional[...]` field is the exception:
245
+ null is a valid value for it.)
237
246
 
238
247
  ---
239
248
 
@@ -250,6 +259,8 @@ print(config.database_server.host) # → "localhost" (or whatever is in the YA
250
259
  print(config.log_level) # → "INFO"
251
260
  ```
252
261
 
262
+ `load()` returns an instance of your model class itself (`type(config) is MyAppConfig`), validated once, exactly as if you had called `MyAppConfig()`. It compares equal to and pickles like any other `MyAppConfig` instance.
263
+
253
264
  `load()` honours environment variable overrides through pydantic-settings' standard source priority: **env vars beat YAML values**. For example:
254
265
 
255
266
  ```bash
@@ -258,10 +269,10 @@ export APP_LOG_LEVEL=DEBUG
258
269
 
259
270
  …will make `config.log_level == "DEBUG"` even if the YAML file says `INFO`.
260
271
 
261
- > **Tip:** if you prefer to keep psyplus out of your runtime path, configure `yaml_file` directly in your model and call it without psyplus:
272
+ > **Tip:** if you prefer to keep psyplus out of your runtime path, configure `yaml_file` directly in your model and add a YAML source (setting `yaml_file` alone is not enough, pydantic-settings does not read it by default):
262
273
  >
263
274
  > ```python
264
- > from pydantic_settings import BaseSettings, SettingsConfigDict
275
+ > from pydantic_settings import BaseSettings, SettingsConfigDict, YamlConfigSettingsSource
265
276
  >
266
277
  > class MyAppConfig(BaseSettings):
267
278
  > model_config = SettingsConfigDict(
@@ -270,8 +281,19 @@ export APP_LOG_LEVEL=DEBUG
270
281
  > env_nested_delimiter="__",
271
282
  > )
272
283
  >
273
- > config = MyAppConfig() # reads YAML + env vars automatically
284
+ > @classmethod
285
+ > def settings_customise_sources(
286
+ > cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings
287
+ > ):
288
+ > return (
289
+ > init_settings, env_settings, dotenv_settings, file_secret_settings,
290
+ > YamlConfigSettingsSource(settings_cls),
291
+ > )
292
+ >
293
+ > config = MyAppConfig() # reads YAML + env vars
274
294
  > ```
295
+ >
296
+ > Such a model still works with `load()`: psyplus then reads its own `file_path` in place of `yaml_file` (see [Environment variable overrides](#environment-variable-overrides)).
275
297
 
276
298
  ---
277
299
 
@@ -294,6 +316,11 @@ md = YamlSettingsPlus(MyAppConfig).render_markdown()
294
316
 
295
317
  Both produce a document with a property table for every field: type, required flag, default, allowed values, environment variable, description, and examples.
296
318
 
319
+ The fields of a nested model get their own sections, headed by their full path (`database.host`,
320
+ `servers[*].port`). A field that holds one of several models (`Union[Cat, Dog]`) gets one section
321
+ per model, labelled `option 1 of 2` and so on. Markdown has no headings below level 6, so deeper
322
+ fields stay at level 6 and the path in the heading shows where they belong.
323
+
297
324
  ---
298
325
 
299
326
  ## Example gallery
@@ -304,7 +331,7 @@ The table below links the bundled test models to their pre-generated output so y
304
331
  |---|---|---|---|---|
305
332
  | `SimpleModel` | [tests/models/simple.py](tests/models/simple.py) | Flat settings: scalars, Optional, Literal, List, Dict, Annotated metadata, `default_factory` | [SimpleModel.yaml](smoke_output/SimpleModel.yaml) | [SimpleModel.md](smoke_output/SimpleModel.md) |
306
333
  | `NestedModel` | [tests/models/nested.py](tests/models/nested.py) | Nested `BaseModel` sub-field, optional sub-model, env-var path generation with `env_nested_delimiter` | [NestedModel.yaml](smoke_output/NestedModel.yaml) | [NestedModel.md](smoke_output/NestedModel.md) |
307
- | `MultiNestedModel` | [tests/models/nested.py](tests/models/nested.py) | `List[BaseModel]` and `Dict[str, BaseModel]` fields — YAML renders each item, Markdown recurses into the item schema | [MultiNestedModel.yaml](smoke_output/MultiNestedModel.yaml) | [MultiNestedModel.md](smoke_output/MultiNestedModel.md) |
334
+ | `MultiNestedModel` | [tests/models/nested.py](tests/models/nested.py) | `List[BaseModel]` and `Dict[str, BaseModel]` fields; YAML renders each item, Markdown recurses into the item schema | [MultiNestedModel.yaml](smoke_output/MultiNestedModel.yaml) | [MultiNestedModel.md](smoke_output/MultiNestedModel.md) |
308
335
  | `MyAppConfig` | [tests/models/readme_example.py](tests/models/readme_example.py) | The full README quick-start model with nested database settings and field examples | [MyAppConfig.yaml](smoke_output/MyAppConfig.yaml) | [MyAppConfig.md](smoke_output/MyAppConfig.md) |
309
336
 
310
337
  These files are regenerated automatically by the smoke script (`pdm run smoke`) and committed to the repository, so they are always in sync with the current code.
@@ -328,7 +355,7 @@ These files are regenerated automatically by the smoke script (`pdm run smoke`)
328
355
  | `generate_markdown_doc(output_path)` | Write a Markdown reference document to `output_path`. |
329
356
  | `render_yaml() → str` | Render the documented YAML as a string without touching the filesystem. |
330
357
  | `render_markdown() → str` | Render the Markdown reference document as a string without touching the filesystem. |
331
- | `load() → BaseSettings` | Load `file_path`, apply env var overrides, return a validated settings instance. |
358
+ | `load() → model` | Load `file_path`, apply env var overrides (including [single list elements](#single-list-elements)), return a validated instance of `model`. |
332
359
 
333
360
  ### `YamlFileGenerator(settings_class, indent_size=2)`
334
361
 
@@ -368,7 +395,8 @@ psyplus works with any standard `pydantic_settings.BaseSettings` subclass. The r
368
395
  | `title="…"` | Appended to the `## field_name - Title ###` header |
369
396
  | `examples=[…]` | `# Example:` block with rendered YAML |
370
397
  | `default=…` | `# Default:` comment row |
371
- | `default_factory=lambda: …` | `# Default:` not shown (factory evaluated at generation time) |
398
+ | `default_factory=lambda: …` | `# Default:` not shown (factory evaluated at generation time, its result is the value in the YAML file) |
399
+ | `default_factory=lambda data: …` | Same, the factory gets the values of the fields defined before it. If it needs a required value without a default, the field is written empty with a note to remove the key |
372
400
  | `max_length=…` / `gt=…` / etc. | `# Constraints:` comment row |
373
401
 
374
402
  ### Configuring env vars
@@ -394,6 +422,26 @@ For nested models the env var path is built as:
394
422
  # e.g. MYAPP_DATABASE__HOST (prefix="MYAPP_", delimiter="__")
395
423
  ```
396
424
 
425
+ What pydantic-settings can set through env vars, and what the generated files show:
426
+
427
+ | Field | Env var example | Behaviour |
428
+ |---|---|---|
429
+ | Field of a nested model, any depth | `MYAPP_DATABASE__SERVER__HOST=db` | Sets only this value. The other values from the YAML file stay. |
430
+ | Whole nested model | `MYAPP_DATABASE='{"name": "x"}'` | JSON, merged into the values from the YAML file. |
431
+ | List (`List[str]`, `List[SubModel]`, tuple, set) | `MYAPP_HOSTS='["a", "b"]'` | JSON, replaces the whole list. |
432
+ | One list element, or a field of it | `MYAPP_HOSTS__1=b`, `MYAPP_SERVERS__0__PORT=8080` | Only with `load()`, see [Single list elements](#single-list-elements). `<list-index>` stands for the position, 0 is the first. Changes this element of the list from the YAML file, the other elements stay. |
433
+ | Dict value (`Dict[str, ...]`) | `MYAPP_LABELS__team=core` | `<dict-key>` stands for the key. Env var names are case-insensitive by default, so the key arrives in lower case, and the generated files say so next to the env var of the dict field. Keys in a JSON value keep their case. |
434
+
435
+ Only the root model's `env_prefix` and `env_nested_delimiter` count. A nested `BaseSettings`
436
+ sub-model's own `env_prefix` is not used for its fields. Without `env_nested_delimiter`, nested
437
+ fields have no env var of their own and can only be set through the JSON value of the top-level
438
+ field. With `case_sensitive=True`, the env var names keep the case of the field names. With
439
+ `env_nested_max_split`, only fields that many levels below the top-level field have an env var
440
+ (a list index counts as a level).
441
+ A sub-model that defaults to null has no fields in the YAML file, so its comment lists the env
442
+ vars of its fields under `Env-vars of the sub-fields:`. For a field that holds one of several
443
+ models (`Union[Cat, Dog]`), this list covers the fields of every model.
444
+
397
445
  #### Nullable fields
398
446
 
399
447
  A nullable field (`Optional[int]`, `int | None`) shows its type as `int or null`. By default,
@@ -416,18 +464,102 @@ For a nullable field whose default is not null, the `Env-var:` hint says which c
416
464
  without it. A field that defaults to null gets no hint, since leaving the env var unset already
417
465
  gives null.
418
466
 
467
+ ### Fields with an alias
468
+
469
+ pydantic-settings reads the YAML file and the env vars by a field's validation alias, so psyplus
470
+ uses it as the YAML key, in the YAML path and in the env var name. That covers `alias=`,
471
+ `validation_alias=` and an `alias_generator` such as `to_camel`, also with `populate_by_name=True`.
472
+ A `serialization_alias` changes nothing.
473
+
474
+ ```python
475
+ class Database(BaseModel):
476
+ host_name: str = Field("localhost", alias="hostName")
477
+
478
+ class MyConfig(BaseSettings):
479
+ model_config = SettingsConfigDict(env_prefix="MYAPP_", env_nested_delimiter="__")
480
+
481
+ log_level: str = Field("INFO", alias="logLevel")
482
+ # → YAML key: logLevel, Env-var: 'LOGLEVEL'
483
+ database: Database = Database()
484
+ # → YAML path: database.hostName, Env-var: 'MYAPP_DATABASE__HOSTNAME'
485
+ ```
486
+
487
+ | Field | YAML key | Env var |
488
+ |---|---|---|
489
+ | `alias="logLevel"` or `validation_alias="logLevel"` | `logLevel` | `LOGLEVEL` |
490
+ | `validation_alias=AliasChoices("a", "b")` | `a`, the first string choice | `A` |
491
+ | `validation_alias=AliasPath("database", "host")` | `host` inside a `database` map | none |
492
+ | `serialization_alias="x"` only | the field name | as without an alias |
493
+
494
+ * **Prefix:** pydantic-settings puts `env_prefix` in front of an alias only when the model sets
495
+ `env_prefix_target="alias"` or `"all"`. With the default `"variable"` only field names get it.
496
+ What counts is the top-level field: `MYAPP_DATABASE__HOSTNAME` above gets the prefix because
497
+ `database` has no alias.
498
+ * **Case:** without `case_sensitive=True` env var names arrive in lower case, and
499
+ pydantic-settings matches them back to a camelCase key only along plain sub-model fields. Below a
500
+ dict, a Union of models, or an `Optional` sub-model deeper down, a key with upper case letters can
501
+ not be set through its own env var, so none is shown there. The JSON value of the field above
502
+ still works.
503
+ * **`populate_by_name`:** below a top-level field with an alias, the fields and dict entries have
504
+ no env var of their own. pydantic-settings files them under the field name, which clashes with the alias key
505
+ from the YAML file. The JSON value of the top-level field still works.
506
+ * **`AliasPath`:** a path of names is written as nested maps, which several fields may share. A
507
+ path with a list index (`AliasPath("servers", 0)`) or one that clashes with another key is not
508
+ written; its comment block says where to add it. No env var sets an `AliasPath` field:
509
+ pydantic-settings would read each element of the path as an env var of its own.
510
+ * **Alias and validation alias differ:** pydantic-settings reads a top-level field from the YAML
511
+ file only under its `alias`. When validation expects another name (`alias="a",
512
+ validation_alias="b"`, an `alias_generator` next to an explicit `validation_alias`, or
513
+ `validate_by_alias=False`), no key works, so the field is left out of the YAML file. A note in
514
+ the YAML file and in the Markdown doc says so. Set it through its env var instead.
515
+
419
516
  ---
420
517
 
421
518
  ## Environment variable overrides
422
519
 
423
- `load()` creates a thin dynamic subclass that adds the YAML file as a pydantic-settings source while keeping the normal env var source with higher priority. The effective source priority (highest → lowest) is:
520
+ `load()` uses the same sources pydantic-settings uses for your model and adds the YAML file as the lowest priority source, just above the field defaults. For a model without its own `settings_customise_sources` the effective source priority (highest → lowest) is:
424
521
 
425
522
  1. **Environment variables** (respects `env_prefix` / `env_nested_delimiter`)
426
- 2. **YAML file** values
427
- 3. **Model defaults**
523
+ 2. **`.env` file** (if the model sets `env_file`)
524
+ 3. **Secrets directory** (if the model sets `secrets_dir`)
525
+ 4. **YAML file** values
526
+ 5. **Model defaults**
428
527
 
429
528
  No changes to your settings class are required.
430
529
 
530
+ If your model defines `settings_customise_sources`, `load()` respects it: the YAML file is added after the sources it returns, and a source it leaves out stays out. A model that drops `env_settings`, for example, ignores env vars in `load()` too. If it already returns a `YamlConfigSettingsSource`, `load()` adds no second one but reads `file_path` through it, at the position your model chose.
531
+
532
+ Command line parsing configured in `model_config` (`cli_parse_args`, `cli_settings_source`) also applies to `load()`, with the command line beating all other sources.
533
+
534
+ A missing YAML file is not an error: `load()` then returns the model defaults plus whatever the env vars, the `.env` file and the secrets directory set.
535
+
536
+ ### Single list elements
537
+
538
+ pydantic-settings itself can only replace a whole list through an env var (`APP_SERVERS='[{"host": "a"}]'`). `load()` also reads env vars that address one element by its position, in the environment and in the `.env` file:
539
+
540
+ ```bash
541
+ export APP_SERVERS__0__HOST=db.internal # field of the first element
542
+ export APP_TAGS__1=beta # second element of a List[str]
543
+ export APP_GROUPS__admins__0=ann # list inside a dict
544
+ export APP_MATRIX__1__0=7 # list inside a list
545
+ ```
546
+
547
+ The generated YAML comments and Markdown docs show these names with `<list-index>` in place of the position (`APP_SERVERS__<list-index>__HOST`). They work with `load()` only: a model instantiated directly (`MyAppConfig()`) reads them the way pydantic-settings does, which fails list validation.
548
+
549
+ The env var changes the list that the lower priority sources provide (usually the YAML file) instead of replacing it:
550
+
551
+ - **Index below the list length:** changes that element. The fields of an element model are merged into it (`APP_SERVERS__0__HOST` keeps the element's `port`), the same goes for a JSON object (`APP_SERVERS__0='{"host": "x"}'`). Any other value replaces the element.
552
+ - **Index equal to the list length:** adds an element. Several new elements need consecutive indices (2, 3, 4 for a list of two). A new element that is a model needs its required fields set too.
553
+ - **Index further beyond the end:** `load()` fails with an error that names the env var.
554
+ - **List not in the YAML file:** the env var changes the field's default list. A field without a default (or with `null`) starts as an empty list, so its indices start at 0. A default from a `default_factory` that takes the other values (`lambda data: ...`) can not be changed this way: `load()` fails with an error, put the list in the YAML file or set it as a whole.
555
+ - **Aliases:** the list is found in the YAML file under the field name or any of its aliases, and the changed list stays under that key.
556
+ - **Whole list and elements together:** an index env var changes the whole list value from the same place (`APP_TAGS='["a", "b"]'` plus `APP_TAGS__1=c` gives `["a", "c"]`). A whole value that is not a JSON list there (a comma separated string for a `NoDecode` field, for example) can not be changed this way, `load()` fails with an error. A whole list from a higher priority source replaces everything below it, as usual.
557
+ - **Not a list index:** `-1` or `01` where a list is expected is an error that names the env var. Other names (`APP_TAGS__NAME`) are left to pydantic-settings, as without psyplus.
558
+ - **A list or a dict (`Union[List[str], Dict[str, str]]`):** a number is a list index only where the value it changes is a list. Where it is a dict, the number is a dict key, as pydantic-settings reads it.
559
+ - **Tuples and sets** work like lists. For a set, the index counts the elements in the order the YAML file lists them.
560
+
561
+ Only the env and `.env` sources that `load()` hands to your model's `settings_customise_sources` (`env_settings`, `dotenv_settings`) read list indices. A source your model builds itself (`EnvSettingsSource(settings_cls, env_prefix=...)`) works as pydantic-settings makes it, without list index support. Under `load()`, `env_settings` and `dotenv_settings` are subclasses of `EnvSettingsSource` and `DotEnvSettingsSource`: `isinstance()` checks on them work, `type(...) is EnvSettingsSource` does not.
562
+
431
563
  ---
432
564
 
433
565
  ## Development
@@ -456,7 +588,7 @@ To visually inspect the generated YAML and Markdown for all built-in test models
456
588
  pdm run smoke
457
589
  ```
458
590
 
459
- This writes four files per model (YAML config template + Markdown reference) to `smoke_output/` and prints a preview of each to the terminal. The output is committed to the repository and linked from the [Example gallery](#example-gallery) section — re-run the script and commit the results whenever the generator logic changes.
591
+ This writes two files per model (YAML config template + Markdown reference) to `smoke_output/` and prints a preview of each to the terminal. The output is committed to the repository and linked from the [Example gallery](#example-gallery) section. Re-run the script and commit the results whenever the generator logic changes.
460
592
 
461
593
  ```
462
594
  smoke_output/