Repository: makukha/pydantic-file-secrets (language: python)

Issue:
Should strip whitespace
The default implementation of `SecretsSettingsSource` strips the read file contents of any whitespace but this one here does not.

Setting `str_strip_whitespace` in the `model_config` does not seem to have any effect on this either.


Discussion / hints:
Thank you! This is definitely a bug.
Btw, I just checked that `str_strip_whitespace` is not respected by original `SecretsSettingsSource`:

```python
def test_str_strip_whitespace_not_respected(secrets_dir):
    class Settings(BaseSettings):
        key: str

        model_config = SettingsConfigDict(
            secrets_dir=secrets_dir,
            str_strip_whitespace=False,
        )

    secrets_dir.add_files(
        ('key', ' value '),
    )

    conf = Settings()
    assert conf.key == 'value'  # str_strip_whitespace not respected
```

Fix patch:
diff --git a/.bumpversion.toml b/.bumpversion.toml
index fa4d1f0..157e399 100644
--- a/.bumpversion.toml
+++ b/.bumpversion.toml
@@ -1,5 +1,5 @@
 [tool.bumpversion]
-current_version = "0.2.0"
+current_version = "0.2.1"
 allow_dirty = true
 files = [
   {filename = "src/pydantic_file_secrets/__version__.py"},
diff --git a/README.md b/README.md
index 450f4a3..d8d4623 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
 # pydantic-file-secrets 🔑
 
-> Use file secrets in nested [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) models, drop-in replacement of `SecretsSettingsSource`.
+> Use file secrets in nested [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) models, drop-in replacement for `SecretsSettingsSource`.
 
 ![GitHub License](https://img.shields.io/github/license/makukha/pydantic-file-secrets)
-[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.2.0/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)
-[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.2.0/docs/badge/coverage.svg)](https://github.com/makukha/pydantic-file-secrets)
+[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.2.1/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)
+[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.2.1/docs/badge/coverage.svg)](https://github.com/makukha/pydantic-file-secrets)
 [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v1.json)](https://github.com/astral-sh/ruff)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) \
-[![pypi](https://img.shields.io/pypi/v/pydantic-file-secrets.svg#0.2.0)](https://pypi.python.org/pypi/pydantic-file-secrets)
+[![pypi](https://img.shields.io/pypi/v/pydantic-file-secrets.svg#0.2.1)](https://pypi.python.org/pypi/pydantic-file-secrets)
 [![versions](https://img.shields.io/pypi/pyversions/pydantic-file-secrets.svg)](https://pypi.org/project/pydantic-file-secrets)
 [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev)
 
diff --git a/pyproject.toml b/pyproject.toml
index 7236aad..a546717 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,9 @@ build-backend = "pdm.backend"
 [project]
 name = "pydantic-file-secrets"
 dynamic = ["version"]
-description = "Use file secrets in nested models of Pydantic Settings."
+description = """Use file secrets in nested Pydantic Settings models, \
+    drop-in replacement for SecretsSettingsSource.
+"""
 authors = [
     {name = "Michael Makukha", email = "m.makukha@gmail.com"},
 ]
diff --git a/src/pydantic_file_secrets/__init__.py b/src/pydantic_file_secrets/__init__.py
index 66d5fa0..0417b5d 100644
--- a/src/pydantic_file_secrets/__init__.py
+++ b/src/pydantic_file_secrets/__init__.py
@@ -147,7 +147,7 @@ def validate_secrets_path(self, path: Path) -> None:
     @staticmethod
     def load_secrets(path: Path) -> dict[str, str]:
         return {
-            str(p.relative_to(path)): p.read_text()
+            str(p.relative_to(path)): p.read_text().strip()
             for p in path.glob('**/*')
             if p.is_file()
         }
diff --git a/src/pydantic_file_secrets/__version__.py b/src/pydantic_file_secrets/__version__.py
index 9f137b9..9767f90 100644
--- a/src/pydantic_file_secrets/__version__.py
+++ b/src/pydantic_file_secrets/__version__.py
@@ -1,1 +1,1 @@
-__version__ = '0.2.0'
+__version__ = '0.2.1'


Test patch:
diff --git a/docs/badge/tests.svg b/docs/badge/tests.svg
index b2ac141..282c9bb 100644
--- a/docs/badge/tests.svg
+++ b/docs/badge/tests.svg
@@ -1,5 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="62" height="20" role="img" aria-label="tests: 28">
-	<title>tests: 28</title>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="62" height="20" role="img" aria-label="tests: 31">
+	<title>tests: 31</title>
 	<linearGradient id="s" x2="0" y2="100%">
 		<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
 		<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
 	<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
 		<text aria-hidden="true" x="195.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text>
 		<text x="195.0" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text>
-		<text aria-hidden="true" x="485.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="150">28</text>
-		<text x="485.0" y="140" transform="scale(.1)" fill="#fff" textLength="150">28</text>
+		<text aria-hidden="true" x="485.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="150">31</text>
+		<text x="485.0" y="140" transform="scale(.1)" fill="#fff" textLength="150">31</text>
 	</g>
 </svg>
diff --git a/tests/test_pydantic_source.py b/tests/test_pydantic_source.py
index ea34d65..4c73ee1 100644
--- a/tests/test_pydantic_source.py
+++ b/tests/test_pydantic_source.py
@@ -35,3 +35,37 @@ class Settings(BaseSettings):
     assert conf.key_empty == ''  # should be None if working
     assert conf.key_none == 'null'  # should be Null if working
     assert isinstance(conf.key_enum, TestEnum)  # should be True if working
+
+
+def test_str_strip_whitespace_not_specified(secrets_dir):
+    class Settings(BaseSettings):
+        key: str
+
+        model_config = SettingsConfigDict(
+            secrets_dir=secrets_dir,
+            # str_strip_whitespace not specified
+        )
+
+    secrets_dir.add_files(
+        ('key', ' value '),
+    )
+
+    conf = Settings()
+    assert conf.key == 'value'  # spaces are stripped by SecretsSettingsSource
+
+
+def test_str_strip_whitespace_not_respected(secrets_dir):
+    class Settings(BaseSettings):
+        key: str
+
+        model_config = SettingsConfigDict(
+            secrets_dir=secrets_dir,
+            str_strip_whitespace=False,
+        )
+
+    secrets_dir.add_files(
+        ('key', ' value '),
+    )
+
+    conf = Settings()
+    assert conf.key == 'value'  # str_strip_whitespace not respected
diff --git a/tests/test_strip_whitespace.py b/tests/test_strip_whitespace.py
new file mode 100644
index 0000000..3114329
--- /dev/null
+++ b/tests/test_strip_whitespace.py
@@ -0,0 +1,16 @@
+def test_strip_whitespace(settings_model, monkeypatch, secrets_dir):
+    monkeypatch.setenv('DB__USER', 'user')
+    secrets_dir.add_files(
+        ('app_key', ' secret1 '),
+        ('db___password', '\tsecret2\n'),  # file name with delimiter
+    )
+    Settings = settings_model(
+        model_config=dict(
+            env_nested_delimiter='__',
+            secrets_dir=secrets_dir,
+            secrets_nested_delimiter='___',
+        ),
+    )
+    conf = Settings()
+    assert conf.app_key == 'secret1'
+    assert conf.db.password == 'secret2'  # noqa: S105