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

Issue:
Does not honor `_secrets_dir` constructor parameter
Setting the `_secrets_dir` parameter on the settings constructor does not have an effect.


Discussion / hints:

Fix patch:
diff --git a/.bumpversion.toml b/.bumpversion.toml
index 157e399..01c1f30 100644
--- a/.bumpversion.toml
+++ b/.bumpversion.toml
@@ -1,5 +1,5 @@
 [tool.bumpversion]
-current_version = "0.2.1"
+current_version = "0.3.0"
 allow_dirty = true
 files = [
   {filename = "src/pydantic_file_secrets/__version__.py"},
diff --git a/README.md b/README.md
index d8d4623..c2cd647 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
 > 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.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)
+[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.3.0/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)
+[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.3.0/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.1)](https://pypi.python.org/pypi/pydantic-file-secrets)
+[![pypi](https://img.shields.io/pypi/v/pydantic-file-secrets.svg#0.3.0)](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)
 
@@ -102,9 +102,10 @@ class Settings(BaseSettings):
         file_secret_settings,
     ):
         return (
-            env_settings,
             init_settings,
-            FileSecretsSettingsSource(settings_cls),
+            env_settings,
+            dotenv_settings,
+            FileSecretsSettingsSource(file_secret_settings),
         )
 
 ```
diff --git a/docs/badge/coverage.svg b/docs/badge/coverage.svg
index d032963..55b37da 100644
--- a/docs/badge/coverage.svg
+++ b/docs/badge/coverage.svg
@@ -1,21 +1,21 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="122" height="20" role="img" aria-label="coverage: 100.00%">
-	<title>coverage: 100.00%</title>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="coverage: 98.06%">
+	<title>coverage: 98.06%</title>
 	<linearGradient id="s" x2="0" y2="100%">
 		<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
 		<stop offset="1" stop-opacity=".1"/>
 	</linearGradient>
 	<clipPath id="r">
-		<rect width="122" height="20" rx="3" fill="#fff"/>
+		<rect width="116" height="20" rx="3" fill="#fff"/>
 	</clipPath>
 	<g clip-path="url(#r)">
 		<rect width="61" height="20" fill="#555"/>
-		<rect x="61" width="61" height="20" fill="#4c1"/>
-		<rect width="122" height="20" fill="url(#s)"/>
+		<rect x="61" width="55" height="20" fill="#4c1"/>
+		<rect width="116" height="20" fill="url(#s)"/>
 	</g>
 	<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="315.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text>
 		<text x="315.0" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text>
-		<text aria-hidden="true" x="905.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">100.00%</text>
-		<text x="905.0" y="140" transform="scale(.1)" fill="#fff" textLength="510">100.00%</text>
+		<text aria-hidden="true" x="875.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="450">98.06%</text>
+		<text x="875.0" y="140" transform="scale(.1)" fill="#fff" textLength="450">98.06%</text>
 	</g>
 </svg>
diff --git a/src/pydantic_file_secrets/__init__.py b/src/pydantic_file_secrets/__init__.py
index 0417b5d..5170e85 100644
--- a/src/pydantic_file_secrets/__init__.py
+++ b/src/pydantic_file_secrets/__init__.py
@@ -4,7 +4,7 @@
 from typing import Any, Literal
 import warnings
 
-from pydantic_settings import BaseSettings, EnvSettingsSource, SettingsError
+from pydantic_settings import BaseSettings, EnvSettingsSource, SecretsSettingsSource, SettingsError
 from pydantic_settings.sources import parse_env_vars
 from pydantic_settings.utils import path_type_label
 
@@ -20,7 +20,7 @@
 class FileSecretsSettingsSource(EnvSettingsSource):
     def __init__(
         self,
-        settings_cls: type[BaseSettings],
+        file_secret_settings: SecretsSettingsSource | BaseSettings,
         secrets_dir: str | Path | list[str | Path] | None = None,
         secrets_dir_missing: SecretsDirMissing | None = None,
         secrets_dir_max_size: int | None = None,
@@ -29,9 +29,19 @@ def __init__(
         secrets_nested_delimiter: str | None = None,
         secrets_nested_subdir: bool | None = None,
     ) -> None:
+        if isinstance(file_secret_settings, BaseSettings):
+            # We allow the first argument to be settings_cls like original
+            # SecretsSettingsSource. However, it is recommended to pass
+            # SecretsSettingsSource instance instead (as it is shown in usage examples),
+            # otherwise `_secrets_dir` arg passed to Settings() constructor
+            # will be ignored.
+            settings_cls = file_secret_settings
+        else:
+            settings_cls = file_secret_settings.settings_cls
         # config options
         conf = settings_cls.model_config
         self.secrets_dir: str | Path | list[str | Path] | None = first_not_none(
+            getattr(file_secret_settings, 'secrets_dir', None),
             secrets_dir,
             conf.get('secrets_dir'),
         )
diff --git a/src/pydantic_file_secrets/__version__.py b/src/pydantic_file_secrets/__version__.py
index 9767f90..282d73f 100644
--- a/src/pydantic_file_secrets/__version__.py
+++ b/src/pydantic_file_secrets/__version__.py
@@ -1,1 +1,1 @@
-__version__ = '0.2.1'
+__version__ = '0.3.0'


Test patch:
diff --git a/docs/badge/tests.svg b/docs/badge/tests.svg
index 282c9bb..e295e07 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: 31">
-	<title>tests: 31</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: 32">
+	<title>tests: 32</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">31</text>
-		<text x="485.0" y="140" transform="scale(.1)" fill="#fff" textLength="150">31</text>
+		<text aria-hidden="true" x="485.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="150">32</text>
+		<text x="485.0" y="140" transform="scale(.1)" fill="#fff" textLength="150">32</text>
 	</g>
 </svg>
diff --git a/tests/conftest.py b/tests/conftest.py
index 9e50357..0f3284b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -36,9 +36,9 @@ def settings_customise_sources(
                 file_secret_settings,
             ) -> tuple:
                 return (
-                    env_settings,
                     init_settings,
-                    FileSecretsSettingsSource(settings_cls),
+                    env_settings,
+                    FileSecretsSettingsSource(file_secret_settings),
                 )
 
         TestSettings.model_config = model_config or {}
diff --git a/tests/test_ignored_options.py b/tests/test_ignored_options.py
index 728228e..9d0a3dc 100644
--- a/tests/test_ignored_options.py
+++ b/tests/test_ignored_options.py
@@ -40,9 +40,9 @@ def settings_customise_sources(
                 file_secret_settings,
             ) -> tuple:
                 return (
-                    env_settings,
                     init_settings,
-                    FileSecretsSettingsSource(settings_cls),
+                    env_settings,
+                    FileSecretsSettingsSource(file_secret_settings),
                 )
 
         SettingsSSS.model_config = model_config
diff --git a/tests/test_settings_args.py b/tests/test_settings_args.py
new file mode 100644
index 0000000..afd0b48
--- /dev/null
+++ b/tests/test_settings_args.py
@@ -0,0 +1,15 @@
+def test_strip_whitespace(settings_model, monkeypatch, secrets_dir):
+    monkeypatch.setenv('DB__USER', 'user')
+    secrets_dir.add_files(
+        ('dir1/app_key', 'secret1'),
+        ('dir2/db___password', 'secret2'),
+    )
+    Settings = settings_model(
+        model_config=dict(
+            env_nested_delimiter='__',
+            secrets_nested_delimiter='___',
+        ),
+    )
+    conf = Settings(_secrets_dir=[secrets_dir / 'dir1', secrets_dir / 'dir2'])
+    assert conf.app_key == 'secret1'
+    assert conf.db.password == 'secret2'  # noqa: S105
diff --git a/tests/test_unimportant.py b/tests/test_unimportant.py
index 7a5ebfb..00eaf0a 100644
--- a/tests/test_unimportant.py
+++ b/tests/test_unimportant.py
@@ -1,5 +1,5 @@
 from pydantic_file_secrets import FileSecretsSettingsSource
-from pydantic_settings import BaseSettings, SettingsConfigDict
+from pydantic_settings import BaseSettings, SecretsSettingsSource, SettingsConfigDict
 
 
 def test_repr(secrets_dir):
@@ -8,5 +8,5 @@ class Settings(BaseSettings):
             secrets_dir=secrets_dir,
         )
 
-    src = FileSecretsSettingsSource(Settings)
+    src = FileSecretsSettingsSource(SecretsSettingsSource(Settings))
     assert f'{src!r}'.startswith(f'{src.__class__.__name__}(')