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:
- `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.

View File

@ -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):

View File

@ -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})"

View File

@ -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 """

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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]

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")
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]

View File

@ -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

View File

@ -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():

View File

@ -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