diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1541d29 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + # - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + + - repo: https://github.com/pycqa/flake8 + rev: "6.0.0" + hooks: + - id: flake8 diff --git a/README.md b/README.md index e1770a7..8fb388c 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,11 @@ Google Cloud SQL database connections from within Google Cloud Run are supported Alternatively you can set all your databases at once, by using the `DATABASES` setting (either in a `PydanticSettings` sub-class or via the `DJANGO_DATABASES` environment variable: ```python +from pydantic_settings import PydanticSettings, types + + def MySettings(PydanticSettings): - DATABASES = {"default": "sqlite:///db.sqlite3"} # type: ignore + DATABASES: types.DATABASES = {"default": "sqlite:///db.sqlite3"} # type: ignore ``` It is also possible to configure additional database connections with environment variables in the same way as the default `DATABASE_URL` configuration by using a `Field` that has a `configure_database` argument that points to the database alias in the `DATABASES` dictionary. diff --git a/pydantic_settings/cache.py b/pydantic_settings/cache.py index e04aa08..4b35fec 100644 --- a/pydantic_settings/cache.py +++ b/pydantic_settings/cache.py @@ -1,5 +1,5 @@ import re -from typing import Dict +from typing import Any, Dict from urllib.parse import parse_qs from django import VERSION @@ -71,9 +71,9 @@ def is_redis_scheme(self) -> bool: def parse(dsn: CacheDsn) -> dict: """Parses a cache URL.""" backend = CACHE_ENGINES[dsn.scheme] - config = {"BACKEND": backend} + config: dict[str, Any] = {"BACKEND": backend} - options = {} + options: dict[str, Any] = {} if dsn.scheme in REDIS_PARSERS: options["PARSER_CLASS"] = REDIS_PARSERS[dsn.scheme] @@ -81,7 +81,7 @@ def parse(dsn: CacheDsn) -> dict: # File based if dsn.host is None: - path = dsn.path + path = dsn.path or "" if dsn.scheme in FILE_UNIX_PREFIX: path = "unix:" + path diff --git a/pydantic_settings/database.py b/pydantic_settings/database.py index 6202ef3..fa10adf 100644 --- a/pydantic_settings/database.py +++ b/pydantic_settings/database.py @@ -1,6 +1,6 @@ import re import urllib.parse -from typing import Dict, Optional, Pattern, Tuple, cast +from typing import TYPE_CHECKING, Pattern, Tuple, Union, cast from urllib.parse import quote_plus from pydantic import AnyUrl @@ -8,6 +8,9 @@ from pydantic_settings.models import DatabaseModel +if TYPE_CHECKING: + from pydantic.networks import Parts + _cloud_sql_regex_cache = None @@ -26,11 +29,12 @@ } -def cloud_sql_regex() -> Pattern[str]: +def cloud_sql_regex() -> Pattern: global _cloud_sql_regex_cache if _cloud_sql_regex_cache is None: _cloud_sql_regex_cache = re.compile( - r"(?:(?P[a-z][a-z0-9+\-.]+)://)?" # scheme https://tools.ietf.org/html/rfc3986#appendix-A + # scheme https://tools.ietf.org/html/rfc3986#appendix-A + r"(?:(?P[a-z][a-z0-9+\-.]+)://)?" r"(?:(?P[^\s:/]*)(?::(?P[^\s/]*))?@)?" # user info r"(?P/[^\s?#]*)?", # path re.IGNORECASE, @@ -64,19 +68,17 @@ def validate(cls, value, field, config): return super().validate(value, field, config) @classmethod - def validate_host( - cls, parts: Dict[str, str] - ) -> Tuple[Optional[str], Optional[str], str, bool]: - host = None + def validate_host(cls, parts: "Parts") -> Tuple[str, Union[str, None], str, bool]: + host: str | None = None for f in ("domain", "ipv4", "ipv6"): - host = parts[f] + host = cast(Union[str, None], parts.get(f)) if host: break if host is None: - return None, None, "file", False + return None, None, "file", False # type: ignore - if host.startswith("%2F"): + if host and host.startswith("%2F"): return host, None, "socket", False return super().validate_host(parts) diff --git a/pydantic_settings/default.py b/pydantic_settings/default.py index a4a3238..4446b31 100644 --- a/pydantic_settings/default.py +++ b/pydantic_settings/default.py @@ -3,7 +3,7 @@ from pydantic import root_validator from pydantic_settings.models import DatabaseModel, TemplateBackendModel -from pydantic_settings.settings import DatabaseModel, PydanticSettings +from pydantic_settings.settings import PydanticSettings class DjangoDefaultProjectSettings(PydanticSettings): @@ -12,7 +12,9 @@ class DjangoDefaultProjectSettings(PydanticSettings): generates for new projects. """ - DATABASES: Dict[str, DatabaseModel] = {"default": "sqlite:///db.sqlite3"} # type: ignore + DATABASES: Dict[str, DatabaseModel] = { + "default": "sqlite:///db.sqlite3", # type: ignore + } TEMPLATES: List[TemplateBackendModel] = [ TemplateBackendModel.parse_obj(data) @@ -54,7 +56,8 @@ class DjangoDefaultProjectSettings(PydanticSettings): AUTH_PASSWORD_VALIDATORS: List[dict] = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + "NAME": "django.contrib.auth.password_validation" + ".UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", diff --git a/pydantic_settings/sentry.py b/pydantic_settings/sentry.py index 36bfd62..e98bff3 100644 --- a/pydantic_settings/sentry.py +++ b/pydantic_settings/sentry.py @@ -2,8 +2,8 @@ import sentry_sdk from pydantic import AnyUrl +from pydantic.config import BaseConfig from pydantic.fields import ModelField -from pydantic.main import BaseConfig from sentry_sdk.integrations.django import DjangoIntegration from .settings import PydanticSettings diff --git a/pydantic_settings/settings.py b/pydantic_settings/settings.py index e8a94e5..95f37e4 100644 --- a/pydantic_settings/settings.py +++ b/pydantic_settings/settings.py @@ -1,17 +1,6 @@ import inspect from pathlib import Path -from typing import ( - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Pattern, - Sequence, - Tuple, - Union, -) +from typing import Any, Dict, Iterable, List, Sequence, Tuple, Union from django.conf import global_settings, settings from django.core.management.utils import get_random_secret_key @@ -25,18 +14,11 @@ validator, ) from pydantic.fields import ModelField -from pydantic.networks import EmailStr, IPvAnyAddress from pydantic.types import FilePath +from pydantic_settings import types from pydantic_settings.cache import CacheDsn from pydantic_settings.database import DatabaseDsn -from pydantic_settings.models import CacheModel, DatabaseModel, TemplateBackendModel - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - DEFAULT_SETTINGS_MODULE_FIELD = Field( "pydantic_settings.settings.PydanticSettings", env="DJANGO_SETTINGS_MODULE" @@ -44,7 +26,9 @@ class SetUp(BaseSettings): - DJANGO_SETTINGS_MODULE: PyObject = "pydantic_settings.settings.PydanticSettings" + DJANGO_SETTINGS_MODULE: PyObject = ( + "pydantic_settings.settings.PydanticSettings" # type: ignore + ) def configure(self): if settings.configured: @@ -56,7 +40,7 @@ def configure(self): if inspect.isclass(self.DJANGO_SETTINGS_MODULE): settings_obj = self.DJANGO_SETTINGS_MODULE() else: - settings_obj = self.DJANGO_SETTINGS_MODULE + settings_obj = self.DJANGO_SETTINGS_MODULE # type: ignore settings_dict = { key: value @@ -79,265 +63,230 @@ def _get_default_setting(setting: str) -> Any: class PydanticSettings(BaseSettings): - BASE_DIR: Optional[DirectoryPath] = None + BASE_DIR: types.BASE_DIR = None - DEBUG: Optional[bool] = global_settings.DEBUG - DEBUG_PROPAGATE_EXCEPTIONS: Optional[ - bool - ] = global_settings.DEBUG_PROPAGATE_EXCEPTIONS - ADMINS: Optional[List[Tuple[str, EmailStr]]] = _get_default_setting("ADMIN") - INTERNAL_IPS: Optional[List[IPvAnyAddress]] = _get_default_setting("INTERNAL_IPS") + DEBUG: bool = global_settings.DEBUG + DEBUG_PROPAGATE_EXCEPTIONS: bool = global_settings.DEBUG_PROPAGATE_EXCEPTIONS + ADMINS: types.ADMINS = _get_default_setting("ADMIN") + INTERNAL_IPS: types.INTERNAL_IPS = _get_default_setting("INTERNAL_IPS") # Would be nice to do something like Union[Literal["*"], IPvAnyAddress, AnyUrl], but # there are a lot of different options that need to be valid and don't necessarily # fit those types. - ALLOWED_HOSTS: Optional[List[str]] = global_settings.ALLOWED_HOSTS + ALLOWED_HOSTS: List[str] = global_settings.ALLOWED_HOSTS # Validate against actual list of valid TZs? # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List - TIME_ZONE: Optional[str] = global_settings.TIME_ZONE - USE_TZ: Optional[bool] + TIME_ZONE: str = global_settings.TIME_ZONE + USE_TZ: bool = global_settings.USE_TZ # Validate LANGUAGE_CODE and LANGUAGES_BIDI against LANGUAGES. - LANGUAGE_CODE: Optional[str] = global_settings.LANGUAGE_CODE - LANGUAGES: Optional[List[Tuple[str, str]]] = global_settings.LANGUAGES - LANGUAGES_BIDI: Optional[List[str]] = global_settings.LANGUAGES_BIDI - - USE_I18N: Optional[bool] = global_settings.USE_I18N - LOCALE_PATHS: Optional[List[DirectoryPath]] = _get_default_setting("LOCALE_PATHS") - LANGUAGE_COOKIE_NAME: Optional[str] = global_settings.LANGUAGE_COOKIE_NAME - LANGUAGE_COOKIE_AGE: Optional[int] = global_settings.LANGUAGE_COOKIE_AGE - LANGUAGE_COOKIE_DOMAIN: Optional[str] = global_settings.LANGUAGE_COOKIE_DOMAIN - LANGUAGE_COOKIE_PATH: Optional[str] = global_settings.LANGUAGE_COOKIE_PATH - LANGUAGE_COOKIE_SECURE: Optional[bool] = _get_default_setting( + LANGUAGE_CODE: str = global_settings.LANGUAGE_CODE + LANGUAGES: types.LANGUAGES = global_settings.LANGUAGES + LANGUAGES_BIDI: List[str] = global_settings.LANGUAGES_BIDI + + USE_I18N: bool = global_settings.USE_I18N + LOCALE_PATHS: types.LOCALE_PATHS = _get_default_setting("LOCALE_PATHS") + LANGUAGE_COOKIE_NAME: str = global_settings.LANGUAGE_COOKIE_NAME + LANGUAGE_COOKIE_AGE: Union[int, None] = global_settings.LANGUAGE_COOKIE_AGE + LANGUAGE_COOKIE_DOMAIN: Union[str, None] = global_settings.LANGUAGE_COOKIE_DOMAIN + LANGUAGE_COOKIE_PATH: str = global_settings.LANGUAGE_COOKIE_PATH + LANGUAGE_COOKIE_SECURE: Union[bool, None] = _get_default_setting( "LANGUAGE_COOKIE_SECURE" ) - LANGUAGE_COOKIE_HTTPONLY: Optional[bool] = _get_default_setting( + LANGUAGE_COOKIE_HTTPONLY: Union[bool, None] = _get_default_setting( "LANGUAGE_COOKIE_HTTPONLY" ) - LANGUAGE_COOKIE_SAMESITE: Optional[ - Literal["Lax", "Strict", "None"] - ] = _get_default_setting("LANGUAGE_COOKIE_SAMESITE") - USE_L10N: Optional[bool] = global_settings.USE_L10N - MANAGERS: Optional[List[Tuple[str, EmailStr]]] = _get_default_setting("MANAGERS") - DEFAULT_CHARSET: Optional[str] = global_settings.DEFAULT_CHARSET - SERVER_EMAIL: Optional[ - Union[EmailStr, Literal["root@localhost"]] - ] = global_settings.SERVER_EMAIL # type: ignore - - DATABASES: Dict[str, DatabaseModel] = global_settings.DATABASES # type: ignore - DATABASE_ROUTERS: Optional[ - List[str] - ] = global_settings.DATABASE_ROUTERS # type: ignore - EMAIL_BACKEND: Optional[str] = global_settings.EMAIL_BACKEND - EMAIL_HOST: Optional[str] = global_settings.EMAIL_HOST - EMAIL_PORT: Optional[int] = global_settings.EMAIL_PORT - EMAIL_USE_LOCALTIME: Optional[bool] = global_settings.EMAIL_USE_LOCALTIME - EMAIL_HOST_USER: Optional[str] = global_settings.EMAIL_HOST_USER - EMAIL_HOST_PASSWORD: Optional[str] = global_settings.EMAIL_HOST_PASSWORD - EMAIL_USE_TLS: Optional[bool] = global_settings.EMAIL_USE_TLS - EMAIL_USE_SSL: Optional[bool] = global_settings.EMAIL_USE_SSL - EMAIL_SSL_CERTFILE: Optional[ - FilePath + LANGUAGE_COOKIE_SAMESITE: types.LANGUAGE_COOKIE_SAMESITE = _get_default_setting( + "LANGUAGE_COOKIE_SAMESITE" + ) + USE_L10N: bool = global_settings.USE_L10N + MANAGERS: types.MANAGERS = _get_default_setting("MANAGERS") + DEFAULT_CHARSET: str = global_settings.DEFAULT_CHARSET + SERVER_EMAIL: types.SERVER_EMAIL = global_settings.SERVER_EMAIL # type: ignore + + DATABASES: types.DATABASES = global_settings.DATABASES # type: ignore + DATABASE_ROUTERS: List[str] = global_settings.DATABASE_ROUTERS # type: ignore + EMAIL_BACKEND: str = global_settings.EMAIL_BACKEND + EMAIL_HOST: str = global_settings.EMAIL_HOST + EMAIL_PORT: int = global_settings.EMAIL_PORT + EMAIL_USE_LOCALTIME: bool = global_settings.EMAIL_USE_LOCALTIME + EMAIL_HOST_USER: str = global_settings.EMAIL_HOST_USER + EMAIL_HOST_PASSWORD: str = global_settings.EMAIL_HOST_PASSWORD + EMAIL_USE_TLS: bool = global_settings.EMAIL_USE_TLS + EMAIL_USE_SSL: bool = global_settings.EMAIL_USE_SSL + EMAIL_SSL_CERTFILE: Union[ + FilePath, None ] = global_settings.EMAIL_SSL_CERTFILE # type: ignore - EMAIL_SSL_KEYFILE: Optional[ - FilePath + EMAIL_SSL_KEYFILE: Union[ + FilePath, None ] = global_settings.EMAIL_SSL_KEYFILE # type: ignore - EMAIL_TIMEOUT: Optional[int] = global_settings.EMAIL_TIMEOUT - INSTALLED_APPS: Optional[List[str]] = global_settings.INSTALLED_APPS - TEMPLATES: Optional[ - List[TemplateBackendModel] - ] = global_settings.TEMPLATES # type: ignore - FORM_RENDERER: Optional[str] = global_settings.FORM_RENDERER - DEFAULT_FROM_EMAIL: Optional[str] = global_settings.DEFAULT_FROM_EMAIL - EMAIL_SUBJECT_PREFIX: Optional[str] = global_settings.EMAIL_SUBJECT_PREFIX - APPEND_SLASH: Optional[bool] = global_settings.APPEND_SLASH - PREPEND_WWW: Optional[bool] = global_settings.PREPEND_WWW - FORCE_SCRIPT_NAME: Optional[str] = global_settings.FORCE_SCRIPT_NAME - DISALLOWED_USER_AGENTS: Optional[ - List[Pattern] - ] = global_settings.DISALLOWED_USER_AGENTS - ABSOLUTE_URL_OVERRIDES: Optional[ - Dict[str, Callable] - ] = global_settings.ABSOLUTE_URL_OVERRIDES - IGNORABLE_404_URLS: Optional[List[Pattern]] = global_settings.IGNORABLE_404_URLS + EMAIL_TIMEOUT: Union[int, None] = global_settings.EMAIL_TIMEOUT + INSTALLED_APPS: List[str] = global_settings.INSTALLED_APPS + TEMPLATES: types.TEMPLATES = global_settings.TEMPLATES # type: ignore + FORM_RENDERER: str = global_settings.FORM_RENDERER + DEFAULT_FROM_EMAIL: str = global_settings.DEFAULT_FROM_EMAIL + EMAIL_SUBJECT_PREFIX: str = global_settings.EMAIL_SUBJECT_PREFIX + APPEND_SLASH: bool = global_settings.APPEND_SLASH + PREPEND_WWW: bool = global_settings.PREPEND_WWW + FORCE_SCRIPT_NAME: Union[str, None] = global_settings.FORCE_SCRIPT_NAME + DISALLOWED_USER_AGENTS: types.DISALLOWED_USER_AGENTS = ( + global_settings.DISALLOWED_USER_AGENTS + ) + ABSOLUTE_URL_OVERRIDES: types.ABSOLUTE_URL_OVERRIDES = ( + global_settings.ABSOLUTE_URL_OVERRIDES + ) + IGNORABLE_404_URLS: types.IGNORABLE_404_URLS = global_settings.IGNORABLE_404_URLS SECRET_KEY: str = Field(default_factory=get_random_secret_key) - DEFAULT_FILE_STORAGE: Optional[str] = global_settings.DEFAULT_FILE_STORAGE - MEDIA_ROOT: Optional[str] = global_settings.MEDIA_ROOT - MEDIA_URL: Optional[str] = global_settings.MEDIA_URL - STATIC_ROOT: Optional[DirectoryPath] = global_settings.STATIC_ROOT # type: ignore - STATIC_URL: Optional[str] = global_settings.STATIC_URL - FILE_UPLOAD_HANDLERS: Optional[List[str]] = global_settings.FILE_UPLOAD_HANDLERS - FILE_UPLOAD_MAX_MEMORY_SIZE: Optional[ - int - ] = global_settings.FILE_UPLOAD_MAX_MEMORY_SIZE - DATA_UPLOAD_MAX_MEMORY_SIZE: Optional[ - int - ] = global_settings.DATA_UPLOAD_MAX_MEMORY_SIZE - DATA_UPLOAD_MAX_NUMBER_FIELDS: Optional[ - int + DEFAULT_FILE_STORAGE: str = global_settings.DEFAULT_FILE_STORAGE + MEDIA_ROOT: str = global_settings.MEDIA_ROOT + MEDIA_URL: str = global_settings.MEDIA_URL + STATIC_ROOT: Union[ + DirectoryPath, None + ] = global_settings.STATIC_ROOT # type: ignore + STATIC_URL: Union[str, None] = global_settings.STATIC_URL + FILE_UPLOAD_HANDLERS: List[str] = global_settings.FILE_UPLOAD_HANDLERS + FILE_UPLOAD_MAX_MEMORY_SIZE: int = global_settings.FILE_UPLOAD_MAX_MEMORY_SIZE + DATA_UPLOAD_MAX_MEMORY_SIZE: int = global_settings.DATA_UPLOAD_MAX_MEMORY_SIZE + DATA_UPLOAD_MAX_NUMBER_FIELDS: Union[ + None, int ] = global_settings.DATA_UPLOAD_MAX_NUMBER_FIELDS - FILE_UPLOAD_TEMP_DIR: Optional[ - DirectoryPath - ] = global_settings.FILE_UPLOAD_TEMP_DIR # type: ignore - FILE_UPLOAD_PERMISSIONS: Optional[int] = global_settings.FILE_UPLOAD_PERMISSIONS - FILE_UPLOAD_DIRECTORY_PERMISSIONS: Optional[ - int + FILE_UPLOAD_TEMP_DIR: types.FILE_UPLOAD_TEMP_DIR = ( + global_settings.FILE_UPLOAD_TEMP_DIR # type: ignore + ) + FILE_UPLOAD_PERMISSIONS: Union[int, None] = global_settings.FILE_UPLOAD_PERMISSIONS + FILE_UPLOAD_DIRECTORY_PERMISSIONS: Union[ + None, int ] = global_settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS - FORMAT_MODULE_PATH: Optional[str] = global_settings.FORMAT_MODULE_PATH - DATE_FORMAT: Optional[str] = global_settings.DATE_FORMAT - DATETIME_FORMAT: Optional[str] = global_settings.DATETIME_FORMAT - TIME_FORMAT: Optional[str] = global_settings.TIME_FORMAT - YEAR_MONTH_FORMAT: Optional[str] = global_settings.YEAR_MONTH_FORMAT - MONTH_DAY_FORMAT: Optional[str] = global_settings.MONTH_DAY_FORMAT - SHORT_DATE_FORMAT: Optional[str] = global_settings.SHORT_DATE_FORMAT - SHORT_DATETIME_FORMAT: Optional[str] = global_settings.SHORT_DATETIME_FORMAT - DATE_INPUT_FORMATS: Optional[List[str]] = global_settings.DATE_INPUT_FORMATS - TIME_INPUT_FORMATS: Optional[List[str]] = global_settings.TIME_INPUT_FORMATS - DATETIME_INPUT_FORMATS: Optional[List[str]] = global_settings.DATETIME_INPUT_FORMATS - FIRST_DAY_OF_WEEK: Optional[int] = global_settings.FIRST_DAY_OF_WEEK - DECIMAL_SEPARATOR: Optional[str] = global_settings.DECIMAL_SEPARATOR - USE_THOUSAND_SEPARATOR: Optional[bool] = global_settings.USE_THOUSAND_SEPARATOR - THOUSAND_SEPARATOR: Optional[str] = global_settings.THOUSAND_SEPARATOR - DEFAULT_TABLESPACE: Optional[str] = global_settings.DEFAULT_TABLESPACE - DEFAULT_INDEX_TABLESPACE: Optional[str] = global_settings.DEFAULT_INDEX_TABLESPACE - X_FRAME_OPTIONS: Optional[str] = global_settings.X_FRAME_OPTIONS - USE_X_FORWARDED_HOST: Optional[bool] = global_settings.USE_X_FORWARDED_HOST - USE_X_FORWARDED_PORT: Optional[bool] = global_settings.USE_X_FORWARDED_PORT - WSGI_APPLICATION: Optional[str] = None - SECURE_PROXY_SSL_HEADER: Optional[ - Tuple[str, str] - ] = global_settings.SECURE_PROXY_SSL_HEADER - DEFAULT_HASHING_ALGORITHM: Optional[ - Literal["sha1", "sha256"] - ] = _get_default_setting("DEFAULT_HASHING_ALGORITHM") - MIDDLEWARE: Optional[List[str]] = global_settings.MIDDLEWARE - SESSION_CACHE_ALIAS: Optional[str] = global_settings.SESSION_CACHE_ALIAS - SESSION_COOKIE_NAME: Optional[str] = global_settings.SESSION_COOKIE_NAME - SESSION_COOKIE_AGE: Optional[int] = global_settings.SESSION_COOKIE_AGE - SESSION_COOKIE_DOMAIN: Optional[str] = global_settings.SESSION_COOKIE_DOMAIN - SESSION_COOKIE_SECURE: Optional[bool] = global_settings.SESSION_COOKIE_SECURE - SESSION_COOKIE_PATH: Optional[str] = global_settings.SESSION_COOKIE_PATH - SESSION_COOKIE_HTTPONLY: Optional[bool] = global_settings.SESSION_COOKIE_HTTPONLY - SESSION_COOKIE_SAMESITE: Optional[ - Literal["Lax", "Strict", "None"] - ] = _get_default_setting( + FORMAT_MODULE_PATH: Union[str, None] = global_settings.FORMAT_MODULE_PATH + DATE_FORMAT: str = global_settings.DATE_FORMAT + DATETIME_FORMAT: str = global_settings.DATETIME_FORMAT + TIME_FORMAT: str = global_settings.TIME_FORMAT + YEAR_MONTH_FORMAT: str = global_settings.YEAR_MONTH_FORMAT + MONTH_DAY_FORMAT: str = global_settings.MONTH_DAY_FORMAT + SHORT_DATE_FORMAT: str = global_settings.SHORT_DATE_FORMAT + SHORT_DATETIME_FORMAT: str = global_settings.SHORT_DATETIME_FORMAT + DATE_INPUT_FORMATS: List[str] = global_settings.DATE_INPUT_FORMATS + TIME_INPUT_FORMATS: List[str] = global_settings.TIME_INPUT_FORMATS + DATETIME_INPUT_FORMATS: List[str] = global_settings.DATETIME_INPUT_FORMATS + FIRST_DAY_OF_WEEK: int = global_settings.FIRST_DAY_OF_WEEK + DECIMAL_SEPARATOR: str = global_settings.DECIMAL_SEPARATOR + USE_THOUSAND_SEPARATOR: bool = global_settings.USE_THOUSAND_SEPARATOR + THOUSAND_SEPARATOR: str = global_settings.THOUSAND_SEPARATOR + DEFAULT_TABLESPACE: str = global_settings.DEFAULT_TABLESPACE + DEFAULT_INDEX_TABLESPACE: str = global_settings.DEFAULT_INDEX_TABLESPACE + X_FRAME_OPTIONS: str = global_settings.X_FRAME_OPTIONS + USE_X_FORWARDED_HOST: bool = global_settings.USE_X_FORWARDED_HOST + USE_X_FORWARDED_PORT: bool = global_settings.USE_X_FORWARDED_PORT + WSGI_APPLICATION: Union[str, None] = None + SECURE_PROXY_SSL_HEADER: types.SECURE_PROXY_SSL_HEADER = ( + global_settings.SECURE_PROXY_SSL_HEADER + ) + DEFAULT_HASHING_ALGORITHM: types.DEFAULT_HASHING_ALGORITHM = _get_default_setting( + "DEFAULT_HASHING_ALGORITHM" + ) + MIDDLEWARE: List[str] = global_settings.MIDDLEWARE + SESSION_CACHE_ALIAS: str = global_settings.SESSION_CACHE_ALIAS + SESSION_COOKIE_NAME: str = global_settings.SESSION_COOKIE_NAME + SESSION_COOKIE_AGE: Union[int, None] = global_settings.SESSION_COOKIE_AGE + SESSION_COOKIE_DOMAIN: Union[str, None] = global_settings.SESSION_COOKIE_DOMAIN + SESSION_COOKIE_SECURE: bool = global_settings.SESSION_COOKIE_SECURE + SESSION_COOKIE_PATH: str = global_settings.SESSION_COOKIE_PATH + SESSION_COOKIE_HTTPONLY: bool = global_settings.SESSION_COOKIE_HTTPONLY + SESSION_COOKIE_SAMESITE: types.SESSION_COOKIE_SAMESITE = _get_default_setting( "SESSION_COOKIE_SAMESITE" - ) # type: ignore - SESSION_SAVE_EVERY_REQUEST: Optional[ - bool - ] = global_settings.SESSION_SAVE_EVERY_REQUEST - SESSION_EXPIRE_AT_BROWSER_CLOSE: Optional[ - bool - ] = global_settings.SESSION_EXPIRE_AT_BROWSER_CLOSE - SESSION_ENGINE: Optional[str] = global_settings.SESSION_ENGINE - SESSION_FILE_PATH: Optional[ - DirectoryPath - ] = global_settings.SESSION_FILE_PATH # type: ignore - SESSION_SERIALIZER: Optional[str] = global_settings.SESSION_SERIALIZER - CACHES: Dict[str, CacheModel] = global_settings.CACHES # type: ignore - CACHE_MIDDLEWARE_KEY_PREFIX: Optional[ - str - ] = global_settings.CACHE_MIDDLEWARE_KEY_PREFIX - CACHE_MIDDLEWARE_SECONDS: Optional[int] = global_settings.CACHE_MIDDLEWARE_SECONDS - CACHE_MIDDLEWARE_ALIAS: Optional[str] = global_settings.CACHE_MIDDLEWARE_ALIAS - AUTH_USER_MODEL: Optional[str] = global_settings.AUTH_USER_MODEL - AUTHENTICATION_BACKENDS: Optional[ - Sequence[str] - ] = global_settings.AUTHENTICATION_BACKENDS - LOGIN_URL: Optional[str] = global_settings.LOGIN_URL - LOGIN_REDIRECT_URL: Optional[str] = global_settings.LOGIN_REDIRECT_URL - PASSWORD_RESET_TIMEOUT_DAYS: Optional[int] = _get_default_setting( + ) + SESSION_SAVE_EVERY_REQUEST: bool = global_settings.SESSION_SAVE_EVERY_REQUEST + SESSION_EXPIRE_AT_BROWSER_CLOSE: bool = ( + global_settings.SESSION_EXPIRE_AT_BROWSER_CLOSE + ) + SESSION_ENGINE: str = global_settings.SESSION_ENGINE + SESSION_FILE_PATH: types.SESSION_FILE_PATH = ( + global_settings.SESSION_FILE_PATH # type: ignore + ) + SESSION_SERIALIZER: str = global_settings.SESSION_SERIALIZER + CACHES: types.CACHES = global_settings.CACHES # type: ignore + CACHE_MIDDLEWARE_KEY_PREFIX: str = global_settings.CACHE_MIDDLEWARE_KEY_PREFIX + CACHE_MIDDLEWARE_SECONDS: Union[ + int, None + ] = global_settings.CACHE_MIDDLEWARE_SECONDS + CACHE_MIDDLEWARE_ALIAS: str = global_settings.CACHE_MIDDLEWARE_ALIAS + AUTH_USER_MODEL: str = global_settings.AUTH_USER_MODEL + AUTHENTICATION_BACKENDS: Sequence[str] = global_settings.AUTHENTICATION_BACKENDS + LOGIN_URL: str = global_settings.LOGIN_URL + LOGIN_REDIRECT_URL: str = global_settings.LOGIN_REDIRECT_URL + PASSWORD_RESET_TIMEOUT_DAYS: Union[int, None] = _get_default_setting( "PASSWORD_RESET_TIMEOUT_DAYS" ) - PASSWORD_RESET_TIMEOUT: Optional[int] = _get_default_setting( + PASSWORD_RESET_TIMEOUT: Union[int, None] = _get_default_setting( "PASSWORD_RESET_TIMEOUT" ) - PASSWORD_HASHERS: Optional[List[str]] = global_settings.PASSWORD_HASHERS - AUTH_PASSWORD_VALIDATORS: Optional[ - List[dict] - ] = global_settings.AUTH_PASSWORD_VALIDATORS - SIGNING_BACKEND: Optional[str] = global_settings.SIGNING_BACKEND - CSRF_FAILURE_VIEW: Optional[str] = global_settings.CSRF_FAILURE_VIEW - CSRF_COOKIE_NAME: Optional[str] = global_settings.CSRF_COOKIE_NAME - CSRF_COOKIE_AGE: Optional[int] = global_settings.CSRF_COOKIE_AGE - CSRF_COOKIE_DOMAIN: Optional[str] = global_settings.CSRF_COOKIE_DOMAIN - CSRF_COOKIE_PATH: Optional[str] = global_settings.CSRF_COOKIE_PATH - CSRF_COOKIE_SECURE: Optional[bool] = global_settings.CSRF_COOKIE_SECURE - CSRF_COOKIE_HTTPONLY: Optional[bool] = global_settings.CSRF_COOKIE_HTTPONLY - CSRF_COOKIE_SAMESITE: Optional[ - Literal["Lax", "Strict", "None"] - ] = _get_default_setting( + PASSWORD_HASHERS: List[str] = global_settings.PASSWORD_HASHERS + AUTH_PASSWORD_VALIDATORS: List[dict] = global_settings.AUTH_PASSWORD_VALIDATORS + SIGNING_BACKEND: str = global_settings.SIGNING_BACKEND + CSRF_FAILURE_VIEW: str = global_settings.CSRF_FAILURE_VIEW + CSRF_COOKIE_NAME: str = global_settings.CSRF_COOKIE_NAME + CSRF_COOKIE_AGE: Union[int, None] = global_settings.CSRF_COOKIE_AGE + CSRF_COOKIE_DOMAIN: Union[str, None] = global_settings.CSRF_COOKIE_DOMAIN + CSRF_COOKIE_PATH: str = global_settings.CSRF_COOKIE_PATH + CSRF_COOKIE_SECURE: bool = global_settings.CSRF_COOKIE_SECURE + CSRF_COOKIE_HTTPONLY: bool = global_settings.CSRF_COOKIE_HTTPONLY + CSRF_COOKIE_SAMESITE: types.CSRF_COOKIE_SAMESITE = _get_default_setting( "CSRF_COOKIE_SAMESITE" - ) # type: ignore - CSRF_HEADER_NAME: Optional[str] = global_settings.CSRF_HEADER_NAME - CSRF_TRUSTED_ORIGINS: Optional[List[str]] = global_settings.CSRF_TRUSTED_ORIGINS - CSRF_USE_SESSIONS: Optional[bool] = global_settings.CSRF_USE_SESSIONS - MESSAGE_STORAGE: Optional[str] = global_settings.MESSAGE_STORAGE - LOGGING_CONFIG: Optional[str] = global_settings.LOGGING_CONFIG - LOGGING: Optional[dict] = global_settings.LOGGING - DEFAULT_EXCEPTION_REPORTER: Optional[str] = _get_default_setting( + ) + CSRF_HEADER_NAME: str = global_settings.CSRF_HEADER_NAME + CSRF_TRUSTED_ORIGINS: List[str] = global_settings.CSRF_TRUSTED_ORIGINS + CSRF_USE_SESSIONS: bool = global_settings.CSRF_USE_SESSIONS + MESSAGE_STORAGE: str = global_settings.MESSAGE_STORAGE + LOGGING_CONFIG: Union[str, None] = global_settings.LOGGING_CONFIG + LOGGING: dict = global_settings.LOGGING + DEFAULT_EXCEPTION_REPORTER: Union[str, None] = _get_default_setting( "DEFAULT_EXCEPTION_REPORTER" ) - DEFAULT_EXCEPTION_REPORTER_FILTER: Optional[ - str - ] = global_settings.DEFAULT_EXCEPTION_REPORTER_FILTER - TEST_RUNNER: Optional[str] = global_settings.TEST_RUNNER - TEST_NON_SERIALIZED_APPS: Optional[ - List[str] - ] = global_settings.TEST_NON_SERIALIZED_APPS - FIXTURE_DIRS: Optional[ - List[DirectoryPath] - ] = global_settings.FIXTURE_DIRS # type: ignore - STATICFILES_DIRS: Optional[ - List[DirectoryPath] - ] = global_settings.STATICFILES_DIRS # type: ignore - STATICFILES_STORAGE: Optional[str] = global_settings.STATICFILES_STORAGE - STATICFILES_FINDERS: Optional[List[str]] = global_settings.STATICFILES_FINDERS - MIGRATION_MODULES: Optional[Dict[str, str]] = global_settings.MIGRATION_MODULES - SILENCED_SYSTEM_CHECKS: Optional[List[str]] = global_settings.SILENCED_SYSTEM_CHECKS - SECURE_BROWSER_XSS_FILTER: Optional[bool] = _get_default_setting( + DEFAULT_EXCEPTION_REPORTER_FILTER: str = ( + global_settings.DEFAULT_EXCEPTION_REPORTER_FILTER + ) + TEST_RUNNER: str = global_settings.TEST_RUNNER + TEST_NON_SERIALIZED_APPS: List[str] = global_settings.TEST_NON_SERIALIZED_APPS + FIXTURE_DIRS: types.FIXTURE_DIRS = global_settings.FIXTURE_DIRS # type: ignore + STATICFILES_DIRS: types.STATICFILES_DIRS = ( + global_settings.STATICFILES_DIRS # type: ignore + ) + STATICFILES_STORAGE: str = global_settings.STATICFILES_STORAGE + STATICFILES_FINDERS: List[str] = global_settings.STATICFILES_FINDERS + MIGRATION_MODULES: Dict[str, str] = global_settings.MIGRATION_MODULES + SILENCED_SYSTEM_CHECKS: List[str] = global_settings.SILENCED_SYSTEM_CHECKS + SECURE_BROWSER_XSS_FILTER: Union[bool, None] = _get_default_setting( "SECURE_BROWSER_XSS_FILTER" ) - SECURE_CONTENT_TYPE_NOSNIFF: Optional[ - bool - ] = global_settings.SECURE_CONTENT_TYPE_NOSNIFF - SECURE_HSTS_INCLUDE_SUBDOMAINS: Optional[ - bool - ] = global_settings.SECURE_HSTS_INCLUDE_SUBDOMAINS - SECURE_HSTS_PRELOAD: Optional[bool] = global_settings.SECURE_HSTS_PRELOAD - SECURE_HSTS_SECONDS: Optional[int] = global_settings.SECURE_HSTS_SECONDS - SECURE_REDIRECT_EXEMPT: Optional[ - List[Pattern] - ] = global_settings.SECURE_REDIRECT_EXEMPT # type: ignore - SECURE_REFERRER_POLICY: Optional[ - Literal[ - "no-referrer", - "no-referrer-when-downgrade", - "origin", - "origin-when-cross-origin", - "same-origin", - "strict-origin", - "strict-origin-when-cross-origin", - "unsafe-url", - ] - ] = _get_default_setting("SECURE_REFERRER_POLICY") - SECURE_SSL_HOST: Optional[str] = global_settings.SECURE_SSL_HOST - SECURE_SSL_REDIRECT: Optional[bool] = global_settings.SECURE_SSL_REDIRECT + SECURE_CONTENT_TYPE_NOSNIFF: bool = global_settings.SECURE_CONTENT_TYPE_NOSNIFF + SECURE_HSTS_INCLUDE_SUBDOMAINS: bool = ( + global_settings.SECURE_HSTS_INCLUDE_SUBDOMAINS + ) + SECURE_HSTS_PRELOAD: bool = global_settings.SECURE_HSTS_PRELOAD + SECURE_HSTS_SECONDS: Union[int, None] = global_settings.SECURE_HSTS_SECONDS + SECURE_REDIRECT_EXEMPT: types.SECURE_REDIRECT_EXEMPT = ( + global_settings.SECURE_REDIRECT_EXEMPT + ) + SECURE_REFERRER_POLICY: types.SECURE_REFERRER_POLICY = _get_default_setting( + "SECURE_REFERRER_POLICY" + ) + SECURE_SSL_HOST: Union[str, None] = global_settings.SECURE_SSL_HOST + SECURE_SSL_REDIRECT: bool = global_settings.SECURE_SSL_REDIRECT - ROOT_URLCONF: Optional[str] = None + ROOT_URLCONF: Union[str, None] = None - default_database_dsn: Optional[DatabaseDsn] = Field( + default_database_dsn: Union[DatabaseDsn, None] = Field( env="DATABASE_URL", configure_database="default" ) - default_cache_dsn: Optional[CacheDsn] = Field( + default_cache_dsn: Union[CacheDsn, None] = Field( env="CACHE_URL", configure_cache="default" ) class Config: env_prefix = "DJANGO_" - @validator("DATABASES", pre=True) + @validator("DATABASES", pre=True, allow_reuse=True) def parse_databases(cls, databases: dict) -> dict: """ Parse any databases specified as DSNs into DatabaseModel objects. @@ -352,7 +301,7 @@ def parse_databases(cls, databases: dict) -> dict: parsed_databases[key] = value return parsed_databases - @root_validator + @root_validator(allow_reuse=True) def set_default_database(cls, values: dict) -> dict: """ Set the default database if it is not already set and is provided by @@ -361,13 +310,13 @@ def set_default_database(cls, values: dict) -> dict: DATABASES = values["DATABASES"] for db_key, attr in cls._get_dsn_fields(field_extra="configure_database"): if not DATABASES.get(db_key): - database_dsn: Optional[DatabaseDsn] = values[attr] + database_dsn: Union[DatabaseDsn, None] = values[attr] if database_dsn: DATABASES[db_key] = database_dsn.to_settings_model() del values[attr] return values - @root_validator + @root_validator(allow_reuse=True) def set_default_cache(cls, values: dict) -> dict: """ Set the default cache if it is not already set and is provided by @@ -375,7 +324,7 @@ def set_default_cache(cls, values: dict) -> dict: """ CACHES = values.get("CACHES") or {} for cache_key, attr in cls._get_dsn_fields(field_extra="configure_cache"): - cache_dsn: Optional[CacheDsn] = values[attr] + cache_dsn: Union[CacheDsn, None] = values[attr] if cache_dsn: CACHES = values.setdefault("CACHES", {}) CACHES[cache_key] = cache_dsn.to_settings_model() @@ -411,7 +360,7 @@ class is being used then ROOT_URLCONF and WSGI_APPLICATION will be prepended if not values["ROOT_URLCONF"]: values["ROOT_URLCONF"] = f"{project_module_name}.urls" - base_dir: Optional[Path] = values["BASE_DIR"] + base_dir: Union[Path, None] = values["BASE_DIR"] if not base_dir: ancestor = module.__name__.count(".") path = Path(inspect.getfile(module)).resolve() diff --git a/pydantic_settings/types.py b/pydantic_settings/types.py new file mode 100644 index 0000000..7d585ee --- /dev/null +++ b/pydantic_settings/types.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from re import Pattern +from typing import Callable, Dict, List, Literal, Optional, Tuple, Union + +from pydantic import DirectoryPath, EmailStr, IPvAnyAddress + +from pydantic_settings.models import CacheModel, DatabaseModel, TemplateBackendModel + +EmailList = List[Tuple[str, EmailStr]] +ReferrerOptions = Literal[ + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin", + "unsafe-url", +] +RegexList = List[Pattern] +SameSiteOptions = Literal["Lax", "Strict", "None"] + +ABSOLUTE_URL_OVERRIDES = Dict[str, Callable] +ADMINS = Optional[EmailList] +BASE_DIR = Optional[DirectoryPath] +CACHES = Dict[str, CacheModel] +CSRF_COOKIE_SAMESITE = Optional[SameSiteOptions] +DATABASES = Dict[str, DatabaseModel] +DEFAULT_HASHING_ALGORITHM = Optional[Literal["sha1", "sha256"]] +DISALLOWED_USER_AGENTS = RegexList +FILE_UPLOAD_TEMP_DIR = Optional[DirectoryPath] +FIXTURE_DIRS = List[DirectoryPath] +IGNORABLE_404_URLS = RegexList +INTERNAL_IPS = Optional[List[IPvAnyAddress]] +LANGUAGE_COOKIE_SAMESITE = Optional[SameSiteOptions] +LANGUAGES = List[Tuple[str, str]] +LOCALE_PATHS = List[DirectoryPath] +MANAGERS = Optional[EmailList] +SECURE_PROXY_SSL_HEADER = Optional[Tuple[str, str]] +SECURE_REDIRECT_EXEMPT = List[str] +SECURE_REFERRER_POLICY = Optional[ReferrerOptions] +SERVER_EMAIL = Union[EmailStr, Literal["root@localhost"]] +SESSION_COOKIE_SAMESITE = Optional[SameSiteOptions] +SESSION_FILE_PATH = Optional[DirectoryPath] +STATICFILES_DIRS = List[DirectoryPath] +TEMPLATES = List[TemplateBackendModel] diff --git a/tests/settings_proj/asgi.py b/tests/settings_proj/asgi.py index 78a318f..3e8fde2 100644 --- a/tests/settings_proj/asgi.py +++ b/tests/settings_proj/asgi.py @@ -7,7 +7,6 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ """ -from django.conf import settings from django.core.asgi import get_asgi_application from pydantic_settings import SetUp diff --git a/tests/test_env.py b/tests/test_env.py index 4f705f7..d1d9cfa 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -15,7 +15,7 @@ def configure_settings(monkeypatch): def func(env: dict = {}): for key, value in env.items(): - monkeypatch.setenv(key, value) + monkeypatch.setenv(key, str(value)) SetUp().configure() yield func @@ -48,8 +48,8 @@ def test_env_loaded2(configure_settings): def test_sqlite_path(configure_settings): """ - Make sure we aren't improperly stripping the leading slash from the path for SQLite databases with an - absolute path + Make sure we aren't improperly stripping the leading slash from the path for SQLite + databases with an absolute path """ configure_settings({"DATABASE_URL": "sqlite:////db/test.db"}) @@ -123,13 +123,13 @@ def test_base_dir(configure_settings): def test_base_dir_default(configure_settings): configure_settings() - assert settings.BASE_DIR == None + assert settings.BASE_DIR is None def test_dynamic_defaults(configure_settings): configure_settings() - assert settings.ROOT_URLCONF == None - assert settings.WSGI_APPLICATION == None + assert settings.ROOT_URLCONF is None + assert settings.WSGI_APPLICATION is None def test_dynamic_defaults_custom_module(configure_settings): @@ -165,7 +165,10 @@ def test_escaped_gcp_cloudsql_socket(configure_settings): configure_settings( { "DJANGO_BASE_DIR": str(tests_dir), - "DATABASE_URL": "postgres://username:password@%2Fcloudsql%2Fproject%3Aregion%3Ainstance/database", + "DATABASE_URL": ( + "postgres://username:password" + "@%2Fcloudsql%2Fproject%3Aregion%3Ainstance/database" + ), } ) @@ -179,17 +182,15 @@ def test_escaped_gcp_cloudsql_socket(configure_settings): assert default["ENGINE"] == "django.db.backends.postgresql" -from django.conf import global_settings - -gd = global_settings.DATABASES - - def test_unescaped_gcp_cloudsql_socket(configure_settings): tests_dir = Path(__file__).parent configure_settings( { "DJANGO_BASE_DIR": str(tests_dir), - "DATABASE_URL": "postgres://username:password@/cloudsql/project:region:instance/database", + "DATABASE_URL": ( + "postgres://username:password" + "@/cloudsql/project:region:instance/database" + ), } ) @@ -219,7 +220,9 @@ def test_default_db(configure_settings): def test_default_db_no_basedir(configure_settings): configure_settings( { - "DJANGO_SETTINGS_MODULE": "pydantic_settings.default.DjangoDefaultProjectSettings", + "DJANGO_SETTINGS_MODULE": ( + "pydantic_settings.default.DjangoDefaultProjectSettings" + ), } )