diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e521f01..d62fed31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] env: PYTHON: ${{ matrix.python }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0b7c3f2..23d201bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,8 +22,3 @@ repos: types: [python] language: system pass_filenames: false - - id: pyupgrade - name: Pyupgrade - entry: pyupgrade --py38-plus - types: [python] - language: system diff --git a/docs/index.md b/docs/index.md index a2401a13..7327e1da 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,8 @@ This makes it easy to: For example: ```py -from typing import Any, Callable, Set +from collections.abc import Callable +from typing import Any from pydantic import ( AliasChoices, @@ -58,7 +59,7 @@ class Settings(BaseSettings): # to override domains: # export my_prefix_domains='["foo.com", "bar.com"]' - domains: Set[str] = set() + domains: set[str] = set() # to override more_settings: # export my_prefix_more_settings='{"foo": "x", "apple": 1}' @@ -384,7 +385,7 @@ You may also populate a complex type by providing your own source class. ```py import json import os -from typing import Any, List, Tuple, Type +from typing import Any from pydantic.fields import FieldInfo @@ -405,17 +406,17 @@ class MyCustomSource(EnvSettingsSource): class Settings(BaseSettings): - numbers: List[int] + numbers: list[int] @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (MyCustomSource(settings_cls),) @@ -432,20 +433,19 @@ this behavior for a field and parse the value in your own validator, you can ann ```py import os -from typing import List +from typing import Annotated from pydantic import field_validator -from typing_extensions import Annotated from pydantic_settings import BaseSettings, NoDecode class Settings(BaseSettings): - numbers: Annotated[List[int], NoDecode] # (1)! + numbers: Annotated[list[int], NoDecode] # (1)! @field_validator('numbers', mode='before') @classmethod - def decode_numbers(cls, v: str) -> List[int]: + def decode_numbers(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] @@ -461,7 +461,6 @@ You can also disable JSON parsing for all fields by setting the `enable_decoding ```py import os -from typing import List from pydantic import field_validator @@ -471,11 +470,11 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) - numbers: List[int] + numbers: list[int] @field_validator('numbers', mode='before') @classmethod - def decode_numbers(cls, v: str) -> List[int]: + def decode_numbers(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] @@ -489,10 +488,9 @@ This will bypass the `enable_decoding` config setting: ```py import os -from typing import List +from typing import Annotated from pydantic import field_validator -from typing_extensions import Annotated from pydantic_settings import BaseSettings, ForceDecode, SettingsConfigDict @@ -500,12 +498,12 @@ from pydantic_settings import BaseSettings, ForceDecode, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) - numbers: Annotated[List[int], ForceDecode] - numbers1: List[int] # (1)! + numbers: Annotated[list[int], ForceDecode] + numbers1: list[int] # (1)! @field_validator('numbers1', mode='before') @classmethod - def decode_numbers1(cls, v: str) -> List[int]: + def decode_numbers1(cls, v: str) -> list[int]: return [int(x) for x in v.split(',')] @@ -732,7 +730,6 @@ is customised](#customise-settings-sources): ```py import os import sys -from typing import Tuple, Type from pydantic_settings import ( BaseSettings, @@ -747,12 +744,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, CliSettingsSource(settings_cls, cli_parse_args=True) @@ -774,13 +771,12 @@ CLI argument parsing of lists supports intermixing of any of the below three sty ```py import sys -from typing import List from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True): - my_list: List[int] + my_list: list[int] sys.argv = ['example.py', '--my_list', '[1,2]'] @@ -809,13 +805,12 @@ These can be used in conjunction with list forms as well, e.g: ```py import sys -from typing import Dict from pydantic_settings import BaseSettings class Settings(BaseSettings, cli_parse_args=True): - my_dict: Dict[str, int] + my_dict: dict[str, int] sys.argv = ['example.py', '--my_dict', '{"k1":1,"k2":2}'] @@ -1260,13 +1255,6 @@ default, boolean fields are "explicit", meaning a boolean value must be explicit Additionally, the provided `CliImplicitFlag` and `CliExplicitFlag` annotations can be used for more granular control when necessary. -!!! note - For `python < 3.9` the `--no-flag` option is not generated due to an underlying `argparse` limitation. - -!!! note - For `python < 3.9` the `CliImplicitFlag` and `CliExplicitFlag` annotations can only be applied to optional boolean - fields. - ```py from pydantic_settings import BaseSettings, CliExplicitFlag, CliImplicitFlag @@ -1802,7 +1790,6 @@ Key Vault arrays (e.g. `MySecret--0`, `MySecret--1`) are not supported. ```py import os -from typing import Tuple, Type from azure.identity import DefaultAzureCredential from pydantic import BaseModel @@ -1826,12 +1813,12 @@ class AzureKeyVaultSettings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: az_key_vault_settings = AzureKeyVaultSettingsSource( settings_cls, os.environ['AZURE_KEY_VAULT_URL'], @@ -1863,8 +1850,6 @@ To use them, you can use the same mechanism described [here](#customise-settings ```py -from typing import Tuple, Type - from pydantic import BaseModel from pydantic_settings import ( @@ -1887,12 +1872,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) ``` @@ -1914,8 +1899,6 @@ This is controlled by providing `SettingsConfigDict(pyproject_toml_table_header= By default, `pyproject_toml_table_header=('tool', 'pydantic-settings')` which will load variables from the `[tool.pydantic-settings]` table. ```python -from typing import Tuple, Type - from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, @@ -1932,12 +1915,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) @@ -1977,7 +1960,6 @@ However, there are two options to change this behavior. ```python from pathlib import Path -from typing import Tuple, Type from pydantic_settings import ( BaseSettings, @@ -1995,12 +1977,12 @@ class DiscoverSettings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) @@ -2012,12 +1994,12 @@ class ExplicitFilePathSettings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( PyprojectTomlConfigSettingsSource( settings_cls, Path('~/.config').resolve() / 'pyproject.toml' @@ -2052,8 +2034,6 @@ Each callable should take an instance of the settings class as its sole argument The order of the returned callables decides the priority of inputs; first item is the highest priority. ```py -from typing import Tuple, Type - from pydantic import PostgresDsn from pydantic_settings import BaseSettings, PydanticBaseSettingsSource @@ -2065,12 +2045,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings, file_secret_settings @@ -2088,7 +2068,7 @@ need to add your own custom sources, `settings_customise_sources` makes this ver ```py import json from pathlib import Path -from typing import Any, Dict, Tuple, Type +from typing import Any from pydantic.fields import FieldInfo @@ -2110,7 +2090,7 @@ class JsonConfigSettingsSource(PydanticBaseSettingsSource): def get_field_value( self, field: FieldInfo, field_name: str - ) -> Tuple[Any, str, bool]: + ) -> tuple[Any, str, bool]: encoding = self.config.get('env_file_encoding') file_content_json = json.loads( Path('tests/example_test_config.json').read_text(encoding) @@ -2123,8 +2103,8 @@ class JsonConfigSettingsSource(PydanticBaseSettingsSource): ) -> Any: return value - def __call__(self) -> Dict[str, Any]: - d: Dict[str, Any] = {} + def __call__(self) -> dict[str, Any]: + d: dict[str, Any] = {} for field_name, field in self.settings_cls.model_fields.items(): field_value, field_key, value_is_complex = self.get_field_value( @@ -2147,12 +2127,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, JsonConfigSettingsSource(settings_cls), @@ -2170,7 +2150,7 @@ print(Settings()) Each source of settings can access the output of the previous ones. ```python -from typing import Any, Dict, Tuple +from typing import Any from pydantic.fields import FieldInfo @@ -2180,15 +2160,15 @@ from pydantic_settings import PydanticBaseSettingsSource class MyCustomSource(PydanticBaseSettingsSource): def get_field_value( self, field: FieldInfo, field_name: str - ) -> Tuple[Any, str, bool]: ... + ) -> tuple[Any, str, bool]: ... - def __call__(self) -> Dict[str, Any]: + def __call__(self) -> dict[str, Any]: # Retrieve the aggregated settings from previous sources current_state = self.current_state current_state.get('some_setting') # Retrive settings from all sources individually - # self.settings_sources_data["SettingsSourceName"]: Dict[str, Any] + # self.settings_sources_data["SettingsSourceName"]: dict[str, Any] settings_sources_data = self.settings_sources_data settings_sources_data['SomeSettingsSource'].get('some_setting') @@ -2200,8 +2180,6 @@ class MyCustomSource(PydanticBaseSettingsSource): You might also want to disable a source: ```py -from typing import Tuple, Type - from pydantic import ValidationError from pydantic_settings import BaseSettings, PydanticBaseSettingsSource @@ -2213,12 +2191,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: # here we choose to ignore arguments from init_settings return env_settings, file_secret_settings diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index cfbbac08..09c74560 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -8,11 +8,16 @@ import typing import warnings from abc import ABC, abstractmethod - -if sys.version_info >= (3, 9): - from argparse import BooleanOptionalAction -from argparse import SUPPRESS, ArgumentParser, Namespace, RawDescriptionHelpFormatter, _SubParsersAction +from argparse import ( + SUPPRESS, + ArgumentParser, + BooleanOptionalAction, + Namespace, + RawDescriptionHelpFormatter, + _SubParsersAction, +) from collections import defaultdict, deque +from collections.abc import Iterator, Mapping, Sequence from dataclasses import asdict, is_dataclass from enum import Enum from pathlib import Path @@ -20,15 +25,12 @@ from types import BuiltinFunctionType, FunctionType, SimpleNamespace from typing import ( TYPE_CHECKING, + Annotated, Any, Callable, - Dict, Generic, - Iterator, - Mapping, NoReturn, Optional, - Sequence, TypeVar, Union, cast, @@ -44,7 +46,7 @@ from pydantic.dataclasses import is_pydantic_dataclass from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined -from typing_extensions import Annotated, _AnnotatedAlias, get_args, get_origin +from typing_extensions import _AnnotatedAlias, get_args, get_origin from pydantic_settings.utils import path_type_label @@ -412,7 +414,7 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, def __call__(self) -> dict[str, Any]: return ( - TypeAdapter(Dict[str, Any]).dump_python(self.init_kwargs) + TypeAdapter(dict[str, Any]).dump_python(self.init_kwargs) if self.nested_model_default_partial_update else self.init_kwargs ) @@ -1253,7 +1255,7 @@ def __init__( cli_parse_args = sys.argv[1:] elif not isinstance(cli_parse_args, (list, tuple)): raise SettingsError( - f'cli_parse_args must be List[str] or Tuple[str, ...], recieved {type(cli_parse_args)}' + f'cli_parse_args must be a list or tuple of strings, received {type(cli_parse_args)}' ) self._load_env_vars(parsed_args=self._parse_args(self.root_parser, cli_parse_args)) @@ -1512,12 +1514,6 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, if field_info.annotation is not bool: raise SettingsError(f'{cli_flag_name} argument {model.__name__}.{field_name} is not of type bool') - elif sys.version_info < (3, 9) and ( - field_info.default is PydanticUndefined and field_info.default_factory is None - ): - raise SettingsError( - f'{cli_flag_name} argument {model.__name__}.{field_name} must have default for python versions < 3.9' - ) def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]]: positional_variadic_arg = [] @@ -1812,19 +1808,11 @@ def _check_kebab_name(self, name: str) -> str: def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None: if kwargs['metavar'] == 'bool': - default = None - if field_info.default is not PydanticUndefined: - default = field_info.default - if model_default is not PydanticUndefined: - default = model_default - if sys.version_info >= (3, 9) or isinstance(default, bool): - if (self.cli_implicit_flags or _CliImplicitFlag in field_info.metadata) and ( - _CliExplicitFlag not in field_info.metadata - ): - del kwargs['metavar'] - kwargs['action'] = ( - BooleanOptionalAction if sys.version_info >= (3, 9) else f'store_{str(not default).lower()}' - ) + if (self.cli_implicit_flags or _CliImplicitFlag in field_info.metadata) and ( + _CliExplicitFlag not in field_info.metadata + ): + del kwargs['metavar'] + kwargs['action'] = BooleanOptionalAction def _convert_positional_arg( self, kwargs: dict[str, Any], field_info: FieldInfo, preferred_alias: str, model_default: Any diff --git a/pyproject.toml b/pyproject.toml index 5b2ebfb3..ee1d7131 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ classifiers = [ 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -39,7 +38,7 @@ classifiers = [ 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Internet', ] -requires-python = '>=3.8' +requires-python = '>=3.9' dependencies = [ 'pydantic>=2.7.0', 'python-dotenv>=0.21.0', @@ -87,7 +86,7 @@ source = [ [tool.ruff] line-length = 120 -target-version = 'py38' +target-version = 'py39' [tool.ruff.lint.pyupgrade] keep-runtime-typing = true diff --git a/tests/test_settings.py b/tests/test_settings.py index 1483dd7d..19f933b0 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -4,10 +4,11 @@ import pathlib import sys import uuid +from collections.abc import Hashable from datetime import date, datetime, timezone from enum import IntEnum from pathlib import Path -from typing import Any, Callable, Dict, Generic, Hashable, List, Optional, Set, Tuple, Type, TypeVar, Union +from typing import Annotated, Any, Callable, Generic, Literal, Optional, TypeVar, Union from unittest import mock import pytest @@ -33,7 +34,7 @@ dataclasses as pydantic_dataclasses, ) from pydantic.fields import FieldInfo -from typing_extensions import Annotated, Literal, override +from typing_extensions import override from pydantic_settings import ( BaseSettings, @@ -200,7 +201,7 @@ class Settings(BaseSettings): pytest.param({'pomme': 'pomme-chosen', 'manzano': 'manzano-chosen'}, 'pomme-chosen', id='pomme-priority'), ], ) -def test_populate_by_name_with_alias_choices_when_using_alias(env, env_vars: Dict[str, str], expected_value: str): +def test_populate_by_name_with_alias_choices_when_using_alias(env, env_vars: dict[str, str], expected_value: str): for k, v in env_vars.items(): env.set(k, v) @@ -280,7 +281,7 @@ class Settings(BaseSettings): def test_merge_dict(env): class Settings(BaseSettings): - top: Dict[str, str] + top: dict[str, str] with pytest.raises(ValidationError): Settings() @@ -334,7 +335,7 @@ class Cfg(BaseSettings): def test_nested_env_optional_json(env): class Child(BaseModel): - num_list: Optional[List[int]] = None + num_list: Optional[list[int]] = None class Cfg(BaseSettings, env_nested_delimiter='__'): child: Optional[Child] = None @@ -437,8 +438,8 @@ class DateModel(BaseModel): class ComplexSettings(BaseSettings): - apples: List[str] = [] - bananas: Set[int] = set() + apples: list[str] = [] + bananas: set[int] = set() carrots: dict = {} date: DateModel = DateModel() @@ -452,7 +453,7 @@ def test_list(env): def test_annotated_list(env): class AnnotatedComplexSettings(BaseSettings): - apples: Annotated[List[str], MinLen(2)] = [] + apples: Annotated[list[str], MinLen(2)] = [] env.set('apples', '["russet", "granny smith"]') s = AnnotatedComplexSettings() @@ -937,12 +938,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings env.set('BAR', 'env setting') @@ -957,7 +958,7 @@ def test_config_file_settings_nornir(env): See https://github.com/pydantic/pydantic/pull/341#issuecomment-450378771 """ - def nornir_settings_source() -> Dict[str, Any]: + def nornir_settings_source() -> dict[str, Any]: return {'param_a': 'config a', 'param_b': 'config b', 'param_c': 'config c'} class Settings(BaseSettings): @@ -968,12 +969,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, init_settings, nornir_settings_source env.set('PARAM_C', 'env setting c') @@ -1434,12 +1435,12 @@ def test_read_dotenv_vars_when_env_file_is_none(): def test_dotenvsource_override(env): class StdinDotEnvSettingsSource(DotEnvSettingsSource): @override - def _read_env_file(self, file_path: Path) -> Dict[str, str]: + def _read_env_file(self, file_path: Path) -> dict[str, str]: assert str(file_path) == '-' return {'foo': 'stdin_foo', 'bar': 'stdin_bar'} @override - def _read_env_files(self) -> Dict[str, str]: + def _read_env_files(self) -> dict[str, str]: return self._read_env_file(Path('-')) source = StdinDotEnvSettingsSource(BaseSettings()) @@ -1597,7 +1598,7 @@ def test_secrets_path_json(tmp_path): p.write_text('{"a": "b"}') class Settings(BaseSettings): - foo: Dict[str, str] + foo: dict[str, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) @@ -1624,7 +1625,7 @@ def test_secrets_path_invalid_json(tmp_path): p.write_text('{"a": "b"') class Settings(BaseSettings): - foo: Dict[str, str] + foo: dict[str, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) @@ -1635,7 +1636,7 @@ class Settings(BaseSettings): def test_secrets_missing(tmp_path): class Settings(BaseSettings): foo: str - bar: List[str] + bar: list[str] model_config = SettingsConfigDict(secrets_dir=tmp_path) @@ -1786,10 +1787,10 @@ class Settings(BaseSettings): def test_external_settings_sources_precedence(env): - def external_source_0() -> Dict[str, str]: + def external_source_0() -> dict[str, str]: return {'apple': 'value 0', 'banana': 'value 2'} - def external_source_1() -> Dict[str, str]: + def external_source_1() -> dict[str, str]: return {'apple': 'value 1', 'raspberry': 'value 3'} class Settings(BaseSettings): @@ -1800,12 +1801,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, env_settings, @@ -1823,7 +1824,7 @@ def test_external_settings_sources_filter_env_vars(): vault_storage = {'user:password': {'apple': 'value 0', 'banana': 'value 2'}} class VaultSettingsSource(PydanticBaseSettingsSource): - def __init__(self, settings_cls: Type[BaseSettings], user: str, password: str): + def __init__(self, settings_cls: type[BaseSettings], user: str, password: str): self.user = user self.password = password super().__init__(settings_cls) @@ -1831,7 +1832,7 @@ def __init__(self, settings_cls: Type[BaseSettings], user: str, password: str): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass - def __call__(self) -> Dict[str, str]: + def __call__(self) -> dict[str, str]: vault_vars = vault_storage[f'{self.user}:{self.password}'] return { field_name: vault_vars[field_name] @@ -1846,12 +1847,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, env_settings, @@ -1899,7 +1900,7 @@ def test_builtins_settings_source_repr(): ) -def _parse_custom_dict(value: str) -> Callable[[str], Dict[int, str]]: +def _parse_custom_dict(value: str) -> Callable[[str], dict[int, str]]: """A custom parsing function passed into env parsing test.""" res = {} for part in value.split(','): @@ -1918,17 +1919,17 @@ def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, val def test_env_setting_source_custom_env_parse(env): class Settings(BaseSettings): - top: Dict[int, str] + top: dict[int, str] @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (CustomEnvSettingsSource(settings_cls),) with pytest.raises(ValidationError): @@ -1946,17 +1947,17 @@ def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, val def test_env_settings_source_custom_env_parse_is_bad(env): class Settings(BaseSettings): - top: Dict[int, str] + top: dict[int, str] @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (BadCustomEnvSettingsSource(settings_cls),) env.set('top', '1=apple,2=banana') @@ -1979,18 +1980,18 @@ def test_secret_settings_source_custom_env_parse(tmp_path): p.write_text('1=apple,2=banana') class Settings(BaseSettings): - top: Dict[int, str] + top: dict[int, str] model_config = SettingsConfigDict(secrets_dir=tmp_path) def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (CustomSecretsSettingsSource(settings_cls, tmp_path),) s = Settings() @@ -2004,17 +2005,17 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> Any: def test_custom_source_get_field_value_error(env): class Settings(BaseSettings): - top: Dict[int, str] + top: dict[int, str] @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (BadCustomSettingsSource(settings_cls),) with pytest.raises( @@ -2025,10 +2026,10 @@ def settings_customise_sources( def test_nested_env_complex_values(env): class SubSubModel(BaseSettings): - dvals: Dict + dvals: dict class SubModel(BaseSettings): - vals: List[str] + vals: list[str] sub_sub_model: SubSubModel class Cfg(BaseSettings): @@ -2050,7 +2051,7 @@ class Cfg(BaseSettings): def test_nested_env_nonexisting_field(env): class SubModel(BaseSettings): - vals: List[str] + vals: list[str] class Cfg(BaseSettings): sub_model: SubModel @@ -2064,7 +2065,7 @@ class Cfg(BaseSettings): def test_nested_env_nonexisting_field_deep(env): class SubModel(BaseSettings): - vals: List[str] + vals: list[str] class Cfg(BaseSettings): sub_model: SubModel @@ -2078,7 +2079,7 @@ class Cfg(BaseSettings): def test_nested_env_union_complex_values(env): class SubModel(BaseSettings): - vals: Union[List[str], Dict[str, str]] + vals: Union[list[str], dict[str, str]] class Cfg(BaseSettings): sub_model: SubModel @@ -2462,7 +2463,7 @@ class NestedSettings(BaseSettings, env_nested_delimiter='__'): def test_env_json_field_dict(env): class Settings(BaseSettings): - x: Json[Dict[str, int]] + x: Json[dict[str, int]] env.set('x', '{"foo": 1}') @@ -2517,9 +2518,9 @@ class Settings(BaseSettings, env_prefix='foobar_', title='Test Settings Model'): def test_root_model_as_field(env): class Foo(BaseModel): x: int - y: Dict[str, int] + y: dict[str, int] - FooRoot = RootModel[List[Foo]] + FooRoot = RootModel[list[Foo]] class Settings(BaseSettings): z: FooRoot @@ -2583,7 +2584,7 @@ def test_dotenv_optional_json_field(tmp_path): class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=p) - data: Optional[Json[Dict[str, str]]] = Field(default=None) + data: Optional[Json[dict[str, str]]] = Field(default=None) s = Settings() assert s.data == {'foo': 'bar'} @@ -2660,11 +2661,11 @@ class Settings(BaseSettings): def test_nested_models_as_dict_value(env): class NestedSettings(BaseModel): - foo: Dict[str, int] + foo: dict[str, int] class Settings(BaseSettings): nested: NestedSettings - sub_dict: Dict[str, NestedSettings] + sub_dict: dict[str, NestedSettings] model_config = SettingsConfigDict(env_nested_delimiter='__') @@ -2676,7 +2677,7 @@ class Settings(BaseSettings): def test_env_nested_dict_value(env): class Settings(BaseSettings): - nested: Dict[str, Dict[str, Dict[str, str]]] + nested: dict[str, dict[str, dict[str, str]]] model_config = SettingsConfigDict(env_nested_delimiter='__') @@ -2725,7 +2726,7 @@ class Settings(BaseSettings): def test_case_insensitive_nested_list(env): class NestedSettings(BaseModel): - FOO: List[str] + FOO: list[str] class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=False) @@ -2742,7 +2743,7 @@ class SettingsSource(PydanticBaseSettingsSource): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass - def __call__(self) -> Dict[str, Any]: + def __call__(self) -> dict[str, Any]: current_state = self.current_state if current_state.get('one') == '1': return {'two': '1'} @@ -2756,12 +2757,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (env_settings, SettingsSource(settings_cls)) env.set('one', '1') @@ -2774,7 +2775,7 @@ class SettingsSource(PydanticBaseSettingsSource): def get_field_value(self, field: FieldInfo, field_name: str) -> Any: pass - def __call__(self) -> Dict[str, Any]: + def __call__(self) -> dict[str, Any]: settings_sources_data = self.settings_sources_data if settings_sources_data == { 'InitSettingsSource': {'one': True, 'two': True}, @@ -2797,12 +2798,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (env_settings, init_settings, function_settings_source, SettingsSource(settings_cls)) env.set('one', '1') @@ -2824,7 +2825,6 @@ class Settings(BaseSettings): assert s.model_dump() == {'POSTGRES_USER': 'postgres', 'postgres_name': 'name', 'postgres_user_2': 'postgres2'} -@pytest.mark.skipif(sys.version_info < (3, 9), reason='requires python 3.9 or higher') def test_annotation_is_complex_root_model_check(): """Test for https://github.com/pydantic/pydantic-settings/issues/390""" @@ -2836,7 +2836,7 @@ class Settings(BaseSettings): def test_nested_model_field_with_alias(env): class NestedSettings(BaseModel): - foo: List[str] = Field(alias='fooalias') + foo: list[str] = Field(alias='fooalias') class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') @@ -2851,7 +2851,7 @@ class Settings(BaseSettings): def test_nested_model_field_with_alias_case_sensitive(monkeypatch): class NestedSettings(BaseModel): - foo: List[str] = Field(alias='fooAlias') + foo: list[str] = Field(alias='fooAlias') class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=True) @@ -2878,7 +2878,7 @@ class Settings(BaseSettings): def test_nested_model_field_with_alias_choices(env): class NestedSettings(BaseModel): - foo: List[str] = Field(alias=AliasChoices('fooalias', 'foo-alias')) + foo: list[str] = Field(alias=AliasChoices('fooalias', 'foo-alias')) class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') @@ -2945,13 +2945,13 @@ class Settings(BaseSettings): def test_field_annotated_no_decode(env): class Settings(BaseSettings): - a: List[str] # this field will be decoded because of default `enable_decoding=True` - b: Annotated[List[str], NoDecode] + a: list[str] # this field will be decoded because of default `enable_decoding=True` + b: Annotated[list[str], NoDecode] # decode the value here. the field value won't be decoded because of NoDecode @field_validator('b', mode='before') @classmethod - def decode_b(cls, v: str) -> List[str]: + def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') @@ -2965,12 +2965,12 @@ def test_field_annotated_no_decode_and_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) - a: Annotated[List[str], NoDecode] + a: Annotated[list[str], NoDecode] # decode the value here. the field value won't be decoded because of NoDecode @field_validator('a', mode='before') @classmethod - def decode_b(cls, v: str) -> List[str]: + def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') @@ -2983,12 +2983,12 @@ def test_field_annotated_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) - a: List[str] + a: list[str] # decode the value here. the field value won't be decoded because of `enable_decoding=False` @field_validator('a', mode='before') @classmethod - def decode_b(cls, v: str) -> List[str]: + def decode_b(cls, v: str) -> list[str]: return json.loads(v) env.set('a', '["one", "two"]') @@ -3001,7 +3001,7 @@ def test_field_annotated_force_decode_disable_decoding(env): class Settings(BaseSettings): model_config = SettingsConfigDict(enable_decoding=False) - a: Annotated[List[str], ForceDecode] + a: Annotated[list[str], ForceDecode] env.set('a', '["one", "two"]') diff --git a/tests/test_source_azure_key_vault.py b/tests/test_source_azure_key_vault.py index 7e9b203a..5b039296 100644 --- a/tests/test_source_azure_key_vault.py +++ b/tests/test_source_azure_key_vault.py @@ -2,8 +2,6 @@ Test pydantic_settings.AzureKeyVaultSettingsSource. """ -from typing import Tuple, Type - import pytest from pydantic import BaseModel, Field from pytest_mock import MockerFixture @@ -89,12 +87,12 @@ class AzureKeyVaultSettings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return ( AzureKeyVaultSettingsSource( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential() diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index e24f8026..55f60266 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -5,7 +5,7 @@ import time import typing from enum import IntEnum -from typing import Any, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union +from typing import Annotated, Any, Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union # noqa: UP035 import pytest import typing_extensions @@ -25,7 +25,6 @@ dataclasses as pydantic_dataclasses, ) from pydantic._internal._repr import Representation -from typing_extensions import Annotated, Literal from pydantic_settings import ( BaseSettings, @@ -173,7 +172,7 @@ class Cfg(BaseSettings): v0_union: Union[SubValue, int] top: TopValue - args: List[str] = [] + args: list[str] = [] args += ['--top', '{"v1": "json-1", "v2": "json-2", "sub": {"v5": "xx"}}'] args += ['--top.sub.v5', '5'] args += ['--v0', '0'] @@ -205,12 +204,12 @@ class CfgPrioritized(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return env_settings, CliSettingsSource(settings_cls, cli_parse_args=['--foo', 'FOO FROM CLI']) env.set('FOO', 'FOO FROM ENV') @@ -659,16 +658,16 @@ class Obj(BaseModel): val: int class Child(BaseModel): - num_list: Optional[List[int]] = None - obj_list: Optional[List[Obj]] = None - str_list: Optional[List[str]] = None - union_list: Optional[List[Union[Obj, int]]] = None + num_list: Optional[list[int]] = None + obj_list: Optional[list[Obj]] = None + str_list: Optional[list[str]] = None + union_list: Optional[list[Union[Obj, int]]] = None class Cfg(BaseSettings): - num_list: Optional[List[int]] = None - obj_list: Optional[List[Obj]] = None - union_list: Optional[List[Union[Obj, int]]] = None - str_list: Optional[List[str]] = None + num_list: Optional[list[int]] = None + obj_list: Optional[list[Obj]] = None + union_list: Optional[list[Union[Obj, int]]] = None + str_list: Optional[list[str]] = None child: Optional[Child] = None def check_answer(cfg, prefix, expected): @@ -684,7 +683,7 @@ def check_answer(cfg, prefix, expected): expected['child'] = None assert cfg.model_dump() == expected - args: List[str] = [] + args: list[str] = [] args = [f'--{prefix}num_list', arg_spaces('[1,2]')] args += [f'--{prefix}num_list', arg_spaces('3,4')] args += [f'--{prefix}num_list', '5', f'--{prefix}num_list', '6'] @@ -742,7 +741,7 @@ def check_answer(cfg, prefix, expected): @pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces]) def test_cli_list_json_value_parsing(arg_spaces): class Cfg(BaseSettings): - json_list: List[Union[str, bool, None]] + json_list: list[Union[str, bool, None]] assert CliApp.run( Cfg, @@ -766,13 +765,13 @@ class Cfg(BaseSettings): @pytest.mark.parametrize('prefix', ['', 'child.']) def test_cli_dict_arg(prefix, arg_spaces): class Child(BaseModel): - check_dict: Dict[str, str] + check_dict: dict[str, str] class Cfg(BaseSettings): - check_dict: Optional[Dict[str, str]] = None + check_dict: Optional[dict[str, str]] = None child: Optional[Child] = None - args: List[str] = [] + args: list[str] = [] args = [f'--{prefix}check_dict', arg_spaces('{"k1":"a","k2":"b"}')] args += [f'--{prefix}check_dict', arg_spaces('{"k3":"c"},{"k4":"d"}')] args += [f'--{prefix}check_dict', arg_spaces('{"k5":"e"}'), f'--{prefix}check_dict', arg_spaces('{"k6":"f"}')] @@ -802,7 +801,7 @@ class Cfg(BaseSettings): arg_spaces('k32="x,y"', has_quote_comma=True), ] cfg = CliApp.run(Cfg, cli_args=args) - expected: Dict[str, Any] = { + expected: dict[str, Any] = { 'check_dict': { 'k1': 'a', 'k2': 'b', @@ -853,7 +852,7 @@ class Cfg(BaseSettings): def test_cli_union_dict_arg(): class Cfg(BaseSettings): - union_str_dict: Union[str, Dict[str, Any]] + union_str_dict: Union[str, dict[str, Any]] with pytest.raises(ValidationError) as exc_info: args = ['--union_str_dict', 'hello world', '--union_str_dict', 'hello world'] @@ -902,7 +901,7 @@ class Cfg(BaseSettings): assert cfg.model_dump() == {'union_str_dict': 'hello=world'} class Cfg(BaseSettings): - union_list_dict: Union[List[str], Dict[str, Any]] + union_list_dict: Union[list[str], dict[str, Any]] with pytest.raises(ValidationError) as exc_info: args = ['--union_list_dict', 'hello,world'] @@ -975,7 +974,7 @@ class Cfg(BaseSettings): def test_cli_nested_dict_arg(): class Cfg(BaseSettings): - check_dict: Dict[str, Any] + check_dict: dict[str, Any] args = ['--check_dict', '{"k1":{"a": 1}},{"k2":{"b": 2}}'] cfg = CliApp.run(Cfg, cli_args=args) @@ -1319,10 +1318,10 @@ def test_cli_variadic_positional_arg(env): class MainRequired(BaseSettings): model_config = SettingsConfigDict(cli_parse_args=True) - values: CliPositionalArg[List[int]] + values: CliPositionalArg[list[int]] class MainOptional(MainRequired): - values: CliPositionalArg[List[int]] = [1, 2, 3] + values: CliPositionalArg[list[int]] = [1, 2, 3] assert CliApp.run(MainOptional, cli_args=[]).model_dump() == {'values': [1, 2, 3]} with pytest.raises(SettingsError, match='error parsing CLI: the following arguments are required: VALUES'): @@ -1461,8 +1460,8 @@ class PositionalArgNotOutermost(BaseSettings, cli_parse_args=True): ): class MultipleVariadicPositionialArgs(BaseSettings, cli_parse_args=True): - strings: CliPositionalArg[List[str]] - numbers: CliPositionalArg[List[int]] + strings: CliPositionalArg[list[str]] + numbers: CliPositionalArg[list[int]] MultipleVariadicPositionialArgs() @@ -1472,13 +1471,13 @@ class MultipleVariadicPositionialArgs(BaseSettings, cli_parse_args=True): ): class VariadicPositionialArgAndSubCommand(BaseSettings, cli_parse_args=True): - strings: CliPositionalArg[List[str]] + strings: CliPositionalArg[list[str]] sub_cmd: CliSubCommand[SubCmd] VariadicPositionialArgAndSubCommand() with pytest.raises( - SettingsError, match=re.escape("cli_parse_args must be List[str] or Tuple[str, ...], recieved ") + SettingsError, match=re.escape("cli_parse_args must be a list or tuple of strings, received ") ): class InvalidCliParseArgsType(BaseSettings, cli_parse_args='invalid type'): @@ -1493,63 +1492,30 @@ class CliFlagNotBool(BaseSettings, cli_parse_args=True): CliFlagNotBool() - if sys.version_info < (3, 9): - with pytest.raises( - SettingsError, - match='CliImplicitFlag argument CliFlag38NotOpt.flag must have default for python versions < 3.9', - ): - - class CliFlag38NotOpt(BaseSettings, cli_parse_args=True): - flag: CliImplicitFlag[bool] - - CliFlag38NotOpt() - @pytest.mark.parametrize('enforce_required', [True, False]) def test_cli_bool_flags(monkeypatch, enforce_required): - if sys.version_info < (3, 9): - - class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required): - explicit_req: bool - explicit_opt: bool = False - implicit_opt: CliImplicitFlag[bool] = False - - class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required): - explicit_req: bool - explicit_opt: CliExplicitFlag[bool] = False - implicit_opt: bool = False - - expected = { - 'explicit_req': True, - 'explicit_opt': False, - 'implicit_opt': False, - } - - assert CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True']).model_dump() == expected - assert CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True']).model_dump() == expected - else: + class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required): + explicit_req: bool + explicit_opt: bool = False + implicit_req: CliImplicitFlag[bool] + implicit_opt: CliImplicitFlag[bool] = False + + class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required): + explicit_req: CliExplicitFlag[bool] + explicit_opt: CliExplicitFlag[bool] = False + implicit_req: bool + implicit_opt: bool = False - class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required): - explicit_req: bool - explicit_opt: bool = False - implicit_req: CliImplicitFlag[bool] - implicit_opt: CliImplicitFlag[bool] = False - - class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required): - explicit_req: CliExplicitFlag[bool] - explicit_opt: CliExplicitFlag[bool] = False - implicit_req: bool - implicit_opt: bool = False - - expected = { - 'explicit_req': True, - 'explicit_opt': False, - 'implicit_req': True, - 'implicit_opt': False, - } + expected = { + 'explicit_req': True, + 'explicit_opt': False, + 'implicit_req': True, + 'implicit_opt': False, + } - assert CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected - assert CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected + assert CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected + assert CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected def test_cli_avoid_json(capsys, monkeypatch): @@ -1991,15 +1957,15 @@ class CfgWithSubCommand(BaseSettings): (str, 'str'), ('foobar', 'str'), ('SomeForwardRefString', 'str'), # included to document current behavior; could be changed - (List['SomeForwardRef'], "List[ForwardRef('SomeForwardRef')]"), # noqa: F821 + (List['SomeForwardRef'], "List[ForwardRef('SomeForwardRef')]"), # noqa: F821, UP006 (Union[str, int], '{str,int}'), (list, 'list'), - (List, 'List'), + (List, 'List'), # noqa: UP006 ([1, 2, 3], 'list'), - (List[Dict[str, int]], 'List[Dict[str,int]]'), - (Tuple[str, int, float], 'Tuple[str,int,float]'), - (Tuple[str, ...], 'Tuple[str,...]'), - (Union[int, List[str], Tuple[str, int]], '{int,List[str],Tuple[str,int]}'), + (List[Dict[str, int]], 'List[Dict[str,int]]'), # noqa: UP006 + (Tuple[str, int, float], 'Tuple[str,int,float]'), # noqa: UP006 + (Tuple[str, ...], 'Tuple[str,...]'), # noqa: UP006 + (Union[int, List[str], Tuple[str, int]], '{int,List[str],Tuple[str,int]}'), # noqa: UP006 (foobar, 'foobar'), (LoggedVar, 'LoggedVar'), (LoggedVar(), 'LoggedVar'), @@ -2042,12 +2008,12 @@ def test_cli_metavar_format(hide_none_type, value, expected): [ (lambda: str | int, '{str,int}'), (lambda: list[int], 'list[int]'), - (lambda: List[int], 'List[int]'), + (lambda: List[int], 'List[int]'), # noqa: UP006 (lambda: list[dict[str, int]], 'list[dict[str,int]]'), (lambda: list[Union[str, int]], 'list[{str,int}]'), (lambda: list[str | int], 'list[{str,int}]'), (lambda: LoggedVar[int], 'LoggedVar[int]'), - (lambda: LoggedVar[Dict[int, str]], 'LoggedVar[Dict[int,str]]'), + (lambda: LoggedVar[Dict[int, str]], 'LoggedVar[Dict[int,str]]'), # noqa: UP006 ], ) @pytest.mark.parametrize('hide_none_type', [True, False]) @@ -2357,7 +2323,7 @@ class PolyB(BaseModel): b: str = '2' type: Literal['b'] = 'b' - def _get_type(model: Union[BaseModel, Dict]) -> str: + def _get_type(model: Union[BaseModel, dict]) -> str: if isinstance(model, dict): return model.get('type', 'a') return model.type # type: ignore diff --git a/tests/test_source_json.py b/tests/test_source_json.py index c6360f95..c91b729a 100644 --- a/tests/test_source_json.py +++ b/tests/test_source_json.py @@ -4,7 +4,7 @@ import json from pathlib import Path -from typing import Tuple, Type, Union +from typing import Union from pydantic import BaseModel @@ -41,12 +41,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) s = Settings() @@ -61,12 +61,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) s = Settings() @@ -89,12 +89,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls, json_file=[p5, p6]),) s = Settings() diff --git a/tests/test_source_pyproject_toml.py b/tests/test_source_pyproject_toml.py index 5468d5b5..6378891f 100644 --- a/tests/test_source_pyproject_toml.py +++ b/tests/test_source_pyproject_toml.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -from typing import Optional, Tuple, Type +from typing import Optional import pytest from pydantic import BaseModel @@ -124,8 +124,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() @@ -166,8 +166,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() @@ -209,8 +209,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() @@ -241,8 +241,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() @@ -257,8 +257,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() @@ -277,8 +277,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls, pyproject),) s = Settings() @@ -311,8 +311,8 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( - cls, settings_cls: Type[BaseSettings], **_kwargs: PydanticBaseSettingsSource - ) -> Tuple[PydanticBaseSettingsSource, ...]: + cls, settings_cls: type[BaseSettings], **_kwargs: PydanticBaseSettingsSource + ) -> tuple[PydanticBaseSettingsSource, ...]: return (PyprojectTomlConfigSettingsSource(settings_cls),) s = Settings() diff --git a/tests/test_source_toml.py b/tests/test_source_toml.py index a7230ce2..87346841 100644 --- a/tests/test_source_toml.py +++ b/tests/test_source_toml.py @@ -4,7 +4,6 @@ import sys from pathlib import Path -from typing import Tuple, Type import pytest from pydantic import BaseModel @@ -50,12 +49,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) s = Settings() @@ -71,12 +70,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls),) s = Settings() @@ -105,12 +104,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (TomlConfigSettingsSource(settings_cls, toml_file=[p1, p2]),) s = Settings() diff --git a/tests/test_source_yaml.py b/tests/test_source_yaml.py index 1929b9dc..e3f17e79 100644 --- a/tests/test_source_yaml.py +++ b/tests/test_source_yaml.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Tuple, Type, Union +from typing import Union import pytest from pydantic import BaseModel @@ -42,12 +42,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) with pytest.raises(ImportError, match=r'^PyYAML is not installed, run `pip install pydantic-settings\[yaml\]`$'): @@ -78,12 +78,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() @@ -99,12 +99,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() @@ -122,12 +122,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls),) s = Settings() @@ -156,12 +156,12 @@ class Settings(BaseSettings): @classmethod def settings_customise_sources( cls, - settings_cls: Type[BaseSettings], + settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: return (YamlConfigSettingsSource(settings_cls, yaml_file=[p3, p4]),) s = Settings()