From f404aa25e85b5bb6d43f5f2d7aea95ae62290207 Mon Sep 17 00:00:00 2001 From: Kyungmin Lee Date: Sat, 4 Sep 2021 19:40:53 +0900 Subject: [PATCH] fix: support properly path type (#2801) * feat: add `StrPath` type * fix: support properly path type * add test * fix: merge * remove useless if statement * fix: cython Co-authored-by: PrettyWood --- pydantic/env_settings.py | 22 ++++++++++++---------- pydantic/typing.py | 13 +++++++++++++ tests/mypy/modules/success.py | 21 ++++++++++++++++++++- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index 2c8c11f..c6a0219 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -6,7 +6,7 @@ from typing import AbstractSet, Any, Callable, Dict, List, Mapping, Optional, Tu from .config import BaseConfig, Extra from .fields import ModelField from .main import BaseModel -from .typing import display_as_type +from .typing import StrPath, display_as_type from .utils import deep_update, path_type, sequence_like env_file_sentinel = str(object()) @@ -28,9 +28,9 @@ class BaseSettings(BaseModel): def __init__( __pydantic_self__, - _env_file: Union[Path, str, None] = env_file_sentinel, + _env_file: Optional[StrPath] = env_file_sentinel, _env_file_encoding: Optional[str] = None, - _secrets_dir: Union[Path, str, None] = None, + _secrets_dir: Optional[StrPath] = None, **values: Any, ) -> None: # Uses something other than `self` the first arg to allow "self" as a settable attribute @@ -43,9 +43,9 @@ class BaseSettings(BaseModel): def _build_values( self, init_kwargs: Dict[str, Any], - _env_file: Union[Path, str, None] = None, + _env_file: Optional[StrPath] = None, _env_file_encoding: Optional[str] = None, - _secrets_dir: Union[Path, str, None] = None, + _secrets_dir: Optional[StrPath] = None, ) -> Dict[str, Any]: # Configure built-in sources init_settings = InitSettingsSource(init_kwargs=init_kwargs) @@ -133,8 +133,8 @@ class InitSettingsSource: class EnvSettingsSource: __slots__ = ('env_file', 'env_file_encoding') - def __init__(self, env_file: Union[Path, str, None], env_file_encoding: Optional[str]): - self.env_file: Union[Path, str, None] = env_file + def __init__(self, env_file: Optional[StrPath], env_file_encoding: Optional[str]): + self.env_file: Optional[StrPath] = env_file self.env_file_encoding: Optional[str] = env_file_encoding def __call__(self, settings: BaseSettings) -> Dict[str, Any]: @@ -183,8 +183,8 @@ class EnvSettingsSource: class SecretsSettingsSource: __slots__ = ('secrets_dir',) - def __init__(self, secrets_dir: Union[Path, str, None]): - self.secrets_dir: Union[Path, str, None] = secrets_dir + def __init__(self, secrets_dir: Optional[StrPath]): + self.secrets_dir: Optional[StrPath] = secrets_dir def __call__(self, settings: BaseSettings) -> Dict[str, Any]: """ @@ -221,7 +221,9 @@ class SecretsSettingsSource: return f'SecretsSettingsSource(secrets_dir={self.secrets_dir!r})' -def read_env_file(file_path: Path, *, encoding: str = None, case_sensitive: bool = False) -> Dict[str, Optional[str]]: +def read_env_file( + file_path: StrPath, *, encoding: str = None, case_sensitive: bool = False +) -> Dict[str, Optional[str]]: try: from dotenv import dotenv_values except ImportError as e: diff --git a/pydantic/typing.py b/pydantic/typing.py index fff0b89..156db0b 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -1,4 +1,5 @@ import sys +from os import PathLike from typing import ( # type: ignore TYPE_CHECKING, AbstractSet, @@ -206,6 +207,17 @@ else: WithArgsTypes = (typing._GenericAlias, types.GenericAlias, types.UnionType) +if sys.version_info < (3, 9): + StrPath = Union[str, PathLike] +else: + StrPath = Union[str, PathLike] + # TODO: Once we switch to Cython 3 to handle generics properly + # (https://github.com/cython/cython/issues/2753), use following lines instead + # of the one above + # # os.PathLike only becomes subscriptable from Python 3.9 onwards + # StrPath = Union[str, PathLike[str]] + + if TYPE_CHECKING: from .fields import ModelField @@ -256,6 +268,7 @@ __all__ = ( 'typing_base', 'get_all_type_hints', 'is_union_origin', + 'StrPath', ) diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py index 39fff47..f9c68b7 100644 --- a/tests/mypy/modules/success.py +++ b/tests/mypy/modules/success.py @@ -4,9 +4,10 @@ Test pydantic's compliance with mypy. Do a little skipping about with types to demonstrate its usage. """ import json +import os import sys from datetime import date, datetime, timedelta -from pathlib import Path +from pathlib import Path, PurePath from typing import Any, Dict, Generic, List, Optional, TypeVar from uuid import UUID @@ -14,6 +15,7 @@ from pydantic import ( UUID1, BaseConfig, BaseModel, + BaseSettings, DirectoryPath, Extra, FilePath, @@ -250,3 +252,20 @@ class Config(BaseConfig): title = 'Record' extra = Extra.ignore max_anystr_length = 1234 + + +class Settings(BaseSettings): + ... + + +class CustomPath(PurePath): + def __init__(self, *args: str): + self.path = os.path.join(*args) + + def __fspath__(self) -> str: + return f'a/custom/{self.path}' + + +def dont_check_path_existence() -> None: + Settings(_env_file='a/path', _secrets_dir='a/path') + Settings(_env_file=CustomPath('a/path'), _secrets_dir=CustomPath('a/path'))