Skip to content

Commit

Permalink
Added photoinfo_from_dict
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed May 5, 2024
1 parent e1f1369 commit e8bd352
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -18,3 +18,4 @@ docsrc/_build/
venv/
.python-version
cov.xml
pyrightconfig.json
5 changes: 3 additions & 2 deletions osxphotos/photo_signature.py
Expand Up @@ -6,7 +6,8 @@
import os

from .photoinfo import PhotoInfo
from .photoinfo_file import PhotoInfoFromDict, PhotoInfoFromFile
from .photoinfo_dict import PhotoInfoFromDict, photoinfo_from_dict
from .photoinfo_file import PhotoInfoFromFile
from .platform import is_macos

if is_macos:
Expand All @@ -19,7 +20,7 @@ def photo_signature(
) -> str:
"""Compute photo signature for a PhotoInfo, a PhotoInfo dict, or file path"""
if isinstance(photo, dict):
photo = PhotoInfoFromDict(photo)
photo = photoinfo_from_dict(photo)
elif not isinstance(photo, PhotoInfo):
photo = PhotoInfoFromFile(photo, exiftool=exiftool)

Expand Down
38 changes: 38 additions & 0 deletions osxphotos/photoinfo_dict.py
@@ -0,0 +1,38 @@
"""Create a PhotoInfo compatible object from a PhotoInfo dictionary created with PhotoInfo.asdict()"""

from __future__ import annotations

import json
from typing import Any

from .exiftool import ExifToolCaching, get_exiftool_path
from .rehydrate import rehydrate_class

try:
EXIFTOOL_PATH = get_exiftool_path()
except FileNotFoundError:
EXIFTOOL_PATH = None

__all__ = ["PhotoInfoFromDict", "photoinfo_from_dict"]


class PhotoInfoFromDict:
"""Create a PhotoInfo compatible object from a PhotoInfo dictionary created with PhotoInfo.asdict() or deserialized from JSON"""

def asdict(self) -> dict[str, Any]:
"""Return the PhotoInfo dictionary"""
return self._data

def json(self) -> str:
"""Return the PhotoInfo dictionary as a JSON string"""
return json.dumps(self._data)


def photoinfo_from_dict(
data: dict[str, Any], exiftool: str | None = None
) -> PhotoInfoFromDict:
"""Create a PhotoInfoFromDict object from a dictionary"""
photoinfo = rehydrate_class(data, PhotoInfoFromDict)
photoinfo._exiftool_path = exiftool or EXIFTOOL_PATH
photoinfo._data = data
return photoinfo
20 changes: 1 addition & 19 deletions osxphotos/photoinfo_file.py
Expand Up @@ -27,7 +27,7 @@

logger = logging.getLogger("osxphotos")

__all__ = ["PhotoInfoFromDict", "PhotoInfoFromFile"]
__all__ = ["PhotoInfoFromFile"]


class PhotoInfoFromFile:
Expand Down Expand Up @@ -192,24 +192,6 @@ def __getattr__(self, name):
raise AttributeError()


class PhotoInfoFromDict:
"""Rehydrate a PhotoInfo class from a dict"""

def __init__(
self,
data: dict,
exiftool: str | None = None,
):
self._data = data
self._exiftool_path = exiftool or EXIFTOOL_PATH

def __getattr__(self, name):
"""Return dict value or None for non-private attribute"""
if not name.startswith("_"):
return self._data.get(name)
raise AttributeError()


def render_photo_template(
filepath: pathlib.Path,
relative_filepath: pathlib.Path | None,
Expand Down
55 changes: 55 additions & 0 deletions osxphotos/rehydrate.py
@@ -0,0 +1,55 @@
"""Rehydrate a class from a dictionary"""

from __future__ import annotations

import datetime
from typing import Any, Type


def rehydrate_class(data: dict[Any, Any], cls: Type) -> object:
"""Rehydrate a class that's been deserialized from JSON created from asdict()
Args:
data: dictionary of class data; datetimes should be in ISO formatted strings
cls: class to rehydrate into
Returns:
Rehydrated class instance
Note:
This function is not a complete solution for all classes, but it's a good starting point.
It doesn't handle all edge cases, such as classes with required arguments in __init__.
The only special data types are datetimes, which are parsed from ISO formatted strings.
Lists of dictionaries and nested dictionary are also supported.
"""
if isinstance(data, list):
# If the data is a list, create a list of rehydrated class instances
return [rehydrate_class(item, cls) for item in data]

instance = cls()

for key, value in data.items():
if isinstance(value, dict):
# If the value is a dictionary, create a new class instance recursively
setattr(instance, key, rehydrate_class(value, type(key, (object,), {})))
elif isinstance(value, list):
# If the value is a list, check if it contains dictionaries
if all(isinstance(item, dict) for item in value):
# If all items in the list are dictionaries, create a list of rehydrated class instances
setattr(
instance,
key,
[rehydrate_class(item, type(key, (object,), {})) for item in value],
)
else:
setattr(instance, key, value)
elif "date" in key.lower() and value is not None:
# If the key contains "date" and the value is not None, try to parse it as a datetime
try:
setattr(instance, key, datetime.datetime.fromisoformat(value))
except (ValueError, TypeError):
setattr(instance, key, value)
else:
setattr(instance, key, value)

return instance
22 changes: 22 additions & 0 deletions tests/test_photoinfo_dict.py
@@ -0,0 +1,22 @@
"""Test PhotoInfoFromDict class"""

from __future__ import annotations

import pytest

from osxphotos.photoinfo_dict import PhotoInfoFromDict, photoinfo_from_dict
from osxphotos.photosdb import PhotosDB

PHOTOSDB = "tests/Test-13.0.0.photoslibrary"


def test_rehydrate_dict():
"""Test rehydrating a dictionary"""
photosdb = PhotosDB(dbfile=PHOTOSDB)
photo = [p for p in photosdb.photos() if p.original_filename == "wedding.jpg"][0]
photo_dict = photo.asdict()
photo2 = photoinfo_from_dict(photo_dict)
assert isinstance(photo2, PhotoInfoFromDict)
assert photo.uuid == photo2.uuid
photo2_dict = photo2.asdict()
assert photo_dict == photo2_dict

0 comments on commit e8bd352

Please sign in to comment.