Skip to content

Commit a305301

Browse files
teh-hippoCopilot
andcommitted
fix: use timezone-aware fallback datetimes in production code
Fix 3 fromtimestamp(0) calls that produced naive, timezone-dependent datetimes: photos.py, autodelete.py, base.py. All now pass tz=utc. Fix 9 tests that hardcoded UTC timezone strings -- now compute expected date paths and timezone offsets dynamically using the same logic as production code (fromtimestamp + astimezone(get_localzone())). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6c00482 commit a305301

File tree

9 files changed

+83
-40
lines changed

9 files changed

+83
-40
lines changed

src/icloudpd/autodelete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def autodelete_photos(
7272
# e.g. ValueError: year=5 is before 1900
7373
# (https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/122)
7474
# Just use the Unix epoch
75-
created_date = datetime.datetime.fromtimestamp(0)
75+
created_date = datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
7676
date_path = folder_structure.format(created_date)
7777

7878
download_dir = os.path.join(directory, date_path)

src/icloudpd/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ def download_builder(
605605
# e.g. ValueError: year=5 is before 1900
606606
# (https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/122)
607607
# Just use the Unix epoch
608-
created_date = datetime.datetime.fromtimestamp(0)
608+
created_date = datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
609609
date_path = folder_structure.format(created_date)
610610

611611
try:

src/pyicloud_ipd/services/photos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ def asset_date(self) -> datetime:
863863
self._asset_record["fields"]["assetDate"]["value"] / 1000.0, tz=pytz.utc
864864
)
865865
except (KeyError, TypeError, ValueError):
866-
dt = datetime.fromtimestamp(0)
866+
dt = datetime.fromtimestamp(0, tz=pytz.utc)
867867
return dt
868868

869869
@property

tests/test_autodelete_photos.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -911,14 +911,17 @@ def test_autodelete_photos_lp(self) -> None:
911911

912912
shutil.copytree(cookie_master_path, cookie_dir)
913913

914-
files_to_create = ["2018/07/30/IMG_7407.JPG", "2018/07/30/IMG_7407-original.JPG"]
914+
files_to_create = [
915+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7407.JPG",
916+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7407-original.JPG",
917+
]
915918

916919
files_to_delete = [
917-
"2018/07/30/IMG_7406.MOV",
918-
"2018/07/26/IMG_7383.PNG",
919-
"2018/07/12/IMG_7190.JPG",
920-
"2018/07/12/IMG_7190-medium.JPG",
921-
"2018/07/12/IMG_7190.MOV", # Live Photo for JPG
920+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7406.MOV",
921+
f"{datetime.datetime.fromtimestamp(1532618424000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7383.PNG",
922+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190.JPG",
923+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190-medium.JPG",
924+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190.MOV", # Live Photo for JPG
922925
]
923926

924927
# create some empty files
@@ -1003,14 +1006,17 @@ def test_autodelete_photos_lp_heic(self) -> None:
10031006

10041007
shutil.copytree(cookie_master_path, cookie_dir)
10051008

1006-
files_to_create = ["2018/07/30/IMG_7407.JPG", "2018/07/30/IMG_7407-original.JPG"]
1009+
files_to_create = [
1010+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7407.JPG",
1011+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7407-original.JPG",
1012+
]
10071013

10081014
files_to_delete = [
1009-
"2018/07/30/IMG_7406.MOV",
1010-
"2018/07/26/IMG_7383.PNG",
1011-
"2018/07/12/IMG_7190.HEIC", # SU1HXzcxOTAuSlBH -> SU1HXzcxOTAuSEVJQw==
1012-
"2018/07/12/IMG_7190-medium.JPG",
1013-
"2018/07/12/IMG_7190_HEVC.MOV", # Live Photo for HEIC
1015+
f"{datetime.datetime.fromtimestamp(1532940539000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7406.MOV",
1016+
f"{datetime.datetime.fromtimestamp(1532618424000.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7383.PNG",
1017+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190.HEIC", # SU1HXzcxOTAuSlBH -> SU1HXzcxOTAuSEVJQw==
1018+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190-medium.JPG",
1019+
f"{datetime.datetime.fromtimestamp(1531371164630.0 / 1000.0, tz=pytz.utc).astimezone(get_localzone()):%Y/%m/%d}/IMG_7190_HEVC.MOV", # Live Photo for HEIC
10141020
]
10151021

10161022
# create some empty files

tests/test_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import inspect
33
import os
44
import shutil
5-
import zoneinfo
65
from argparse import ArgumentError
76
from typing import Sequence, Tuple
87
from unittest import TestCase
98

109
import pytest
10+
from tzlocal import get_localzone
1111

1212
from icloudpd.cli import format_help, parse
1313
from icloudpd.config import GlobalConfig, UserConfig
@@ -348,8 +348,8 @@ def test_cli_parser(self) -> None:
348348
align_raw=RawTreatmentPolicy.AS_IS,
349349
file_match_policy=FileMatchPolicy.NAME_SIZE_DEDUP_WITH_SUFFIX,
350350
skip_created_before=datetime.datetime(
351-
year=2025, month=1, day=2, tzinfo=zoneinfo.ZoneInfo(key="Etc/UTC")
352-
),
351+
year=2025, month=1, day=2
352+
).astimezone(get_localzone()),
353353
skip_created_after=datetime.timedelta(days=2),
354354
skip_photos=False,
355355
),

tests/test_download_photos.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pytz
1414
from piexif._exceptions import InvalidImageDataError
1515
from requests import Response
16+
from tzlocal import get_localzone
1617

1718
from icloudpd import constants
1819
from pyicloud_ipd.asset_version import AssetVersion
@@ -2365,12 +2366,16 @@ def test_download_and_skip_old(self) -> None:
23652366
"Skipping IMG_7404.MOV, only downloading photos.",
23662367
result.output,
23672368
)
2369+
_lz = get_localzone()
2370+
_img7407_created = datetime.datetime.fromtimestamp(1532951045108 / 1000.0, tz=pytz.utc).astimezone(_lz)
2371+
_img7408_created = datetime.datetime.fromtimestamp(1532951050176 / 1000.0, tz=pytz.utc).astimezone(_lz)
2372+
_threshold = datetime.datetime(2018, 7, 31).astimezone(_lz)
23682373
self.assertIn(
2369-
"Skipping IMG_7407.JPG, as it was created 2018-07-30 11:44:05.108000+00:00, before 2018-07-31 00:00:00+00:00.",
2374+
f"Skipping IMG_7407.JPG, as it was created {_img7407_created}, before {_threshold}.",
23702375
result.output,
23712376
)
23722377
self.assertIn(
2373-
"Skipping IMG_7408.JPG, as it was created 2018-07-30 11:44:10.176000+00:00, before 2018-07-31 00:00:00+00:00.",
2378+
f"Skipping IMG_7408.JPG, as it was created {_img7408_created}, before {_threshold}.",
23742379
result.output,
23752380
)
23762381
self.assertIn("All photos have been downloaded", result.output)
@@ -2445,8 +2450,11 @@ def test_download_and_skip_new(self) -> None:
24452450
result.output,
24462451
)
24472452

2453+
_lz = get_localzone()
2454+
_img7409_created = datetime.datetime.fromtimestamp(1533021744816 / 1000.0, tz=pytz.utc).astimezone(_lz)
2455+
_threshold = datetime.datetime(2018, 7, 31).astimezone(_lz)
24482456
self.assertIn(
2449-
"Skipping IMG_7409.JPG, as it was created 2018-07-31 07:22:24.816000+00:00, after 2018-07-31 00:00:00+00:00",
2457+
f"Skipping IMG_7409.JPG, as it was created {_img7409_created}, after {_threshold}.",
24502458
result.output,
24512459
)
24522460
self.assertIn("All photos have been downloaded", result.output)

tests/test_download_photos_id.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
import piexif
1111
import pytest
12+
import pytz
1213
from piexif._exceptions import InvalidImageDataError
1314
from requests import Response
15+
from tzlocal import get_localzone
1416

1517
from icloudpd import constants
1618
from icloudpd.string_helpers import truncate_middle
@@ -2262,12 +2264,16 @@ def test_download_and_skip_old_name_id7(self) -> None:
22622264
"Skipping IMG_7404_QVI5TWx.MOV, only downloading photos.",
22632265
result.output,
22642266
)
2267+
_lz = get_localzone()
2268+
_img7407_created = datetime.datetime.fromtimestamp(1532951045108 / 1000.0, tz=pytz.utc).astimezone(_lz)
2269+
_img7408_created = datetime.datetime.fromtimestamp(1532951050176 / 1000.0, tz=pytz.utc).astimezone(_lz)
2270+
_threshold = datetime.datetime(2018, 7, 31).astimezone(_lz)
22652271
self.assertIn(
2266-
"Skipping IMG_7407_QVovd0F.JPG, as it was created 2018-07-30 11:44:05.108000+00:00, before 2018-07-31 00:00:00+00:00.",
2272+
f"Skipping IMG_7407_QVovd0F.JPG, as it was created {_img7407_created}, before {_threshold}.",
22672273
result.output,
22682274
)
22692275
self.assertIn(
2270-
"Skipping IMG_7408_QVI4T2l.JPG, as it was created 2018-07-30 11:44:10.176000+00:00, before 2018-07-31 00:00:00+00:00.",
2276+
f"Skipping IMG_7408_QVI4T2l.JPG, as it was created {_img7408_created}, before {_threshold}.",
22712277
result.output,
22722278
)
22732279
self.assertIn("All photos have been downloaded", result.output)
@@ -2339,8 +2345,11 @@ def test_download_and_skip_new_name_id7(self) -> None:
23392345
result.output,
23402346
)
23412347

2348+
_lz = get_localzone()
2349+
_img7409_created = datetime.datetime.fromtimestamp(1533021744816 / 1000.0, tz=pytz.utc).astimezone(_lz)
2350+
_threshold = datetime.datetime(2018, 7, 31).astimezone(_lz)
23422351
self.assertIn(
2343-
"Skipping IMG_7409_QVk2Yyt.JPG, as it was created 2018-07-31 07:22:24.816000+00:00, after 2018-07-31 00:00:00+00:00.",
2352+
f"Skipping IMG_7409_QVk2Yyt.JPG, as it was created {_img7409_created}, after {_threshold}.",
23442353
result.output,
23452354
)
23462355
self.assertIn("All photos have been downloaded", result.output)

tests/test_folder_structure.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import datetime
12
import inspect
23
import os
34
import sys
45
from typing import List, Tuple
56
from unittest import TestCase
67

78
import pytest
9+
import pytz
10+
from tzlocal import get_localzone
811

912
from tests.helpers import (
1013
path_from_project_root,
@@ -52,30 +55,37 @@ def test_default_folder_structure(self) -> None:
5255

5356
filenames = result.output.splitlines()
5457

58+
_lz = get_localzone()
59+
_d7409 = datetime.datetime.fromtimestamp(1533021744816 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
60+
_d7408 = datetime.datetime.fromtimestamp(1532951050176 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
61+
_d7407 = datetime.datetime.fromtimestamp(1532951045108 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
62+
_d7405 = datetime.datetime.fromtimestamp(1532950655469 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
63+
_d7404 = datetime.datetime.fromtimestamp(1532950654855 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
64+
5565
self.assertEqual(len(filenames), 8)
5666
self.assertEqual(
57-
os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.JPG")), filenames[0]
67+
os.path.join(data_dir, os.path.normpath(f"{_d7409}/IMG_7409.JPG")), filenames[0]
5868
)
5969
self.assertEqual(
60-
os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.MOV")), filenames[1]
70+
os.path.join(data_dir, os.path.normpath(f"{_d7409}/IMG_7409.MOV")), filenames[1]
6171
)
6272
self.assertEqual(
63-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")), filenames[2]
73+
os.path.join(data_dir, os.path.normpath(f"{_d7408}/IMG_7408.JPG")), filenames[2]
6474
)
6575
self.assertEqual(
66-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")), filenames[3]
76+
os.path.join(data_dir, os.path.normpath(f"{_d7408}/IMG_7408.MOV")), filenames[3]
6777
)
6878
self.assertEqual(
69-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.JPG")), filenames[4]
79+
os.path.join(data_dir, os.path.normpath(f"{_d7407}/IMG_7407.JPG")), filenames[4]
7080
)
7181
self.assertEqual(
72-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.MOV")), filenames[5]
82+
os.path.join(data_dir, os.path.normpath(f"{_d7407}/IMG_7407.MOV")), filenames[5]
7383
)
7484
self.assertEqual(
75-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7405.MOV")), filenames[6]
85+
os.path.join(data_dir, os.path.normpath(f"{_d7405}/IMG_7405.MOV")), filenames[6]
7686
)
7787
self.assertEqual(
78-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7404.MOV")), filenames[7]
88+
os.path.join(data_dir, os.path.normpath(f"{_d7404}/IMG_7404.MOV")), filenames[7]
7989
)
8090

8191
def test_folder_structure_none(self) -> None:

tests/test_listing_recent_photos.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import datetime
12
import inspect
23
import json
34
import os
45
from unittest import TestCase, mock
56

67
import pytest
8+
import pytz
9+
from tzlocal import get_localzone
710

811
from tests.helpers import (
912
path_from_project_root,
@@ -43,30 +46,37 @@ def test_listing_recent_photos(self) -> None:
4346

4447
filenames = result.output.splitlines()
4548

49+
_lz = get_localzone()
50+
_d7409 = datetime.datetime.fromtimestamp(1533021744816 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
51+
_d7408 = datetime.datetime.fromtimestamp(1532951050176 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
52+
_d7407 = datetime.datetime.fromtimestamp(1532951045108 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
53+
_d7405 = datetime.datetime.fromtimestamp(1532950655469 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
54+
_d7404 = datetime.datetime.fromtimestamp(1532950654855 / 1000.0, tz=pytz.utc).astimezone(_lz).strftime("%Y/%m/%d")
55+
4656
self.assertEqual(len(filenames), 8)
4757
self.assertIn(
48-
os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.JPG")), filenames[0]
58+
os.path.join(data_dir, os.path.normpath(f"{_d7409}/IMG_7409.JPG")), filenames[0]
4959
)
5060
self.assertEqual(
51-
os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.MOV")), filenames[1]
61+
os.path.join(data_dir, os.path.normpath(f"{_d7409}/IMG_7409.MOV")), filenames[1]
5262
)
5363
self.assertEqual(
54-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")), filenames[2]
64+
os.path.join(data_dir, os.path.normpath(f"{_d7408}/IMG_7408.JPG")), filenames[2]
5565
)
5666
self.assertEqual(
57-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")), filenames[3]
67+
os.path.join(data_dir, os.path.normpath(f"{_d7408}/IMG_7408.MOV")), filenames[3]
5868
)
5969
self.assertEqual(
60-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.JPG")), filenames[4]
70+
os.path.join(data_dir, os.path.normpath(f"{_d7407}/IMG_7407.JPG")), filenames[4]
6171
)
6272
self.assertEqual(
63-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7407.MOV")), filenames[5]
73+
os.path.join(data_dir, os.path.normpath(f"{_d7407}/IMG_7407.MOV")), filenames[5]
6474
)
6575
self.assertEqual(
66-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7405.MOV")), filenames[6]
76+
os.path.join(data_dir, os.path.normpath(f"{_d7405}/IMG_7405.MOV")), filenames[6]
6777
)
6878
self.assertEqual(
69-
os.path.join(data_dir, os.path.normpath("2018/07/30/IMG_7404.MOV")), filenames[7]
79+
os.path.join(data_dir, os.path.normpath(f"{_d7404}/IMG_7404.MOV")), filenames[7]
7080
)
7181

7282
def test_listing_photos_does_not_create_folders(self) -> None:

0 commit comments

Comments
 (0)