Skip to content

Commit 49f5d5b

Browse files
bump min py 3.9->3.10
1 parent ad1a381 commit 49f5d5b

File tree

17 files changed

+98
-110
lines changed

17 files changed

+98
-110
lines changed

.github/workflows/quality-checks.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
runs-on: ubuntu-22.04
3434
strategy:
3535
matrix:
36-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
36+
python-version: ['3.10', '3.11', '3.12', '3.13']
3737
steps:
3838
- uses: actions/checkout@v4
3939
- name: Set up Python ${{ matrix.python-version }}
@@ -54,7 +54,7 @@ jobs:
5454
runs-on: ubuntu-22.04
5555
strategy:
5656
matrix:
57-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
57+
python-version: ['3.10', '3.11', '3.12', '3.13']
5858
steps:
5959
- uses: actions/checkout@v4
6060
- name: Set up Python ${{ matrix.python-version }}
@@ -80,7 +80,7 @@ jobs:
8080
runs-on: ubuntu-22.04
8181
strategy:
8282
matrix:
83-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
83+
python-version: ['3.10', '3.11', '3.12', '3.13']
8484
steps:
8585
- name: Install Locales for Tests
8686
run: |

CHANGELOG.md

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

33
## Unreleased
44

5+
- chore: bump min python version 3.9->3.10
6+
57
## 1.28.1 (2025-06-08)
68

79
- fix: UserWarning about obsoleted code [#1148](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1144) [#1142](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1148)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ version="1.28.1"
1010
name = "icloudpd"
1111
description = "icloudpd is a command-line tool to download photos and videos from iCloud."
1212
readme = "README_PYPI.md"
13-
requires-python = ">=3.9,<3.14"
13+
requires-python = ">=3.10,<3.14"
1414
keywords = ["icloud", "photo"]
1515
license = {file="LICENSE.md"}
1616
authors=[

scripts/type_check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33
echo "Running mypy..."
4-
python3 -m mypy src tests --strict --python-version 3.9
4+
python3 -m mypy src tests --strict --python-version 3.10
55
# --strict-equality --warn-return-any --disallow-any-generics --disallow-untyped-defs --disallow-untyped-calls --check-untyped-defs

src/foundation/core/optional/__init__.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable, Optional, TypeVar
1+
from typing import Callable, TypeVar
22

33
_Tin = TypeVar("_Tin")
44
_Tin2 = TypeVar("_Tin2")
@@ -7,13 +7,13 @@
77

88

99
def bind(
10-
func: Callable[[_Tin], Optional[_Tout]],
11-
) -> Callable[[Optional[_Tin]], Optional[_Tout]]:
10+
func: Callable[[_Tin], _Tout | None],
11+
) -> Callable[[_Tin | None], _Tout | None]:
1212
"""
1313
Monadic bind for Optional.
1414
1515
Example usage:
16-
>>> def div8(divider: int) -> Optional[float]:
16+
>>> def div8(divider: int) -> float | None:
1717
... if divider == 0:
1818
... return None
1919
... return 8 / divider
@@ -41,7 +41,7 @@ def bind(
4141
4242
"""
4343

44-
def _intern(input: Optional[_Tin]) -> Optional[_Tout]:
44+
def _intern(input: _Tin | None) -> _Tout | None:
4545
if input:
4646
return func(input)
4747
return None
@@ -51,7 +51,7 @@ def _intern(input: Optional[_Tin]) -> Optional[_Tout]:
5151

5252
def lift2(
5353
func: Callable[[_Tin, _Tin2], _Tout],
54-
) -> Callable[[Optional[_Tin], Optional[_Tin2]], Optional[_Tout]]:
54+
) -> Callable[[_Tin | None, _Tin2 | None], _Tout | None]:
5555
"""
5656
Lifts regular function into Optional. (Lift2 for Optional Applicative Functor)
5757
(a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
@@ -75,7 +75,7 @@ def lift2(
7575
7676
"""
7777

78-
def _intern(input: Optional[_Tin], input2: Optional[_Tin2]) -> Optional[_Tout]:
78+
def _intern(input: _Tin | None, input2: _Tin2 | None) -> _Tout | None:
7979
if input and input2:
8080
return func(input, input2)
8181
return None
@@ -85,15 +85,13 @@ def _intern(input: Optional[_Tin], input2: Optional[_Tin2]) -> Optional[_Tout]:
8585

8686
def lift3(
8787
func: Callable[[_Tin, _Tin2, _Tin3], _Tout],
88-
) -> Callable[[Optional[_Tin], Optional[_Tin2], Optional[_Tin3]], Optional[_Tout]]:
88+
) -> Callable[[_Tin | None, _Tin2 | None, _Tin3 | None], _Tout | None]:
8989
"""
9090
Lifts regular function into Optional. see lift2 for Optional Applicative Functor
9191
(a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
9292
"""
9393

94-
def _intern(
95-
input: Optional[_Tin], input2: Optional[_Tin2], input3: Optional[_Tin3]
96-
) -> Optional[_Tout]:
94+
def _intern(input: _Tin | None, input2: _Tin2 | None, input3: _Tin3 | None) -> _Tout | None:
9795
if input and input2 and input3:
9896
return func(input, input2, input3)
9997
return None

src/icloudpd/authentication.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import sys
55
import time
6-
from typing import Callable, Dict, Optional, Tuple
6+
from typing import Callable, Dict, Tuple
77

88
import click
99

@@ -28,24 +28,22 @@ def authenticator(
2828
lp_filename_generator: Callable[[str], str],
2929
raw_policy: RawTreatmentPolicy,
3030
file_match_policy: FileMatchPolicy,
31-
password_providers: Dict[
32-
str, Tuple[Callable[[str], Optional[str]], Callable[[str, str], None]]
33-
],
31+
password_providers: Dict[str, Tuple[Callable[[str], str | None], Callable[[str, str], None]]],
3432
mfa_provider: MFAProvider,
3533
status_exchange: StatusExchange,
36-
) -> Callable[[str, Optional[str], bool, Optional[str]], PyiCloudService]:
34+
) -> Callable[[str, str | None, bool, str | None], PyiCloudService]:
3735
"""Wraping authentication with domain context"""
3836

3937
def authenticate_(
4038
username: str,
41-
cookie_directory: Optional[str] = None,
39+
cookie_directory: str | None = None,
4240
raise_error_on_2sa: bool = False,
43-
client_id: Optional[str] = None,
41+
client_id: str | None = None,
4442
) -> PyiCloudService:
4543
"""Authenticate with iCloud username and password"""
4644
logger.debug("Authenticating...")
47-
icloud: Optional[PyiCloudService] = None
48-
_valid_password: Optional[str] = None
45+
icloud: PyiCloudService | None = None
46+
_valid_password: str | None = None
4947
for _, _pair in password_providers.items():
5048
_reader, _ = _pair
5149
_password = _reader(username)

src/icloudpd/base.py

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@
2828
Dict,
2929
Iterable,
3030
NoReturn,
31-
Optional,
3231
Sequence,
3332
Tuple,
3433
TypeVar,
35-
Union,
3634
cast,
3735
)
3836

@@ -146,17 +144,17 @@ def mfa_provider_generator(
146144
raise ValueError(f"mfa provider has unsupported value of '{provider}'")
147145

148146

149-
def ask_password_in_console(_user: str) -> Optional[str]:
150-
return typing.cast(Optional[str], click.prompt("iCloud Password", hide_input=True))
147+
def ask_password_in_console(_user: str) -> str | None:
148+
return typing.cast(str | None, click.prompt("iCloud Password", hide_input=True))
151149
# return getpass.getpass(
152150
# f'iCloud Password for {_user}:'
153151
# )
154152

155153

156154
def get_password_from_webui(
157155
logger: Logger, status_exchange: StatusExchange
158-
) -> Callable[[str], Optional[str]]:
159-
def _intern(_user: str) -> Optional[str]:
156+
) -> Callable[[str], str | None]:
157+
def _intern(_user: str) -> str | None:
160158
"""Request two-factor authentication through Webui."""
161159
if not status_exchange.replace_status(Status.NO_INPUT_NEEDED, Status.NEED_PASSWORD):
162160
logger.error("Expected NO_INPUT_NEEDED, but got something else")
@@ -216,8 +214,8 @@ def _intern(username: str, password: str) -> None:
216214

217215
def password_provider_generator(
218216
_ctx: click.Context, _param: click.Parameter, providers: Sequence[str]
219-
) -> Dict[str, Tuple[Callable[[str], Optional[str]], Callable[[str, str], None]]]:
220-
def _map(provider: str) -> Tuple[Callable[[str], Optional[str]], Callable[[str, str], None]]:
217+
) -> Dict[str, Tuple[Callable[[str], str | None], Callable[[str, str], None]]]:
218+
def _map(provider: str) -> Tuple[Callable[[str], str | None], Callable[[str, str], None]]:
221219
if provider == "webui":
222220
return (ask_password_in_console, dummy_password_writter)
223221
if provider == "console":
@@ -263,7 +261,7 @@ def file_match_policy_generator(
263261

264262
def skip_created_before_generator(
265263
_ctx: click.Context, _param: click.Parameter, formatted: str
266-
) -> Optional[Union[datetime.datetime, datetime.timedelta]]:
264+
) -> datetime.datetime | datetime.timedelta | None:
267265
if formatted is None:
268266
return None
269267
result = parse_timestamp_or_timedelta(formatted)
@@ -606,16 +604,16 @@ def report_version(ctx: click.Context, _param: click.Parameter, value: bool) ->
606604
callback=report_version,
607605
)
608606
def main(
609-
directory: Optional[str],
607+
directory: str | None,
610608
username: str,
611-
password: Optional[str],
609+
password: str | None,
612610
auth_only: bool,
613611
cookie_directory: str,
614612
size: Sequence[AssetVersionSize],
615613
live_photo_size: LivePhotoVersionSize,
616-
recent: Optional[int],
617-
until_found: Optional[int],
618-
album: Optional[str],
614+
recent: int | None,
615+
until_found: int | None,
616+
album: str | None,
619617
list_albums: bool,
620618
library: str,
621619
list_libraries: bool,
@@ -627,32 +625,30 @@ def main(
627625
only_print_filenames: bool,
628626
folder_structure: str,
629627
set_exif_datetime: bool,
630-
smtp_username: Optional[str],
631-
smtp_password: Optional[str],
628+
smtp_username: str | None,
629+
smtp_password: str | None,
632630
smtp_host: str,
633631
smtp_port: int,
634632
smtp_no_tls: bool,
635-
notification_email: Optional[str],
636-
notification_email_from: Optional[str],
633+
notification_email: str | None,
634+
notification_email_from: str | None,
637635
log_level: str,
638636
no_progress_bar: bool,
639-
notification_script: Optional[str],
637+
notification_script: str | None,
640638
threads_num: int,
641639
delete_after_download: bool,
642-
keep_icloud_recent_days: Optional[int],
640+
keep_icloud_recent_days: int | None,
643641
domain: str,
644-
watch_with_interval: Optional[int],
642+
watch_with_interval: int | None,
645643
dry_run: bool,
646644
filename_cleaner: Callable[[str], str],
647645
lp_filename_generator: Callable[[str], str],
648646
raw_policy: RawTreatmentPolicy,
649-
password_providers: Dict[
650-
str, Tuple[Callable[[str], Optional[str]], Callable[[str, str], None]]
651-
],
647+
password_providers: Dict[str, Tuple[Callable[[str], str | None], Callable[[str, str], None]]],
652648
file_match_policy: FileMatchPolicy,
653649
mfa_provider: MFAProvider,
654650
use_os_locale: bool,
655-
skip_created_before: Optional[Union[datetime.datetime, datetime.timedelta]],
651+
skip_created_before: datetime.datetime | datetime.timedelta | None,
656652
) -> NoReturn:
657653
"""Download all iCloud photos to a local directory"""
658654

@@ -865,7 +861,7 @@ def download_builder(
865861
dry_run: bool,
866862
file_match_policy: FileMatchPolicy,
867863
xmp_sidecar: bool,
868-
skip_created_before: Optional[Union[datetime.datetime, datetime.timedelta]],
864+
skip_created_before: datetime.datetime | datetime.timedelta | None,
869865
) -> Callable[[PyiCloudService], Callable[[Counter, PhotoAsset], bool]]:
870866
"""factory for downloader"""
871867

@@ -1206,43 +1202,41 @@ def composed(ex: Exception, retries: int) -> None:
12061202

12071203
def core(
12081204
downloader: Callable[[PyiCloudService], Callable[[Counter, PhotoAsset], bool]],
1209-
directory: Optional[str],
1205+
directory: str | None,
12101206
username: str,
12111207
auth_only: bool,
12121208
cookie_directory: str,
12131209
primary_sizes: Sequence[AssetVersionSize],
1214-
recent: Optional[int],
1215-
until_found: Optional[int],
1216-
album: Optional[str],
1210+
recent: int | None,
1211+
until_found: int | None,
1212+
album: str | None,
12171213
list_albums: bool,
12181214
library: str,
12191215
list_libraries: bool,
12201216
skip_videos: bool,
12211217
auto_delete: bool,
12221218
only_print_filenames: bool,
12231219
folder_structure: str,
1224-
smtp_username: Optional[str],
1225-
smtp_password: Optional[str],
1220+
smtp_username: str | None,
1221+
smtp_password: str | None,
12261222
smtp_host: str,
12271223
smtp_port: int,
12281224
smtp_no_tls: bool,
1229-
notification_email: Optional[str],
1230-
notification_email_from: Optional[str],
1225+
notification_email: str | None,
1226+
notification_email_from: str | None,
12311227
no_progress_bar: bool,
1232-
notification_script: Optional[str],
1228+
notification_script: str | None,
12331229
delete_after_download: bool,
1234-
keep_icloud_recent_days: Optional[int],
1230+
keep_icloud_recent_days: int | None,
12351231
domain: str,
12361232
logger: logging.Logger,
1237-
watch_interval: Optional[int],
1233+
watch_interval: int | None,
12381234
dry_run: bool,
12391235
filename_cleaner: Callable[[str], str],
12401236
lp_filename_generator: Callable[[str], str],
12411237
raw_policy: RawTreatmentPolicy,
12421238
file_match_policy: FileMatchPolicy,
1243-
password_providers: Dict[
1244-
str, Tuple[Callable[[str], Optional[str]], Callable[[str, str], None]]
1245-
],
1239+
password_providers: Dict[str, Tuple[Callable[[str], str | None], Callable[[str, str], None]]],
12461240
mfa_provider: MFAProvider,
12471241
status_exchange: StatusExchange,
12481242
) -> int:
@@ -1344,7 +1338,7 @@ def core(
13441338

13451339
photos.exception_handler = error_handler
13461340

1347-
photos_count: Optional[int] = len(photos)
1341+
photos_count: int | None = len(photos)
13481342

13491343
photos_enumerator: Iterable[PhotoAsset] = photos
13501344

0 commit comments

Comments
 (0)