Repository: samuelcolvin/pydantic (python)

Issue:
secrets_dir configuration setting raises an error when the directory doesn't exist
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7.3
            pydantic compiled: True
                 install path: ~/.pyenv/versions/3.8.5/envs/fetcher/lib/python3.8/site-packages/pydantic
               python version: 3.8.5 (default, Sep  4 2020, 14:39:18)  [GCC 9.3.0]
                     platform: Linux-5.8.0-7630-generic-x86_64-with-glibc2.29
     optional deps. installed: ['typing-extensions', 'devtools']
```

I have declared a settings model that can read values from the environment, a `.env` file, or from Docker secrets as follows.

```py
import pydantic

class Settings(BaseSettings):

    password: SecretStr = Field(..., env="APP_PASSWORD")

    class Config:
        """Configure settings values discovery locations."""

        env_file = ".env"
        secrets_dir = "/run/secrets"
```

[As per the documentation](https://pydantic-docs.helpmanual.io/usage/settings/#field-value-priority), checking for a value from a secret is the fourth attempt pydantic makes. However, when I just work in my development environment where the directory `/run/secrets` doesn't exist, instantiating the model raises an error.

```
pydantic.env_settings.SettingsError: directory "/run/secrets" does not exist
```

This is annoying because it happens even though I have the values in a `.env` file. Thus I have to circumvent this by using `Settings(_secrets_dir=".")` which would mean that I need a special case for my dev environment. I think, when values are provided to the instance, or from the environment, or from `.env` first, it shouldn't matter whether the `secrets_dir` exists or not.

Pull request:
fix: Change non-existent secrets dir into a warning
## Change Summary

Previously a non existent secrets directory would throw an error, this has now been turned into a warning.

This makes local development easier, as a secrets directory is usually something that only exists in Kubernetes or Docker.

## Related issue number

Fixes #2175

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [X] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/2175-davidolrik.md b/changes/2175-davidolrik.md
new file mode 100644
index 000000000..eff5995e1
--- /dev/null
+++ b/changes/2175-davidolrik.md
@@ -0,0 +1,1 @@
+Allow non-existent secrets directory by only issuing a warning
diff --git a/changes/2242-tayoogunbiyi.md b/changes/2242-tayoogunbiyi.md
new file mode 100644
index 000000000..db9bb03d4
--- /dev/null
+++ b/changes/2242-tayoogunbiyi.md
@@ -0,0 +1,1 @@
+add documentation for con* type functions
\ No newline at end of file
diff --git a/changes/2249-cybojenix.md b/changes/2249-cybojenix.md
new file mode 100644
index 000000000..ad7021f59
--- /dev/null
+++ b/changes/2249-cybojenix.md
@@ -0,0 +1,2 @@
+Prevent overriding positional arguments with keyword arguments in
+`validate_arguments`, as per behaviour with native functions.
\ No newline at end of file
diff --git a/docs/examples/validation_decorator_parameter_types.py b/docs/examples/validation_decorator_parameter_types.py
index 0f80133b5..9d0b59148 100644
--- a/docs/examples/validation_decorator_parameter_types.py
+++ b/docs/examples/validation_decorator_parameter_types.py
@@ -65,4 +65,4 @@ def armageddon(
 
 
 print(armageddon(1, 2, e=3))
-print(armageddon(1, 2, 3, 4, 5, 6, c=7, e=8, f=9, g=10, spam=11))
+print(armageddon(1, 2, 3, 4, 5, 6, e=8, f=9, g=10, spam=11))
diff --git a/docs/usage/settings.md b/docs/usage/settings.md
index b14a1baf9..654bc2cdd 100644
--- a/docs/usage/settings.md
+++ b/docs/usage/settings.md
@@ -160,8 +160,8 @@ settings = Settings(_secrets_dir='/var/run')
 ```
 
 In either case, the value of the passed argument can be any valid directory, either absolute or relative to the
-current working directory. From there, *pydantic* will handle everything for you by loading in your variables and
-validating them.
+current working directory. **Note that a non existent directory will only generate a warning**.
+From there, *pydantic* will handle everything for you by loading in your variables and validating them.
 
 Even when using a secrets directory, *pydantic* will still read environment variables from a dotenv file or the environment,
 **a dotenv file and environment variables will always take priority over values loaded from the secrets directory**.
diff --git a/docs/usage/types.md b/docs/usage/types.md
index f418dd94d..39aeb92a8 100644
--- a/docs/usage/types.md
+++ b/docs/usage/types.md
@@ -756,6 +756,51 @@ _(This script is complete, it should run "as is")_
 
 Where `Field` refers to the [field function](schema.md#field-customisation).
 
+### Arguments to `conlist`
+The following arguments are available when using the `conlist` type function
+
+- `item_type: Type[T]`: type of the list items
+- `min_items: int = None`: minimum number of items in the list
+- `max_items: int = None`: maximum number of items in the list
+
+### Arguments to `conset`
+The following arguments are available when using the `conset` type function
+
+- `item_type: Type[T]`: type of the set items
+- `min_items: int = None`: minimum number of items in the set
+- `max_items: int = None`: maximum number of items in the set
+
+### Arguments to `conint`
+The following arguments are available when using the `conint` type function
+
+- `strict: bool = False`: controls type coercion
+- `gt: int = None`: enforces integer to be greater than the set value
+- `ge: int = None`: enforces integer to be greater than or equal to the set value
+- `lt: int = None`: enforces integer to be less than the set value
+- `le: int = None`: enforces integer to be less than or equal to the set value
+- `multiple_of: int = None`: enforces integer to be a multiple of the set value
+
+### Arguments to `confloat`
+The following arguments are available when using the `confloat` type function
+
+- `strict: bool = False`: controls type coercion
+- `gt: float = None`: enforces float to be greater than the set value
+- `ge: float = None`: enforces float to be greater than or equal to the set value
+- `lt: float = None`: enforces float to be less than the set value
+- `le: float = None`: enforces float to be less than or equal to the set value
+- `multiple_of: float = None`: enforces float to be a multiple of the set value
+
+### Arguments to `condecimal`
+The following arguments are available when using the `condecimal` type function
+
+- `gt: Decimal = None`: enforces decimal to be greater than the set value
+- `ge: Decimal = None`: enforces decimal to be greater than or equal to the set value
+- `lt: Decimal = None`: enforces decimal to be less than the set value
+- `le: Decimal = None`: enforces decimal to be less than or equal to the set value
+- `max_digits: int = None`: maximum number of digits within the decimal. it does not include a zero before the decimal point or trailing decimal zeroes
+- `decimal_places: int = None`: max number of decimal places allowed. it does not include trailing decimal zeroes
+- `multiple_of: Decimal = None`: enforces decimal to be a multiple of the set value
+
 ### Arguments to `constr`
 The following arguments are available when using the `constr` type function
 
@@ -775,6 +820,7 @@ The following arguments are available when using the `conbytes` type function
 - `min_length: int = None`: minimum length of the byte string
 - `max_length: int = None`: maximum length of the byte string
 
+
 ## Strict Types
 
 You can use the `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool` types 
diff --git a/pydantic/decorator.py b/pydantic/decorator.py
index 85d04c610..383fa2fef 100644
--- a/pydantic/decorator.py
+++ b/pydantic/decorator.py
@@ -66,6 +66,7 @@ def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'Config
 ALT_V_ARGS = 'v__args'
 ALT_V_KWARGS = 'v__kwargs'
 V_POSITIONAL_ONLY_NAME = 'v__positional_only'
+V_DUPLICATE_KWARGS = 'v__duplicate_kwargs'
 
 
 class ValidatedFunction:
@@ -74,10 +75,10 @@ class ValidatedFunction:
 
         parameters: Mapping[str, Parameter] = signature(function).parameters
 
-        if parameters.keys() & {ALT_V_ARGS, ALT_V_KWARGS, V_POSITIONAL_ONLY_NAME}:
+        if parameters.keys() & {ALT_V_ARGS, ALT_V_KWARGS, V_POSITIONAL_ONLY_NAME, V_DUPLICATE_KWARGS}:
             raise ConfigError(
-                f'"{ALT_V_ARGS}", "{ALT_V_KWARGS}" and "{V_POSITIONAL_ONLY_NAME}" are not permitted as argument '
-                f'names when using the "{validate_arguments.__name__}" decorator'
+                f'"{ALT_V_ARGS}", "{ALT_V_KWARGS}", "{V_POSITIONAL_ONLY_NAME}" and "{V_DUPLICATE_KWARGS}" '
+                f'are not permitted as argument names when using the "{validate_arguments.__name__}" decorator'
             )
 
         self.raw_function = function
@@ -105,6 +106,7 @@ class ValidatedFunction:
             elif p.kind == Parameter.POSITIONAL_OR_KEYWORD:
                 self.arg_mapping[i] = name
                 fields[name] = annotation, default
+                fields[V_DUPLICATE_KWARGS] = List[str], None
             elif p.kind == Parameter.KEYWORD_ONLY:
                 fields[name] = annotation, default
             elif p.kind == Parameter.VAR_POSITIONAL:
@@ -161,11 +163,14 @@ class ValidatedFunction:
 
         var_kwargs = {}
         wrong_positional_args = []
+        duplicate_kwargs = []
         non_var_fields = set(self.model.__fields__) - {self.v_args_name, self.v_kwargs_name}
         for k, v in kwargs.items():
             if k in non_var_fields:
                 if k in self.positional_only_args:
                     wrong_positional_args.append(k)
+                if k in values:
+                    duplicate_kwargs.append(k)
                 values[k] = v
             else:
                 var_kwargs[k] = v
@@ -174,6 +179,8 @@ class ValidatedFunction:
             values[self.v_kwargs_name] = var_kwargs
         if wrong_positional_args:
             values[V_POSITIONAL_ONLY_NAME] = wrong_positional_args
+        if duplicate_kwargs:
+            values[V_DUPLICATE_KWARGS] = duplicate_kwargs
         return values
 
     def execute(self, m: BaseModel) -> Any:
@@ -246,6 +253,12 @@ class ValidatedFunction:
                 keys = ', '.join(map(repr, v))
                 raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}')
 
+            @validator(V_DUPLICATE_KWARGS, check_fields=False, allow_reuse=True)
+            def check_duplicate_kwargs(cls, v: List[str]) -> None:
+                plural = '' if len(v) == 1 else 's'
+                keys = ', '.join(map(repr, v))
+                raise TypeError(f'multiple values for argument{plural}: {keys}')
+
             class Config(CustomConfig):
                 extra = Extra.forbid
 
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
index ff8ab140a..71b5a976c 100644
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -197,7 +197,9 @@ class SecretsSettingsSource:
         secrets_path = Path(self.secrets_dir).expanduser()
 
         if not secrets_path.exists():
-            raise SettingsError(f'directory "{secrets_path}" does not exist')
+            warnings.warn(f'directory "{secrets_path}" does not exist')
+            return secrets
+
         if not secrets_path.is_dir():
             raise SettingsError(f'secrets_dir must reference a directory, not a {path_type(secrets_path)}')
 


Test patch:
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index fa668d5f5..83d159c10 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -52,6 +52,20 @@ def test_args():
         {'loc': ('kwargs',), 'msg': "unexpected keyword argument: 'apple'", 'type': 'type_error'}
     ]
 
+    with pytest.raises(ValidationError) as exc_info:
+        foo(1, 2, a=3)
+
+    assert exc_info.value.errors() == [
+        {'loc': ('v__duplicate_kwargs',), 'msg': "multiple values for argument: 'a'", 'type': 'type_error'}
+    ]
+
+    with pytest.raises(ValidationError) as exc_info:
+        foo(1, 2, a=3, b=4)
+
+    assert exc_info.value.errors() == [
+        {'loc': ('v__duplicate_kwargs',), 'msg': "multiple values for arguments: 'a', 'b'", 'type': 'type_error'}
+    ]
+
 
 def test_wrap():
     @validate_arguments
@@ -68,7 +82,7 @@ def test_wrap():
     assert foo_bar.vd.arg_mapping == {0: 'a', 1: 'b'}
     assert foo_bar.vd.positional_only_args == set()
     assert issubclass(foo_bar.model, BaseModel)
-    assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs'}
+    assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs', 'v__duplicate_kwargs'}
     assert foo_bar.model.__name__ == 'FooBar'
     assert foo_bar.model.schema()['title'] == 'FooBar'
     # signature is slightly different on 3.6
@@ -168,7 +182,7 @@ def test_args_name():
     def foo(args: int, kwargs: int):
         return f'args={args!r}, kwargs={kwargs!r}'
 
-    assert foo.model.__fields__.keys() == {'args', 'kwargs', 'v__args', 'v__kwargs'}
+    assert foo.model.__fields__.keys() == {'args', 'kwargs', 'v__args', 'v__kwargs', 'v__duplicate_kwargs'}
     assert foo(1, 2) == 'args=1, kwargs=2'
 
     with pytest.raises(ValidationError) as exc_info:
@@ -191,10 +205,36 @@ def test_args_name():
 
 
 def test_v_args():
-    with pytest.raises(ConfigError, match='"v__args", "v__kwargs" and "v__positional_only" are not permitted'):
+    with pytest.raises(
+        ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted'
+    ):
+
+        @validate_arguments
+        def foo1(v__args: int):
+            pass
+
+    with pytest.raises(
+        ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted'
+    ):
+
+        @validate_arguments
+        def foo2(v__kwargs: int):
+            pass
+
+    with pytest.raises(
+        ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted'
+    ):
+
+        @validate_arguments
+        def foo3(v__positional_only: int):
+            pass
+
+    with pytest.raises(
+        ConfigError, match='"v__args", "v__kwargs", "v__positional_only" and "v__duplicate_kwargs" are not permitted'
+    ):
 
         @validate_arguments
-        def foo(v__args: int):
+        def foo4(v__duplicate_kwargs: int):
             pass
 
 
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 9a4f53c88..7513c4c53 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -835,12 +835,10 @@ def test_secrets_invalid_secrets_dir(tmp_path):
 @pytest.mark.skipif(sys.platform.startswith('win'), reason='windows paths break regex')
 def test_secrets_missing_location(tmp_path):
     class Settings(BaseSettings):
-        foo: str
-
         class Config:
             secrets_dir = tmp_path / 'does_not_exist'
 
-    with pytest.raises(SettingsError, match=f'directory "{tmp_path}/does_not_exist" does not exist'):
+    with pytest.warns(UserWarning, match=f'directory "{tmp_path}/does_not_exist" does not exist'):
         Settings()