From 603dabb8f420a89e993d5aadcd3a5614bbb262dd Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Tue, 27 Oct 2020 06:54:42 -0700 Subject: [PATCH] Cleaned up as_dict/asdict, issue #144, #188 --- README.md | 15 +++++--- osxphotos/exiftool.py | 4 +-- osxphotos/personinfo.py | 11 +++--- osxphotos/photoinfo/photoinfo.py | 35 +++++++++++++------ .../photosdb/_photosdb_process_comments.py | 7 ++++ osxphotos/placeinfo.py | 4 +-- tests/test_catalina_10_15_6.py | 7 ++-- tests/test_cli.py | 2 +- tests/test_comments.py | 29 +++++++++++++++ tests/test_exiftool.py | 4 +-- tests/test_places_catalina_10_15_1.py | 6 ++-- tests/test_places_mojave_10_14_6.py | 4 +-- 12 files changed, 94 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 44c3bdc1..98153a25 100644 --- a/README.md +++ b/README.md @@ -1294,7 +1294,8 @@ exiftool must be installed in the path for this to work. If exiftool cannot be `ExifTool` provides the following methods: -- `as_dict()`: returns all EXIF metadata found in the file as a dictionary in following form (Note: this shows just a subset of available metadata). See [exiftool](https://exiftool.org/) documentation to understand which metadata keys are available. +- `asdict()`: returns all EXIF metadata found in the file as a dictionary in following form (Note: this shows just a subset of available metadata). See [exiftool](https://exiftool.org/) documentation to understand which metadata keys are available. + ```python {'Composite:Aperture': 2.2, 'Composite:GPSPosition': '-34.9188916666667 138.596861111111', @@ -1307,7 +1308,7 @@ exiftool must be installed in the path for this to work. If exiftool cannot be } ``` -- `json()`: returns same information as `as_dict()` but as a serialized JSON string. +- `json()`: returns same information as `asdict()` but as a serialized JSON string. - `setvalue(tag, value)`: write to the EXIF data in the photo file. To delete a tag, use setvalue with value = `None`. For example: ```python @@ -1318,7 +1319,7 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo") photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach") ``` -**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.as_dict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`. +**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.asdict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`. #### `score` Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the computed aesthetic scores for each photo. @@ -1326,7 +1327,10 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the **Note**: Valid only for Photos 5; returns None for earlier Photos versions. #### `json()` -Returns a JSON representation of all photo info +Returns a JSON representation of all photo info. + +#### `asdict()` +Returns a dictionary representation of all photo info. #### `export()` `export(dest, *filename, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)` @@ -1674,6 +1678,9 @@ Returns a list of [FaceInfo](#faceinfo) objects associated with this person sort #### `json()` Returns a json string representation of the PersonInfo instance. +#### `asdict()` +Returns a dictionary representation of the PersonInfo instance. + ### FaceInfo [PhotoInfo.face_info](#photofaceinfo) return a list of FaceInfo objects representing detected faces in a photo. The FaceInfo class has the following properties and methods. diff --git a/osxphotos/exiftool.py b/osxphotos/exiftool.py index b54ac9d6..b613d4d4 100644 --- a/osxphotos/exiftool.py +++ b/osxphotos/exiftool.py @@ -228,7 +228,7 @@ class ExifTool: ver = self.run_commands("-ver", no_file=True) return ver.decode("utf-8") - def as_dict(self): + def asdict(self): """ return dictionary of all EXIF tags and values from exiftool returns empty dict if no tags """ @@ -245,7 +245,7 @@ class ExifTool: def _read_exif(self): """ read exif data from file """ - data = self.as_dict() + data = self.asdict() self.data = {k: v for k, v in data.items()} def __str__(self): diff --git a/osxphotos/personinfo.py b/osxphotos/personinfo.py index 53263dfa..93267dd9 100644 --- a/osxphotos/personinfo.py +++ b/osxphotos/personinfo.py @@ -66,10 +66,10 @@ class PersonInfo: # no faces return [] - def json(self): - """ Returns JSON representation of class instance """ + def asdict(self): + """ Returns dictionary representation of class instance """ keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None - person = { + return { "uuid": self.uuid, "name": self.name, "displayname": self.display_name, @@ -77,7 +77,10 @@ class PersonInfo: "facecount": self.facecount, "keyphoto": keyphoto, } - return json.dumps(person) + + def json(self): + """ Returns JSON representation of class instance """ + return json.dumps(self.asdict()) def __str__(self): return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})" diff --git a/osxphotos/photoinfo/photoinfo.py b/osxphotos/photoinfo/photoinfo.py index 6f7c7849..5b043b0a 100644 --- a/osxphotos/photoinfo/photoinfo.py +++ b/osxphotos/photoinfo/photoinfo.py @@ -5,6 +5,7 @@ PhotosDB.photos() returns a list of PhotoInfo objects """ import dataclasses +import datetime import json import logging import os @@ -950,22 +951,23 @@ class PhotoInfo: } return yaml.dump(info, sort_keys=False) - def json(self): - """ return JSON representation """ + def asdict(self): + """ return dict representation """ - date_modified_iso = ( - self.date_modified.isoformat() if self.date_modified else None - ) folders = {album.title: album.folder_names for album in self.album_info} exif = dataclasses.asdict(self.exif_info) if self.exif_info else {} - place = self.place.as_dict() if self.place else {} + place = self.place.asdict() if self.place else {} score = dataclasses.asdict(self.score) if self.score else {} + comments = [comment.asdict() for comment in self.comments] + likes = [like.asdict() for like in self.likes] + faces = [face.asdict() for face in self.face_info] - pic = { + return { + "library": self._db._library_path, "uuid": self.uuid, "filename": self.filename, "original_filename": self.original_filename, - "date": self.date.isoformat(), + "date": self.date, "description": self.description, "title": self.title, "keywords": self.keywords, @@ -974,6 +976,7 @@ class PhotoInfo: "albums": self.albums, "folders": folders, "persons": self.persons, + "faces": faces, "path": self.path, "ismissing": self.ismissing, "hasadjustments": self.hasadjustments, @@ -987,12 +990,13 @@ class PhotoInfo: "isphoto": self.isphoto, "ismovie": self.ismovie, "uti": self.uti, + "uti_original": self.uti_original, "burst": self.burst, "live_photo": self.live_photo, "path_live_photo": self.path_live_photo, "iscloudasset": self.iscloudasset, "incloud": self.incloud, - "date_modified": date_modified_iso, + "date_modified": self.date_modified, "portrait": self.portrait, "screenshot": self.screenshot, "slow_mo": self.slow_mo, @@ -1001,6 +1005,8 @@ class PhotoInfo: "selfie": self.selfie, "panorama": self.panorama, "has_raw": self.has_raw, + "israw": self.israw, + "raw_original": self.raw_original, "uti_raw": self.uti_raw, "path_raw": self.path_raw, "place": place, @@ -1014,8 +1020,17 @@ class PhotoInfo: "original_width": self.original_width, "original_orientation": self.original_orientation, "original_filesize": self.original_filesize, + "comments": comments, + "likes": likes, } - return json.dumps(pic) + + def json(self): + """ Return JSON representation """ + def default(o): + if isinstance(o, (datetime.date, datetime.datetime)): + return o.isoformat() + + return json.dumps(self.asdict(), sort_keys=True, default=default) def __eq__(self, other): """ Compare two PhotoInfo objects for equality """ diff --git a/osxphotos/photosdb/_photosdb_process_comments.py b/osxphotos/photosdb/_photosdb_process_comments.py index 9f04f2a3..2840c0f6 100644 --- a/osxphotos/photosdb/_photosdb_process_comments.py +++ b/osxphotos/photosdb/_photosdb_process_comments.py @@ -1,6 +1,7 @@ """ PhotosDB method for processing comments and likes on shared photos. Do not import this module directly """ +import dataclasses import datetime from dataclasses import dataclass @@ -30,6 +31,9 @@ class CommentInfo: ismine: bool text: str + def asdict(self): + return dataclasses.asdict(self) + @dataclass class LikeInfo: @@ -39,6 +43,9 @@ class LikeInfo: user: str ismine: bool + def asdict(self): + return dataclasses.asdict(self) + # The following methods do not get imported into PhotosDB # but will get called by _process_comments diff --git a/osxphotos/placeinfo.py b/osxphotos/placeinfo.py index 18de2a0c..facffc0c 100644 --- a/osxphotos/placeinfo.py +++ b/osxphotos/placeinfo.py @@ -491,7 +491,7 @@ class PlaceInfo4(PlaceInfo): } return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")" - def as_dict(self): + def asdict(self): return { "name": self.name, "names": self.names._asdict(), @@ -634,7 +634,7 @@ class PlaceInfo5(PlaceInfo): } return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")" - def as_dict(self): + def asdict(self): return { "name": self.name, "names": self.names._asdict(), diff --git a/tests/test_catalina_10_15_6.py b/tests/test_catalina_10_15_6.py index 0fbf5565..4a6cfa71 100644 --- a/tests/test_catalina_10_15_6.py +++ b/tests/test_catalina_10_15_6.py @@ -133,6 +133,7 @@ RawInfo = namedtuple( "uti_raw", ], ) + RAW_DICT = { "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": RawInfo( "raw image, no jpeg pair", @@ -181,6 +182,7 @@ RAW_DICT = { def photosdb(): return osxphotos.PhotosDB(dbfile=PHOTOS_DB) + def test_init1(): # test named argument @@ -922,7 +924,6 @@ def test_from_to_date(photosdb): os.environ["TZ"] = "US/Pacific" time.tzset() - photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28)) assert len(photos) == 7 @@ -941,7 +942,6 @@ def test_from_to_date_tz(photosdb): os.environ["TZ"] = "US/Pacific" time.tzset() - photos = photosdb.photos( from_date=datetime.datetime(2018, 9, 28, 13, 7, 0), to_date=datetime.datetime(2018, 9, 28, 13, 9, 0), @@ -978,8 +978,7 @@ def test_date_invalid(): # doesn't run correctly with the module-level fixture from datetime import datetime, timedelta, timezone import osxphotos - - + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]]) assert len(photos) == 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index 8bedb3ad..59030374 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -854,7 +854,7 @@ def test_export_exiftool(): files = glob.glob("*") assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]]) - exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).as_dict() + exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).asdict() for key in CLI_EXIFTOOL[uuid]: assert exif[key] == CLI_EXIFTOOL[uuid][key] diff --git a/tests/test_comments.py b/tests/test_comments.py index ad86baee..b0c5b194 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -53,6 +53,23 @@ LIKE_UUID_DICT = { ], } +COMMENT_UUID_ASDICT = { + "4E4944A0-3E5C-4028-9600-A8709F2FA1DB": { + "datetime": datetime.datetime(2020, 9, 19, 22, 54, 12, 947978), + "user": None, + "ismine": True, + "text": "Nice trophy", + } +} + +LIKE_UUID_ASDICT = { + "65BADBD7-A50C-4956-96BA-1BB61155DA17": { + "datetime": datetime.datetime(2020, 9, 18, 10, 28, 52, 570000), + "user": None, + "ismine": False, + } +} + @pytest.fixture(scope="module") def photosdb(): @@ -69,3 +86,15 @@ def test_likes(photosdb): for uuid in LIKE_UUID_DICT: photo = photosdb.get_photo(uuid) assert photo.likes == LIKE_UUID_DICT[uuid] + + +def test_comments_as_dict(photosdb): + for uuid in COMMENT_UUID_ASDICT: + photo = photosdb.get_photo(uuid) + assert photo.comments[0].asdict() == COMMENT_UUID_ASDICT[uuid] + + +def test_likes_as_dict(photosdb): + for uuid in LIKE_UUID_ASDICT: + photo = photosdb.get_photo(uuid) + assert photo.likes[0].asdict() == LIKE_UUID_ASDICT[uuid] diff --git a/tests/test_exiftool.py b/tests/test_exiftool.py index 9044acf7..ed1d8b15 100644 --- a/tests/test_exiftool.py +++ b/tests/test_exiftool.py @@ -198,7 +198,7 @@ def test_as_dict(): import osxphotos.exiftool exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) - exifdata = exif1.as_dict() + exifdata = exif1.asdict() assert exifdata["XMP:TagsList"] == "wedding" @@ -227,7 +227,7 @@ def test_photoinfo_exiftool(): for uuid in EXIF_UUID: photo = photosdb.photos(uuid=[uuid])[0] exiftool = photo.exiftool - exif_dict = exiftool.as_dict() + exif_dict = exiftool.asdict() for key, val in EXIF_UUID[uuid].items(): assert exif_dict[key] == val diff --git a/tests/test_places_catalina_10_15_1.py b/tests/test_places_catalina_10_15_1.py index 157c8e25..3e6d9d79 100644 --- a/tests/test_places_catalina_10_15_1.py +++ b/tests/test_places_catalina_10_15_1.py @@ -130,15 +130,15 @@ def test_place_no_place_info(): assert photo.place is None -def test_place_place_info_as_dict(): - # test PlaceInfo.as_dict() +def test_place_place_info_asdict(): + # test PlaceInfo.asdict() import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photo = photosdb.photos(uuid=[UUID_DICT["place_maui"]])[0] assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo) - assert photo.place.as_dict() == MAUI_DICT + assert photo.place.asdict() == MAUI_DICT # def test_place_str(): diff --git a/tests/test_places_mojave_10_14_6.py b/tests/test_places_mojave_10_14_6.py index ee980b9a..5e54cce2 100644 --- a/tests/test_places_mojave_10_14_6.py +++ b/tests/test_places_mojave_10_14_6.py @@ -89,11 +89,11 @@ def test_place_str(): def test_place_as_dict(): - # test PlaceInfo.as_dict() + # test PlaceInfo.asdict() import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photo = photosdb.photos(uuid=[UUID_DICT["place_uk"]])[0] assert photo.place is not None assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo) - assert photo.place.as_dict() == UK_DICT + assert photo.place.asdict() == UK_DICT