Repository: pydantic/pydantic

Issue:
Add encoding to `read_env_file()`
# Feature Request

Output of `import pydantic.utils; print(pydantic.utils.version_info())`:

```
             pydantic version: 1.5.1
            pydantic compiled: True
               python version: 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
                     platform: Windows-7-6.1.7601-SP1
     optional deps. installed: ['email-validator']
```

## Description

Hi, there's known problem on Windows with parsing dotenv files - https://github.com/pypa/pipenv/issues/1963. `python-dotenv` would parse files with default encoding (cp1251 for Cyrillic Windows). As a result we get `Р›РёСЃС‚ 1` instead of `Лист 1`.

It looks like [this](https://github.com/samuelcolvin/pydantic/blob/960b24a5aab7ae0631bfbfbe0047b4d8600c6012/pydantic/env_settings.py#L111) function need to fetch encoding from `Config` class somehow.

<!-- Where possible please include a self-contained code snippet describing your feature request: -->

## Example

`.env` file (UTF-8):

```
foo=Лист 1
```

Code snippet:

```py
import pydantic

class Settings(pydantic.BaseSettings):
    foo: str

    class Config:
        env_file_encoding = 'utf-8'

settings = Settings(_env_file='.env')
print(settings)
# foo='Лист 1'
```



Discussion:

Resolving patch:
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -23,14 +23,28 @@ class BaseSettings(BaseModel):
     Heroku and any 12 factor app design.
     """
 
-    def __init__(__pydantic_self__, _env_file: Union[Path, str, None] = env_file_sentinel, **values: Any) -> None:
+    def __init__(
+        __pydantic_self__,
+        _env_file: Union[Path, str, None] = env_file_sentinel,
+        _env_file_encoding: Optional[str] = None,
+        **values: Any,
+    ) -> None:
         # Uses something other than `self` the first arg to allow "self" as a settable attribute
-        super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
-
-    def _build_values(self, init_kwargs: Dict[str, Any], _env_file: Union[Path, str, None] = None) -> Dict[str, Any]:
-        return deep_update(self._build_environ(_env_file), init_kwargs)
-
-    def _build_environ(self, _env_file: Union[Path, str, None] = None) -> Dict[str, Optional[str]]:
+        super().__init__(
+            **__pydantic_self__._build_values(values, _env_file=_env_file, _env_file_encoding=_env_file_encoding)
+        )
+
+    def _build_values(
+        self,
+        init_kwargs: Dict[str, Any],
+        _env_file: Union[Path, str, None] = None,
+        _env_file_encoding: Optional[str] = None,
+    ) -> Dict[str, Any]:
+        return deep_update(self._build_environ(_env_file, _env_file_encoding), init_kwargs)
+
+    def _build_environ(
+        self, _env_file: Union[Path, str, None] = None, _env_file_encoding: Optional[str] = None
+    ) -> Dict[str, Optional[str]]:
         """
         Build environment variables suitable for passing to the Model.
         """
@@ -42,10 +56,16 @@ def _build_environ(self, _env_file: Union[Path, str, None] = None) -> Dict[str,
             env_vars = {k.lower(): v for k, v in os.environ.items()}
 
         env_file = _env_file if _env_file != env_file_sentinel else self.__config__.env_file
+        env_file_encoding = _env_file_encoding if _env_file_encoding is not None else self.__config__.env_file_encoding
         if env_file is not None:
             env_path = Path(env_file)
             if env_path.is_file():
-                env_vars = {**read_env_file(env_path, case_sensitive=self.__config__.case_sensitive), **env_vars}
+                env_vars = {
+                    **read_env_file(
+                        env_path, encoding=env_file_encoding, case_sensitive=self.__config__.case_sensitive
+                    ),
+                    **env_vars,
+                }
 
         for field in self.__fields__.values():
             env_val: Optional[str] = None
@@ -68,6 +88,7 @@ def _build_environ(self, _env_file: Union[Path, str, None] = None) -> Dict[str,
     class Config:
         env_prefix = ''
         env_file = None
+        env_file_encoding = None
         validate_all = True
         extra = Extra.forbid
         arbitrary_types_allowed = True
@@ -102,13 +123,13 @@ def prepare_field(cls, field: ModelField) -> None:
     __config__: Config  # type: ignore
 
 
-def read_env_file(file_path: Path, *, case_sensitive: bool = False) -> Dict[str, Optional[str]]:
+def read_env_file(file_path: Path, *, encoding: str = None, case_sensitive: bool = False) -> Dict[str, Optional[str]]:
     try:
         from dotenv import dotenv_values
     except ImportError as e:
         raise ImportError('python-dotenv is not installed, run `pip install pydantic[dotenv]`') from e
 
-    file_vars: Dict[str, Optional[str]] = dotenv_values(file_path)
+    file_vars: Dict[str, Optional[str]] = dotenv_values(file_path, encoding=encoding)
     if not case_sensitive:
         return {k.lower(): v for k, v in file_vars.items()}
     else:


Test patch:
diff --git a/tests/test_settings.py b/tests/test_settings.py
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -320,7 +320,7 @@ class Settings(BaseSettings):
         foo: int
         bar: str
 
-        def _build_values(self, init_kwargs, _env_file):
+        def _build_values(self, init_kwargs, _env_file, _env_file_encoding):
             return {**init_kwargs, **self._build_environ()}
 
     env.set('BAR', 'env setting')
@@ -340,7 +340,7 @@ class Settings(BaseSettings):
         b: str
         c: str
 
-        def _build_values(self, init_kwargs, _env_file):
+        def _build_values(self, init_kwargs, _env_file, _env_file_encoding):
             config_settings = init_kwargs.pop('__config_settings__')
             return {**config_settings, **init_kwargs, **self._build_environ()}
 
@@ -430,6 +430,22 @@ class Config:
     assert s.c == 'best string'
 
 
+@pytest.mark.skipif(not dotenv, reason='python-dotenv not installed')
+def test_env_file_config_custom_encoding(tmp_path):
+    p = tmp_path / '.env'
+    p.write_text('pika=p!±@', encoding='latin-1')
+
+    class Settings(BaseSettings):
+        pika: str
+
+        class Config:
+            env_file = p
+            env_file_encoding = 'latin-1'
+
+    s = Settings()
+    assert s.pika == 'p!±@'
+
+
 @pytest.mark.skipif(not dotenv, reason='python-dotenv not installed')
 def test_env_file_none(tmp_path):
     p = tmp_path / '.env'
@@ -529,6 +545,21 @@ class Settings(BaseSettings):
     }
 
 
+@pytest.mark.skipif(not dotenv, reason='python-dotenv not installed')
+def test_env_file_custom_encoding(tmp_path):
+    p = tmp_path / '.env'
+    p.write_text('pika=p!±@', encoding='latin-1')
+
+    class Settings(BaseSettings):
+        pika: str
+
+    with pytest.raises(UnicodeDecodeError):
+        Settings(_env_file=str(p))
+
+    s = Settings(_env_file=str(p), _env_file_encoding='latin-1')
+    assert s.dict() == {'pika': 'p!±@'}
+
+
 @pytest.mark.skipif(dotenv, reason='python-dotenv is installed')
 def test_dotenv_not_installed(tmp_path):
     p = tmp_path / '.env'