Cleaned up as_dict/asdict, issue #144, #188

This commit is contained in:
Rhet Turnbull 2020-10-27 06:54:42 -07:00
parent 091f1d9bb4
commit 603dabb8f4
12 changed files with 94 additions and 34 deletions

View File

@ -1294,7 +1294,8 @@ exiftool must be installed in the path for this to work. If exiftool cannot be
`ExifTool` provides the following methods: `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 ```python
{'Composite:Aperture': 2.2, {'Composite:Aperture': 2.2,
'Composite:GPSPosition': '-34.9188916666667 138.596861111111', '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: - `setvalue(tag, value)`: write to the EXIF data in the photo file. To delete a tag, use setvalue with value = `None`. For example:
```python ```python
@ -1318,7 +1319,7 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo")
photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach") 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` #### `score`
Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the computed aesthetic scores for each photo. 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. **Note**: Valid only for Photos 5; returns None for earlier Photos versions.
#### `json()` #### `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()`
`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)` `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()` #### `json()`
Returns a json string representation of the PersonInfo instance. Returns a json string representation of the PersonInfo instance.
#### `asdict()`
Returns a dictionary representation of the PersonInfo instance.
### FaceInfo ### 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. [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.

View File

@ -228,7 +228,7 @@ class ExifTool:
ver = self.run_commands("-ver", no_file=True) ver = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8") return ver.decode("utf-8")
def as_dict(self): def asdict(self):
""" return dictionary of all EXIF tags and values from exiftool """ return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags returns empty dict if no tags
""" """
@ -245,7 +245,7 @@ class ExifTool:
def _read_exif(self): def _read_exif(self):
""" read exif data from file """ """ read exif data from file """
data = self.as_dict() data = self.asdict()
self.data = {k: v for k, v in data.items()} self.data = {k: v for k, v in data.items()}
def __str__(self): def __str__(self):

View File

@ -66,10 +66,10 @@ class PersonInfo:
# no faces # no faces
return [] return []
def json(self): def asdict(self):
""" Returns JSON representation of class instance """ """ Returns dictionary representation of class instance """
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
person = { return {
"uuid": self.uuid, "uuid": self.uuid,
"name": self.name, "name": self.name,
"displayname": self.display_name, "displayname": self.display_name,
@ -77,7 +77,10 @@ class PersonInfo:
"facecount": self.facecount, "facecount": self.facecount,
"keyphoto": keyphoto, "keyphoto": keyphoto,
} }
return json.dumps(person)
def json(self):
""" Returns JSON representation of class instance """
return json.dumps(self.asdict())
def __str__(self): def __str__(self):
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})" return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"

View File

@ -5,6 +5,7 @@ PhotosDB.photos() returns a list of PhotoInfo objects
""" """
import dataclasses import dataclasses
import datetime
import json import json
import logging import logging
import os import os
@ -950,22 +951,23 @@ class PhotoInfo:
} }
return yaml.dump(info, sort_keys=False) return yaml.dump(info, sort_keys=False)
def json(self): def asdict(self):
""" return JSON representation """ """ 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} folders = {album.title: album.folder_names for album in self.album_info}
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {} 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 {} 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, "uuid": self.uuid,
"filename": self.filename, "filename": self.filename,
"original_filename": self.original_filename, "original_filename": self.original_filename,
"date": self.date.isoformat(), "date": self.date,
"description": self.description, "description": self.description,
"title": self.title, "title": self.title,
"keywords": self.keywords, "keywords": self.keywords,
@ -974,6 +976,7 @@ class PhotoInfo:
"albums": self.albums, "albums": self.albums,
"folders": folders, "folders": folders,
"persons": self.persons, "persons": self.persons,
"faces": faces,
"path": self.path, "path": self.path,
"ismissing": self.ismissing, "ismissing": self.ismissing,
"hasadjustments": self.hasadjustments, "hasadjustments": self.hasadjustments,
@ -987,12 +990,13 @@ class PhotoInfo:
"isphoto": self.isphoto, "isphoto": self.isphoto,
"ismovie": self.ismovie, "ismovie": self.ismovie,
"uti": self.uti, "uti": self.uti,
"uti_original": self.uti_original,
"burst": self.burst, "burst": self.burst,
"live_photo": self.live_photo, "live_photo": self.live_photo,
"path_live_photo": self.path_live_photo, "path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset, "iscloudasset": self.iscloudasset,
"incloud": self.incloud, "incloud": self.incloud,
"date_modified": date_modified_iso, "date_modified": self.date_modified,
"portrait": self.portrait, "portrait": self.portrait,
"screenshot": self.screenshot, "screenshot": self.screenshot,
"slow_mo": self.slow_mo, "slow_mo": self.slow_mo,
@ -1001,6 +1005,8 @@ class PhotoInfo:
"selfie": self.selfie, "selfie": self.selfie,
"panorama": self.panorama, "panorama": self.panorama,
"has_raw": self.has_raw, "has_raw": self.has_raw,
"israw": self.israw,
"raw_original": self.raw_original,
"uti_raw": self.uti_raw, "uti_raw": self.uti_raw,
"path_raw": self.path_raw, "path_raw": self.path_raw,
"place": place, "place": place,
@ -1014,8 +1020,17 @@ class PhotoInfo:
"original_width": self.original_width, "original_width": self.original_width,
"original_orientation": self.original_orientation, "original_orientation": self.original_orientation,
"original_filesize": self.original_filesize, "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): def __eq__(self, other):
""" Compare two PhotoInfo objects for equality """ """ Compare two PhotoInfo objects for equality """

View File

@ -1,6 +1,7 @@
""" PhotosDB method for processing comments and likes on shared photos. """ PhotosDB method for processing comments and likes on shared photos.
Do not import this module directly """ Do not import this module directly """
import dataclasses
import datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass
@ -30,6 +31,9 @@ class CommentInfo:
ismine: bool ismine: bool
text: str text: str
def asdict(self):
return dataclasses.asdict(self)
@dataclass @dataclass
class LikeInfo: class LikeInfo:
@ -39,6 +43,9 @@ class LikeInfo:
user: str user: str
ismine: bool ismine: bool
def asdict(self):
return dataclasses.asdict(self)
# The following methods do not get imported into PhotosDB # The following methods do not get imported into PhotosDB
# but will get called by _process_comments # but will get called by _process_comments

View File

@ -491,7 +491,7 @@ class PlaceInfo4(PlaceInfo):
} }
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")" return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self): def asdict(self):
return { return {
"name": self.name, "name": self.name,
"names": self.names._asdict(), "names": self.names._asdict(),
@ -634,7 +634,7 @@ class PlaceInfo5(PlaceInfo):
} }
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")" return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self): def asdict(self):
return { return {
"name": self.name, "name": self.name,
"names": self.names._asdict(), "names": self.names._asdict(),

View File

@ -133,6 +133,7 @@ RawInfo = namedtuple(
"uti_raw", "uti_raw",
], ],
) )
RAW_DICT = { RAW_DICT = {
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": RawInfo( "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": RawInfo(
"raw image, no jpeg pair", "raw image, no jpeg pair",
@ -181,6 +182,7 @@ RAW_DICT = {
def photosdb(): def photosdb():
return osxphotos.PhotosDB(dbfile=PHOTOS_DB) return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
def test_init1(): def test_init1():
# test named argument # test named argument
@ -922,7 +924,6 @@ def test_from_to_date(photosdb):
os.environ["TZ"] = "US/Pacific" os.environ["TZ"] = "US/Pacific"
time.tzset() time.tzset()
photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28)) photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28))
assert len(photos) == 7 assert len(photos) == 7
@ -941,7 +942,6 @@ def test_from_to_date_tz(photosdb):
os.environ["TZ"] = "US/Pacific" os.environ["TZ"] = "US/Pacific"
time.tzset() time.tzset()
photos = photosdb.photos( photos = photosdb.photos(
from_date=datetime.datetime(2018, 9, 28, 13, 7, 0), from_date=datetime.datetime(2018, 9, 28, 13, 7, 0),
to_date=datetime.datetime(2018, 9, 28, 13, 9, 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 # doesn't run correctly with the module-level fixture
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import osxphotos import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]]) photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
assert len(photos) == 1 assert len(photos) == 1

View File

@ -854,7 +854,7 @@ def test_export_exiftool():
files = glob.glob("*") files = glob.glob("*")
assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]]) 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]: for key in CLI_EXIFTOOL[uuid]:
assert exif[key] == CLI_EXIFTOOL[uuid][key] assert exif[key] == CLI_EXIFTOOL[uuid][key]

View File

@ -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") @pytest.fixture(scope="module")
def photosdb(): def photosdb():
@ -69,3 +86,15 @@ def test_likes(photosdb):
for uuid in LIKE_UUID_DICT: for uuid in LIKE_UUID_DICT:
photo = photosdb.get_photo(uuid) photo = photosdb.get_photo(uuid)
assert photo.likes == LIKE_UUID_DICT[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]

View File

@ -198,7 +198,7 @@ def test_as_dict():
import osxphotos.exiftool import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
exifdata = exif1.as_dict() exifdata = exif1.asdict()
assert exifdata["XMP:TagsList"] == "wedding" assert exifdata["XMP:TagsList"] == "wedding"
@ -227,7 +227,7 @@ def test_photoinfo_exiftool():
for uuid in EXIF_UUID: for uuid in EXIF_UUID:
photo = photosdb.photos(uuid=[uuid])[0] photo = photosdb.photos(uuid=[uuid])[0]
exiftool = photo.exiftool exiftool = photo.exiftool
exif_dict = exiftool.as_dict() exif_dict = exiftool.asdict()
for key, val in EXIF_UUID[uuid].items(): for key, val in EXIF_UUID[uuid].items():
assert exif_dict[key] == val assert exif_dict[key] == val

View File

@ -130,15 +130,15 @@ def test_place_no_place_info():
assert photo.place is None assert photo.place is None
def test_place_place_info_as_dict(): def test_place_place_info_asdict():
# test PlaceInfo.as_dict() # test PlaceInfo.asdict()
import osxphotos import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photo = photosdb.photos(uuid=[UUID_DICT["place_maui"]])[0] photo = photosdb.photos(uuid=[UUID_DICT["place_maui"]])[0]
assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo) assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo)
assert photo.place.as_dict() == MAUI_DICT assert photo.place.asdict() == MAUI_DICT
# def test_place_str(): # def test_place_str():

View File

@ -89,11 +89,11 @@ def test_place_str():
def test_place_as_dict(): def test_place_as_dict():
# test PlaceInfo.as_dict() # test PlaceInfo.asdict()
import osxphotos import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photo = photosdb.photos(uuid=[UUID_DICT["place_uk"]])[0] photo = photosdb.photos(uuid=[UUID_DICT["place_uk"]])[0]
assert photo.place is not None assert photo.place is not None
assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo) assert isinstance(photo.place, osxphotos.placeinfo.PlaceInfo)
assert photo.place.as_dict() == UK_DICT assert photo.place.asdict() == UK_DICT