Skip to content

Commit 4239ea4

Browse files
authored
Drop Python3.9 support (#699)
1 parent 5008c69 commit 4239ea4

File tree

19 files changed

+917
-1110
lines changed

19 files changed

+917
-1110
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
fail-fast: false
3737
matrix:
3838
os: [ubuntu-latest, macos-latest, windows-latest]
39-
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
39+
python: ['3.10', '3.11', '3.12', '3.13']
4040

4141
env:
4242
PYTHON: ${{ matrix.python }}

pydantic_settings/sources/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
from abc import ABC, abstractmethod
88
from dataclasses import asdict, is_dataclass
99
from pathlib import Path
10-
from typing import TYPE_CHECKING, Any, Optional, cast
10+
from typing import TYPE_CHECKING, Any, cast, get_args
1111

1212
from pydantic import AliasChoices, AliasPath, BaseModel, TypeAdapter
1313
from pydantic._internal._typing_extra import ( # type: ignore[attr-defined]
1414
get_origin,
1515
)
1616
from pydantic._internal._utils import is_model_class
1717
from pydantic.fields import FieldInfo
18-
from typing_extensions import get_args
1918
from typing_inspection import typing_objects
2019
from typing_inspection.introspection import is_union_origin
2120

@@ -36,7 +35,7 @@
3635

3736
def get_subcommand(
3837
model: PydanticModel, is_required: bool = True, cli_exit_on_error: bool | None = None
39-
) -> Optional[PydanticModel]:
38+
) -> PydanticModel | None:
4039
"""
4140
Get the subcommand from a model.
4241

pydantic_settings/sources/providers/aws.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
from collections.abc import Mapping
5-
from typing import TYPE_CHECKING, Optional
5+
from typing import TYPE_CHECKING
66

77
from ..utils import parse_env_vars
88
from .env import EnvSettingsSource
@@ -57,7 +57,7 @@ def __init__(
5757
env_parse_enums=env_parse_enums,
5858
)
5959

60-
def _load_env_vars(self) -> Mapping[str, Optional[str]]:
60+
def _load_env_vars(self) -> Mapping[str, str | None]:
6161
response = self._secretsmanager_client.get_secret_value(SecretId=self._secret_id) # type: ignore
6262

6363
return parse_env_vars(

pydantic_settings/sources/providers/azure.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations as _annotations
44

55
from collections.abc import Iterator, Mapping
6-
from typing import TYPE_CHECKING, Optional
6+
from typing import TYPE_CHECKING
77

88
from pydantic.alias_generators import to_snake
99
from pydantic.fields import FieldInfo
@@ -37,7 +37,7 @@ def import_azure_key_vault() -> None:
3737
) from e
3838

3939

40-
class AzureKeyVaultMapping(Mapping[str, Optional[str]]):
40+
class AzureKeyVaultMapping(Mapping[str, str | None]):
4141
_loaded_secrets: dict[str, str | None]
4242
_secret_client: SecretClient
4343
_secret_names: list[str]
@@ -121,7 +121,7 @@ def __init__(
121121
env_parse_enums=env_parse_enums,
122122
)
123123

124-
def _load_env_vars(self) -> Mapping[str, Optional[str]]:
124+
def _load_env_vars(self) -> Mapping[str, str | None]:
125125
secret_client = SecretClient(vault_url=self._url, credential=self._credential)
126126
return AzureKeyVaultMapping(
127127
secret_client=secret_client,

pydantic_settings/sources/providers/cli.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
_SubParsersAction,
1717
)
1818
from collections import defaultdict
19-
from collections.abc import Mapping, Sequence
19+
from collections.abc import Callable, Mapping, Sequence
2020
from enum import Enum
2121
from functools import cached_property
2222
from textwrap import dedent
@@ -25,14 +25,13 @@
2525
TYPE_CHECKING,
2626
Annotated,
2727
Any,
28-
Callable,
2928
Generic,
3029
Literal,
3130
NoReturn,
32-
Optional,
3331
TypeVar,
34-
Union,
3532
cast,
33+
get_args,
34+
get_origin,
3635
overload,
3736
)
3837

@@ -43,7 +42,6 @@
4342
from pydantic.dataclasses import is_pydantic_dataclass
4443
from pydantic.fields import FieldInfo
4544
from pydantic_core import PydanticUndefined
46-
from typing_extensions import get_args, get_origin
4745
from typing_inspection import typing_objects
4846
from typing_inspection.introspection import is_union_origin
4947

@@ -95,21 +93,21 @@ class _CliArg(BaseModel):
9593
arg_prefix: str
9694
case_sensitive: bool
9795
hide_none_type: bool
98-
kebab_case: Optional[Union[bool, Literal['all', 'no_enums']]]
99-
enable_decoding: Optional[bool]
96+
kebab_case: bool | Literal['all', 'no_enums'] | None
97+
enable_decoding: bool | None
10098
env_prefix_len: int
10199
args: list[str] = []
102100
kwargs: dict[str, Any] = {}
103101

104102
_alias_names: tuple[str, ...] = PrivateAttr(())
105-
_alias_paths: dict[str, Optional[int]] = PrivateAttr({})
103+
_alias_paths: dict[str, int | None] = PrivateAttr({})
106104
_is_alias_path_only: bool = PrivateAttr(False)
107105
_field_info: FieldInfo = PrivateAttr()
108106

109107
def __init__(
110108
self,
111109
field_info: FieldInfo,
112-
parser_map: defaultdict[str | FieldInfo, dict[Optional[int] | str, _CliArg]],
110+
parser_map: defaultdict[str | FieldInfo, dict[int | None | str, _CliArg]],
113111
**values: Any,
114112
) -> None:
115113
super().__init__(**values)
@@ -132,12 +130,12 @@ def __init__(
132130
parser_map[self.field_info][index] = parser_map[alias_path_dest][index]
133131

134132
@classmethod
135-
def get_kebab_case(cls, name: str, kebab_case: Optional[Union[bool, Literal['all', 'no_enums']]]) -> str:
133+
def get_kebab_case(cls, name: str, kebab_case: bool | Literal['all', 'no_enums'] | None) -> str:
136134
return name.replace('_', '-') if kebab_case not in (None, False) else name
137135

138136
@classmethod
139137
def get_enum_names(
140-
cls, annotation: type[Any], kebab_case: Optional[Union[bool, Literal['all', 'no_enums']]]
138+
cls, annotation: type[Any], kebab_case: bool | Literal['all', 'no_enums'] | None
141139
) -> tuple[str, ...]:
142140
enum_names: tuple[str, ...] = ()
143141
annotation = _strip_annotated(annotation)
@@ -157,7 +155,7 @@ def field_info(self) -> FieldInfo:
157155
return self._field_info
158156

159157
@cached_property
160-
def subcommand_dest(self) -> Optional[str]:
158+
def subcommand_dest(self) -> str | None:
161159
return f'{self.arg_prefix}:subcommand' if _CliSubCommand in self.field_info.metadata else None
162160

163161
@cached_property
@@ -206,7 +204,7 @@ def alias_names(self) -> tuple[str, ...]:
206204
return self._alias_names
207205

208206
@cached_property
209-
def alias_paths(self) -> dict[str, Optional[int]]:
207+
def alias_paths(self) -> dict[str, int | None]:
210208
return self._alias_paths
211209

212210
@cached_property
@@ -236,7 +234,7 @@ def is_no_decode(self) -> bool:
236234

237235

238236
T = TypeVar('T')
239-
CliSubCommand = Annotated[Union[T, None], _CliSubCommand]
237+
CliSubCommand = Annotated[T | None, _CliSubCommand]
240238
CliPositionalArg = Annotated[T, _CliPositionalArg]
241239
_CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool)
242240
CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag]
@@ -585,9 +583,7 @@ def _is_nested_alias_path_only_workaround(
585583
return True
586584
return False
587585

588-
def _get_merge_parsed_list_types(
589-
self, parsed_list: list[str], field_name: str
590-
) -> tuple[Optional[type], Optional[type]]:
586+
def _get_merge_parsed_list_types(self, parsed_list: list[str], field_name: str) -> tuple[type | None, type | None]:
591587
merge_type = self._cli_dict_args.get(field_name, list)
592588
if (
593589
merge_type is list
@@ -606,7 +602,7 @@ def _get_merge_parsed_list_types(
606602

607603
def _merged_list_to_str(self, merged_list: list[str], field_name: str) -> str:
608604
decode_list: list[str] = []
609-
is_use_decode: Optional[bool] = None
605+
is_use_decode: bool | None = None
610606
cli_arg_map = self._parser_map.get(field_name, {})
611607
for index, item in enumerate(merged_list):
612608
cli_arg = cli_arg_map.get(index)
@@ -867,7 +863,7 @@ def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
867863
self._add_subparsers = self._connect_parser_method(add_subparsers_method, 'add_subparsers_method')
868864
self._formatter_class = formatter_class
869865
self._cli_dict_args: dict[str, type[Any] | None] = {}
870-
self._parser_map: defaultdict[str | FieldInfo, dict[Optional[int] | str, _CliArg]] = defaultdict(dict)
866+
self._parser_map: defaultdict[str | FieldInfo, dict[int | None | str, _CliArg]] = defaultdict(dict)
871867
self._add_parser_args(
872868
parser=self.root_parser,
873869
model=self.settings_cls,
@@ -892,7 +888,7 @@ def _add_parser_args(
892888
is_model_suppressed: bool = False,
893889
) -> ArgumentParser:
894890
subparsers: Any = None
895-
alias_path_args: dict[str, Optional[int]] = {}
891+
alias_path_args: dict[str, int | None] = {}
896892
# Ignore model default if the default is a model and not a subclass of the current model.
897893
model_default = (
898894
None
@@ -1159,7 +1155,7 @@ def _add_parser_submodels(
11591155
def _add_parser_alias_paths(
11601156
self,
11611157
parser: Any,
1162-
alias_path_args: dict[str, Optional[int]],
1158+
alias_path_args: dict[str, int | None],
11631159
added_args: list[str],
11641160
arg_prefix: str,
11651161
subcommand_prefix: str,

pydantic_settings/sources/providers/env.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
from typing import (
66
TYPE_CHECKING,
77
Any,
8+
get_args,
9+
get_origin,
810
)
911

1012
from pydantic import Json, TypeAdapter, ValidationError
1113
from pydantic._internal._utils import deep_update, is_model_class
1214
from pydantic.dataclasses import is_pydantic_dataclass
1315
from pydantic.fields import FieldInfo
14-
from typing_extensions import get_args, get_origin
1516
from typing_inspection.introspection import is_union_origin
1617

1718
from ...utils import _lenient_issubclass

pydantic_settings/sources/providers/gcp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections.abc import Iterator, Mapping
44
from functools import cached_property
5-
from typing import TYPE_CHECKING, Optional
5+
from typing import TYPE_CHECKING
66

77
from .env import EnvSettingsSource
88

@@ -33,7 +33,7 @@ def import_gcp_secret_manager() -> None:
3333
) from e
3434

3535

36-
class GoogleSecretManagerMapping(Mapping[str, Optional[str]]):
36+
class GoogleSecretManagerMapping(Mapping[str, str | None]):
3737
_loaded_secrets: dict[str, str | None]
3838
_secret_client: SecretManagerServiceClient
3939

@@ -140,7 +140,7 @@ def __init__(
140140
env_parse_enums=env_parse_enums,
141141
)
142142

143-
def _load_env_vars(self) -> Mapping[str, Optional[str]]:
143+
def _load_env_vars(self) -> Mapping[str, str | None]:
144144
return GoogleSecretManagerMapping(
145145
self._secret_client, project_id=self._project_id, case_sensitive=self.case_sensitive
146146
)

pydantic_settings/sources/providers/nested_secrets.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import reduce
44
from glob import iglob
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
6+
from typing import TYPE_CHECKING, Any, Literal, Optional
77

88
from ...exceptions import SettingsError
99
from ...utils import path_type_label
@@ -23,17 +23,17 @@
2323
class NestedSecretsSettingsSource(EnvSettingsSource):
2424
def __init__(
2525
self,
26-
file_secret_settings: Union[PydanticBaseSettingsSource, SecretsSettingsSource],
26+
file_secret_settings: PydanticBaseSettingsSource | SecretsSettingsSource,
2727
secrets_dir: Optional['PathType'] = None,
28-
secrets_dir_missing: Optional[Literal['ok', 'warn', 'error']] = None,
29-
secrets_dir_max_size: Optional[int] = None,
30-
secrets_case_sensitive: Optional[bool] = None,
31-
secrets_prefix: Optional[str] = None,
32-
secrets_nested_delimiter: Optional[str] = None,
33-
secrets_nested_subdir: Optional[bool] = None,
28+
secrets_dir_missing: Literal['ok', 'warn', 'error'] | None = None,
29+
secrets_dir_max_size: int | None = None,
30+
secrets_case_sensitive: bool | None = None,
31+
secrets_prefix: str | None = None,
32+
secrets_nested_delimiter: str | None = None,
33+
secrets_nested_subdir: bool | None = None,
3434
# args for compatibility with SecretsSettingsSource, don't use directly
35-
case_sensitive: Optional[bool] = None,
36-
env_prefix: Optional[str] = None,
35+
case_sensitive: bool | None = None,
36+
env_prefix: str | None = None,
3737
) -> None:
3838
# We allow the first argument to be settings_cls like original
3939
# SecretsSettingsSource. However, it is recommended to pass
@@ -46,7 +46,7 @@ def __init__(
4646
)
4747
# config options
4848
conf = settings_cls.model_config
49-
self.secrets_dir: Optional[PathType] = first_not_none(
49+
self.secrets_dir: PathType | None = first_not_none(
5050
getattr(file_secret_settings, 'secrets_dir', None),
5151
secrets_dir,
5252
conf.get('secrets_dir'),
@@ -79,7 +79,7 @@ def __init__(
7979
)
8080

8181
# nested options
82-
self.secrets_nested_delimiter: Optional[str] = first_not_none(
82+
self.secrets_nested_delimiter: str | None = first_not_none(
8383
secrets_nested_delimiter,
8484
conf.get('secrets_nested_delimiter'),
8585
conf.get('env_nested_delimiter'),

pydantic_settings/sources/types.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
from collections.abc import Sequence
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, Any, Union
7+
from typing import TYPE_CHECKING, Any
88

99
if TYPE_CHECKING:
1010
from pydantic._internal._dataclasses import PydanticDataclass
1111
from pydantic.main import BaseModel
1212

13-
PydanticModel = Union[PydanticDataclass, BaseModel]
13+
PydanticModel = PydanticDataclass | BaseModel
1414
else:
1515
PydanticModel = Any
1616

@@ -31,8 +31,8 @@ class ForceDecode:
3131
pass
3232

3333

34-
DotenvType = Union[Path, str, Sequence[Union[Path, str]]]
35-
PathType = Union[Path, str, Sequence[Union[Path, str]]]
34+
DotenvType = Path | str | Sequence[Path | str]
35+
PathType = Path | str | Sequence[Path | str]
3636
DEFAULT_PATH: PathType = Path('')
3737

3838
# This is used as default value for `_env_file` in the `BaseSettings` class and

pydantic_settings/sources/utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
from collections.abc import Mapping, Sequence
77
from dataclasses import is_dataclass
88
from enum import Enum
9-
from typing import Any, Optional, cast
9+
from typing import Any, cast, get_args, get_origin
1010

1111
from pydantic import BaseModel, Json, RootModel, Secret
1212
from pydantic._internal._utils import is_model_class
1313
from pydantic.dataclasses import is_pydantic_dataclass
14-
from typing_extensions import get_args, get_origin
1514
from typing_inspection import typing_objects
1615

1716
from ..exceptions import SettingsError
@@ -120,7 +119,7 @@ def _strip_annotated(annotation: Any) -> Any:
120119
return annotation
121120

122121

123-
def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> Optional[str]:
122+
def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> str | None:
124123
for type_ in (annotation, get_origin(annotation), *get_args(annotation)):
125124
if _lenient_issubclass(type_, Enum):
126125
if value in tuple(val.value for val in type_):
@@ -149,7 +148,7 @@ def _get_model_fields(model_cls: type[Any]) -> dict[str, Any]:
149148
def _get_alias_names(
150149
field_name: str,
151150
field_info: Any,
152-
alias_path_args: Optional[dict[str, Optional[int]]] = None,
151+
alias_path_args: dict[str, int | None] | None = None,
153152
case_sensitive: bool = True,
154153
) -> tuple[tuple[str, ...], bool]:
155154
"""Get alias names for a field, handling alias paths and case sensitivity."""

0 commit comments

Comments
 (0)