Unicode refactor (#1101)

* Began refactoring for improving unicode handling

* Added platform and unicode modules

* Added tests for unicode utilities

* Added tests for unicode utilities

* Added tests for unicode utilities

* Added tests for unicode utilities

* Fixed unicode tests for linux

* Fixed unicode tests for linux

* Fixed duplicate alubm name with --add-to-album

* Fixed test for linux

* Fix for duplicate unicode kewyords, see #907, #1085
This commit is contained in:
Rhet Turnbull 2023-06-24 10:50:10 -07:00 committed by GitHub
parent 7ccfe26e37
commit bb8e164f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 487 additions and 151 deletions

View File

@ -7,7 +7,7 @@ from typing import Callable
from osxphotos import ExportResults, PhotoInfo from osxphotos import ExportResults, PhotoInfo
from osxphotos.exiftool import ExifTool from osxphotos.exiftool import ExifTool
from osxphotos.utils import normalize_unicode from osxphotos.unicode import normalize_unicode
# Update this for your custom keyword to rating mapping # Update this for your custom keyword to rating mapping
RATINGS = { RATINGS = {

View File

@ -21,10 +21,10 @@ from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototables import PhotoTables from .phototables import PhotoTables
from .phototemplate import PhotoTemplate from .phototemplate import PhotoTemplate
from .placeinfo import PlaceInfo from .placeinfo import PlaceInfo
from .platform import is_macos
from .queryoptions import QueryOptions from .queryoptions import QueryOptions
from .scoreinfo import ScoreInfo from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo from .searchinfo import SearchInfo
from .utils import is_macos
if is_macos: if is_macos:
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript

View File

@ -18,9 +18,6 @@ OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
# Apple Epoch is Jan 1, 2001 # Apple Epoch is Jan 1, 2001
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds() TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
# Unicode format to use for comparing strings
UNICODE_FORMAT = "NFC"
# which Photos library database versions have been tested # which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622 # Photos 2.0 (10.12.6) == 2622
# Photos 3.0 (10.13.6) == 3301 # Photos 3.0 (10.13.6) == 3301

View File

@ -13,7 +13,7 @@ from osxphotos.debug import (
set_debug, set_debug,
wrap_function, wrap_function,
) )
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
# apply any debug functions # apply any debug functions
# need to do this before importing anything else so that the debug functions # need to do this before importing anything else so that the debug functions

View File

@ -7,8 +7,9 @@ import datetime
import click import click
import osxphotos import osxphotos
from osxphotos.platform import assert_macos
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
from osxphotos.utils import assert_macos, pluralize from osxphotos.utils import pluralize
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
from .click_rich_echo import rich_click_echo as echo from .click_rich_echo import rich_click_echo as echo

View File

@ -12,8 +12,8 @@ import click
import osxphotos import osxphotos
from osxphotos.phototemplate import RenderOptions from osxphotos.phototemplate import RenderOptions
from osxphotos.platform import assert_macos
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import assert_macos
assert_macos() assert_macos()

View File

@ -9,7 +9,7 @@ import click
from osxphotos._constants import PROFILE_SORT_KEYS from osxphotos._constants import PROFILE_SORT_KEYS
from osxphotos._version import __version__ from osxphotos._version import __version__
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
from .about import about from .about import about
from .albums import albums from .albums import albums

View File

@ -10,7 +10,7 @@ from typing import Any, Callable
import click import click
from ..utils import is_macos from ..platform import is_macos
from .common import OSXPHOTOS_HIDDEN, print_version from .common import OSXPHOTOS_HIDDEN, print_version
from .param_types import * from .param_types import *

View File

@ -15,7 +15,8 @@ from xdg import xdg_config_home, xdg_data_home
import osxphotos import osxphotos
from osxphotos._constants import APP_NAME from osxphotos._constants import APP_NAME
from osxphotos._version import __version__ from osxphotos._version import __version__
from osxphotos.utils import get_latest_version, get_macos_version from osxphotos.platform import get_macos_version
from osxphotos.utils import get_latest_version
# used to show/hide hidden commands # used to show/hide hidden commands
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False)) OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))

View File

@ -1,6 +1,6 @@
"""Detect dark mode on MacOS >= 10.14 or fake it elsewhere""" """Detect dark mode on MacOS >= 10.14 or fake it elsewhere"""
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import Foundation import Foundation

View File

@ -45,16 +45,11 @@ from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
from osxphotos.photoinfo import PhotoInfoNone from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate, RenderOptions from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.platform import get_macos_version, is_macos
from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs
from osxphotos.unicode import normalize_fs_path
from osxphotos.uti import get_preferred_uti_extension from osxphotos.uti import get_preferred_uti_extension
from osxphotos.utils import ( from osxphotos.utils import format_sec_to_hhmmss, pluralize, under_test
format_sec_to_hhmmss,
get_macos_version,
is_macos,
normalize_fs_path,
pluralize,
under_test,
)
if is_macos: if is_macos:
from osxmetadata import ( from osxmetadata import (

View File

@ -20,7 +20,7 @@ from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS_PATHLIB, TEMPLATE_SUBSTITUTIONS_PATHLIB,
get_template_help, get_template_help,
) )
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES

View File

@ -41,8 +41,10 @@ from osxphotos.exiftool import ExifToolCaching, get_exiftool_path
from osxphotos.photoinfo import PhotoInfoNone from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photosalbum import PhotosAlbumPhotoScript from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototemplate import PhotoTemplate, RenderOptions from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.platform import assert_macos
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import assert_macos, pluralize from osxphotos.unicode import normalize_unicode
from osxphotos.utils import pluralize
assert_macos() assert_macos()
@ -357,11 +359,12 @@ def set_photo_metadata(
merge_keywords: bool, merge_keywords: bool,
) -> MetaData: ) -> MetaData:
"""Set metadata (title, description, keywords) for a Photo object""" """Set metadata (title, description, keywords) for a Photo object"""
photo.title = metadata.title photo.title = normalize_unicode(metadata.title)
photo.description = metadata.description photo.description = normalize_unicode(metadata.description)
keywords = metadata.keywords.copy() keywords = metadata.keywords.copy()
keywords =normalize_unicode(keywords)
if merge_keywords: if merge_keywords:
if old_keywords := photo.keywords: if old_keywords := normalize_unicode(photo.keywords):
keywords.extend(old_keywords) keywords.extend(old_keywords)
keywords = list(set(keywords)) keywords = list(set(keywords))
photo.keywords = keywords photo.keywords = keywords
@ -420,7 +423,7 @@ def set_photo_title(
verbose( verbose(
f"Setting title of photo [filename]{filepath.name}[/] to '{title_text[0]}'" f"Setting title of photo [filename]{filepath.name}[/] to '{title_text[0]}'"
) )
photo.title = title_text[0] photo.title = normalize_unicode(title_text[0])
return title_text[0] return title_text[0]
else: else:
return "" return ""
@ -448,7 +451,7 @@ def set_photo_description(
verbose( verbose(
f"Setting description of photo [filename]{filepath.name}[/] to '{description_text[0]}'" f"Setting description of photo [filename]{filepath.name}[/] to '{description_text[0]}'"
) )
photo.description = description_text[0] photo.description = normalize_unicode(description_text[0])
return description_text[0] return description_text[0]
else: else:
return "" return ""
@ -469,8 +472,9 @@ def set_photo_keywords(
kw = render_photo_template(filepath, relative_filepath, keyword, exiftool_path) kw = render_photo_template(filepath, relative_filepath, keyword, exiftool_path)
keywords.extend(kw) keywords.extend(kw)
if keywords: if keywords:
keywords = normalize_unicode(keywords)
if merge: if merge:
if old_keywords := photo.keywords: if old_keywords := normalize_unicode(photo.keywords):
keywords.extend(old_keywords) keywords.extend(old_keywords)
keywords = list(set(keywords)) keywords = list(set(keywords))
verbose(f"Setting keywords of photo [filename]{filepath.name}[/] to {keywords}") verbose(f"Setting keywords of photo [filename]{filepath.name}[/] to {keywords}")

View File

@ -20,8 +20,9 @@ from rich.panel import Panel
from osxphotos import PhotoInfo, PhotosDB from osxphotos import PhotoInfo, PhotosDB
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
from osxphotos.platform import assert_macos
from osxphotos.rich_utils import add_rich_markup_tag from osxphotos.rich_utils import add_rich_markup_tag
from osxphotos.utils import assert_macos, dd_to_dms_str from osxphotos.utils import dd_to_dms_str
assert_macos() assert_macos()

View File

@ -12,8 +12,8 @@ from osxphotos.cli.click_rich_echo import (
) )
from osxphotos.debug import set_debug from osxphotos.debug import set_debug
from osxphotos.phototemplate import RenderOptions from osxphotos.phototemplate import RenderOptions
from osxphotos.platform import assert_macos, is_macos
from osxphotos.queryoptions import query_options_from_kwargs from osxphotos.queryoptions import query_options_from_kwargs
from osxphotos.utils import assert_macos, is_macos
if is_macos: if is_macos:
from osxphotos.photosalbum import PhotosAlbum from osxphotos.photosalbum import PhotosAlbum

View File

@ -20,13 +20,13 @@ from osxphotos._constants import _PHOTOS_4_VERSION
from osxphotos.cli.click_rich_echo import rich_echo_error as echo_error from osxphotos.cli.click_rich_echo import rich_echo_error as echo_error
from osxphotos.photoinfo import PhotoInfo from osxphotos.photoinfo import PhotoInfo
from osxphotos.photosdb import PhotosDB from osxphotos.photosdb import PhotosDB
from osxphotos.platform import assert_macos, is_macos
from osxphotos.pyrepl import embed_repl from osxphotos.pyrepl import embed_repl
from osxphotos.queryoptions import ( from osxphotos.queryoptions import (
IncompatibleQueryOptions, IncompatibleQueryOptions,
QueryOptions, QueryOptions,
query_options_from_kwargs, query_options_from_kwargs,
) )
from osxphotos.utils import assert_macos, is_macos
if is_macos: if is_macos:
import photoscript import photoscript

View File

@ -8,7 +8,8 @@ import click
from osxphotos._constants import UUID_PATTERN from osxphotos._constants import UUID_PATTERN
from osxphotos.export_db_utils import get_uuid_for_filepath from osxphotos.export_db_utils import get_uuid_for_filepath
from osxphotos.photosdb.photosdb_utils import get_photos_library_version from osxphotos.photosdb.photosdb_utils import get_photos_library_version
from osxphotos.utils import assert_macos, get_last_library_path from osxphotos.platform import assert_macos
from osxphotos.utils import get_last_library_path
assert_macos() assert_macos()

View File

@ -15,13 +15,14 @@ from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photosalbum import PhotosAlbum from osxphotos.photosalbum import PhotosAlbum
from osxphotos.photosdb.photosdb_utils import get_db_version from osxphotos.photosdb.photosdb_utils import get_db_version
from osxphotos.phototemplate import PhotoTemplate, RenderOptions from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.platform import assert_macos
from osxphotos.queryoptions import ( from osxphotos.queryoptions import (
IncompatibleQueryOptions, IncompatibleQueryOptions,
QueryOptions, QueryOptions,
query_options_from_kwargs, query_options_from_kwargs,
) )
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import assert_macos, pluralize from osxphotos.utils import pluralize
assert_macos() assert_macos()

View File

@ -25,7 +25,8 @@ from osxphotos.photodates import (
update_photo_time_for_new_timezone, update_photo_time_for_new_timezone,
) )
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from osxphotos.utils import assert_macos, noop, pluralize from osxphotos.platform import assert_macos
from osxphotos.utils import noop, pluralize
assert_macos() assert_macos()

View File

@ -2,7 +2,7 @@
import click import click
from osxphotos.utils import assert_macos from osxphotos.platform import assert_macos
assert_macos() assert_macos()

View File

@ -7,7 +7,8 @@ from osxphotos import PhotosDB
from osxphotos.exiftool import ExifTool from osxphotos.exiftool import ExifTool
from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz
from .utils import assert_macos, noop from .platform import assert_macos
from .utils import noop
assert_macos() assert_macos()

View File

@ -27,7 +27,7 @@ import osxphotos
from ._constants import OSXPHOTOS_EXPORT_DB, SQLITE_CHECK_SAME_THREAD from ._constants import OSXPHOTOS_EXPORT_DB, SQLITE_CHECK_SAME_THREAD
from ._version import __version__ from ._version import __version__
from .fileutil import FileUtil from .fileutil import FileUtil
from .utils import normalize_fs_path from .unicode import normalize_fs_path
__all__ = [ __all__ = [
"ExportDB", "ExportDB",

View File

@ -10,7 +10,8 @@ from abc import ABC, abstractmethod
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from .imageconverter import ImageConverter from .imageconverter import ImageConverter
from .utils import is_macos, normalize_fs_path from .platform import is_macos
from .unicode import normalize_fs_path
if is_macos: if is_macos:
import Foundation import Foundation

View File

@ -10,7 +10,7 @@ import sys
# needed to capture system-level stderr # needed to capture system-level stderr
from wurlitzer import pipes from wurlitzer import pipes
from .utils import is_macos from .platform import is_macos
if is_macos: if is_macos:
import Metal import Metal

View File

@ -13,7 +13,7 @@ filename is needed.
import pathvalidate import pathvalidate
from osxphotos.utils import normalize_unicode from osxphotos.unicode import normalize_unicode
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN

View File

@ -36,17 +36,17 @@ from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftoo
from .export_db import ExportDB, ExportDBTemp from .export_db import ExportDB, ExportDBTemp
from .fileutil import FileUtil from .fileutil import FileUtil
from .phototemplate import RenderOptions from .phototemplate import RenderOptions
from .platform import is_macos
from .rich_utils import add_rich_markup_tag from .rich_utils import add_rich_markup_tag
from .unicode import normalize_fs_path
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import ( from .utils import (
hexdigest, hexdigest,
increment_filename, increment_filename,
increment_filename_with_count, increment_filename_with_count,
is_macos,
lineno, lineno,
list_directory, list_directory,
lock_filename, lock_filename,
normalize_fs_path,
unlock_filename, unlock_filename,
) )

View File

@ -61,11 +61,12 @@ from .photoexporter import ExportOptions, PhotoExporter
from .phototables import PhotoTables from .phototables import PhotoTables
from .phototemplate import PhotoTemplate, RenderOptions from .phototemplate import PhotoTemplate, RenderOptions
from .placeinfo import PlaceInfo4, PlaceInfo5 from .placeinfo import PlaceInfo4, PlaceInfo5
from .platform import assert_macos, is_macos
from .query_builder import get_query from .query_builder import get_query
from .scoreinfo import ScoreInfo from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo from .searchinfo import SearchInfo
from .uti import get_preferred_uti_extension, get_uti_for_extension from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, assert_macos, hexdigest, is_macos, list_directory from .utils import _get_resource_loc, hexdigest, list_directory
if is_macos: if is_macos:
from osxmetadata import OSXMetaData from osxmetadata import OSXMetaData

View File

@ -23,6 +23,8 @@ import sys
import threading import threading
import time import time
from .platform import get_macos_version
assert sys.platform == "darwin" assert sys.platform == "darwin"
import AVFoundation import AVFoundation
@ -37,7 +39,7 @@ from wurlitzer import pipes
from .fileutil import FileUtil from .fileutil import FileUtil
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import get_macos_version, increment_filename from .utils import increment_filename
__all__ = [ __all__ = [
"NSURL_to_path", "NSURL_to_path",

View File

@ -1,11 +1,15 @@
""" PhotosAlbum class to create an album in default Photos library and add photos to it """ """ PhotosAlbum class to create an album in default Photos library and add photos to it """
from __future__ import annotations
import unicodedata
from typing import List, Optional from typing import List, Optional
from more_itertools import chunked from more_itertools import chunked
from .photoinfo import PhotoInfo from .photoinfo import PhotoInfo
from .utils import assert_macos, noop, pluralize from .platform import assert_macos
from .utils import noop, pluralize
assert_macos() assert_macos()
@ -15,19 +19,36 @@ from photoscript import Album, Folder, Photo, PhotosLibrary
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"] __all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]
def get_unicode_variants(s: str) -> list[str]:
"""Get all unicode variants of string"""
variants = []
for form in ["NFC", "NFD", "NFKC", "NFKD"]:
normalized = unicodedata.normalize(form, s)
variants.append(normalized)
return variants
def folder_by_path(folders: List[str], verbose: Optional[callable] = None) -> Folder: def folder_by_path(folders: List[str], verbose: Optional[callable] = None) -> Folder:
"""Get (and create if necessary) a Photos Folder by path (passed as list of folder names)""" """Get (and create if necessary) a Photos Folder by path (passed as list of folder names)"""
library = PhotosLibrary() library = PhotosLibrary()
verbose = verbose or noop verbose = verbose or noop
top_folder_name = folders.pop(0) top_folder_name = folders.pop(0)
top_folder = library.folder(top_folder_name, top_level=True)
if not top_folder: for folder_variant in get_unicode_variants(top_folder_name):
top_folder = library.folder(folder_variant, top_level=True)
if top_folder is not None:
break
else:
verbose(f"Creating folder '{top_folder_name}'") verbose(f"Creating folder '{top_folder_name}'")
top_folder = library.create_folder(top_folder_name) top_folder = library.create_folder(top_folder_name)
current_folder = top_folder current_folder = top_folder
for folder_name in folders: for folder_name in folders:
folder = current_folder.folder(folder_name) for folder_variant in get_unicode_variants(folder_name):
if not folder: folder = current_folder.folder(folder_variant)
if folder is not None:
break
else:
verbose(f"Creating folder '{folder_name}'") verbose(f"Creating folder '{folder_name}'")
folder = current_folder.create_folder(folder_name) folder = current_folder.create_folder(folder_name)
current_folder = folder current_folder = folder
@ -44,15 +65,24 @@ def album_by_path(
# have folders # have folders
album_name = folders_album.pop() album_name = folders_album.pop()
folder = folder_by_path(folders_album, verbose) folder = folder_by_path(folders_album, verbose)
album = folder.album(album_name) for album_variant in get_unicode_variants(album_name):
if album is None: # Get album if it exists
# need to check every unicode variant to avoid creating duplicate albums with same visual representation (#1085)
album = folder.album(album_variant)
if album is not None:
break
else:
verbose(f"Creating album '{album_name}'") verbose(f"Creating album '{album_name}'")
album = folder.create_album(album_name) album = folder.create_album(album_name)
else: else:
# only have album name # only have album name
album_name = folders_album[0] album_name = folders_album[0]
album = library.album(album_name, top_level=True) for album_variant in get_unicode_variants(album_name):
if album is None: album = library.album(album_variant, top_level=True)
if album is not None:
break
else:
# album doesn't exist, create it
verbose(f"Creating album '{album_name}'") verbose(f"Creating album '{album_name}'")
album = library.create_album(album_name) album = library.create_album(album_name)
@ -101,15 +131,10 @@ class PhotosAlbum:
try: try:
photos.append(photoscript.Photo(p.uuid)) photos.append(photoscript.Photo(p.uuid))
except Exception as e: except Exception as e:
print(
f"Error creating Photo object for photo {self._format_uuid(p.uuid)}: {e}"
)
self.verbose( self.verbose(
f"Error creating Photo object for photo {self._format_uuid(p.uuid)}: {e}" f"Error creating Photo object for photo {self._format_uuid(p.uuid)}: {e}"
) )
print(f"photos: {photos}")
for photolist in chunked(photos, 10): for photolist in chunked(photos, 10):
print(f"photolist: {photolist}")
self.album.add(photolist) self.album.add(photolist)
photo_len = len(photo_list) photo_len = len(photo_list)
self.verbose( self.verbose(

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import sqlite3 import sqlite3
from .utils import assert_macos from .platform import assert_macos
assert_macos() assert_macos()

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION, TIME_DELTA from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION, TIME_DELTA
from ..sqlite_utils import sqlite_open_ro from ..sqlite_utils import sqlite_open_ro
from ..utils import normalize_unicode from ..unicode import normalize_unicode
def _process_comments(self): def _process_comments(self):

View File

@ -4,7 +4,7 @@
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..sqlite_utils import sqlite_open_ro from ..sqlite_utils import sqlite_open_ro
from ..utils import normalize_unicode from ..unicode import normalize_unicode
from .photosdb_utils import get_db_version from .photosdb_utils import get_db_version
""" """

View File

@ -10,7 +10,7 @@ from functools import lru_cache
from .._constants import _PHOTOS_4_VERSION, search_category_factory from .._constants import _PHOTOS_4_VERSION, search_category_factory
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import normalize_unicode from ..unicode import normalize_unicode
""" """
This module should be imported in the class defintion of PhotosDB in photosdb.py This module should be imported in the class defintion of PhotosDB in photosdb.py

View File

@ -56,17 +56,12 @@ from ..fileutil import FileUtil
from ..personinfo import PersonInfo from ..personinfo import PersonInfo
from ..photoinfo import PhotoInfo from ..photoinfo import PhotoInfo
from ..phototemplate import RenderOptions from ..phototemplate import RenderOptions
from ..platform import get_macos_version, is_macos
from ..queryoptions import QueryOptions from ..queryoptions import QueryOptions
from ..rich_utils import add_rich_markup_tag from ..rich_utils import add_rich_markup_tag
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import ( from ..unicode import normalize_unicode
_check_file_exists, from ..utils import _check_file_exists, get_last_library_path, noop
get_last_library_path,
get_macos_version,
is_macos,
noop,
normalize_unicode,
)
from .photosdb_utils import get_db_model_version, get_db_version from .photosdb_utils import get_db_model_version, get_db_version
if is_macos: if is_macos:

View File

@ -11,8 +11,7 @@ from collections import namedtuple # pylint: disable=syntax-error
import yaml import yaml
from bpylist2 import archiver from bpylist2 import archiver
from ._constants import UNICODE_FORMAT from .unicode import normalize_unicode
from .utils import normalize_unicode
__all__ = [ __all__ = [
"PLRevGeoLocationInfo", "PLRevGeoLocationInfo",

29
osxphotos/platform.py Normal file
View File

@ -0,0 +1,29 @@
"""Functions for multi-platform support"""
import platform
import sys
is_macos = sys.platform == "darwin"
def assert_macos():
assert is_macos, "This feature only runs on macOS"
def get_macos_version():
assert_macos()
# returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".")
if len(version) == 2:
(ver, major) = version
minor = "0"
elif len(version) == 3:
(ver, major, minor) = version
else:
raise (
ValueError(
f"Could not parse version string: {platform.mac_ver()} {version}"
)
)
return (ver, major, minor)

View File

@ -4,7 +4,7 @@ import logging
import sys import sys
from typing import List, Optional from typing import List, Optional
from .utils import assert_macos, get_macos_version from .platform import assert_macos, get_macos_version
assert_macos() assert_macos()

View File

@ -2,7 +2,7 @@
from typing import Union from typing import Union
from .utils import is_macos from .platform import is_macos
def format_offset_time(offset: int) -> str: def format_offset_time(offset: int) -> str:

90
osxphotos/unicode.py Normal file
View File

@ -0,0 +1,90 @@
"""Utilities for working with unicode strings."""
from __future__ import annotations
import pathlib
import unicodedata
from typing import Literal, TypeVar, Union
from osxphotos.platform import is_macos
# Unicode format to use for comparing strings
DEFAULT_UNICODE_FORM = "NFC"
# global unicode format
_GLOBAL_UNICODE_FORM = DEFAULT_UNICODE_FORM
# global unicode format to use for filesystem paths
_GLOBAL_UNICODE_FS_FORM = "NFD" if is_macos else "NFC"
PathType = TypeVar("PathType", bound=Union[str, pathlib.Path])
UnicodeDataType = TypeVar(
"UnicodeDataType", bound=Union[str, list[str], tuple[str, ...], None]
)
__all__ = [
"get_unicode_form",
"set_unicode_form",
"get_unicode_fs_form",
"set_unicode_fs_form",
"normalize_fs_path",
"normalize_unicode",
"DEFAULT_UNICODE_FORM",
]
def get_unicode_form() -> Literal["NFC", "NFKC", "NFD", "NFKD"]:
"""Return the global unicode format"""
global _GLOBAL_UNICODE_FORM
return _GLOBAL_UNICODE_FORM
def set_unicode_form(format: Literal["NFC", "NFKC", "NFD", "NFKD"]) -> None:
"""Set the global unicode format"""
if format not in ["NFC", "NFKC", "NFD", "NFKD"]:
raise ValueError(f"Invalid unicode format: {format}")
global _GLOBAL_UNICODE_FORM
_GLOBAL_UNICODE_FORM = format
def get_unicode_fs_form() -> Literal["NFC", "NFKC", "NFD", "NFKD"]:
"""Return the global unicode filesystem format"""
global _GLOBAL_UNICODE_FS_FORM
return _GLOBAL_UNICODE_FS_FORM
def set_unicode_fs_form(format: str) -> Literal["NFC", "NFKC", "NFD", "NFKD"]:
"""Set the global unicode filesystem format"""
if format not in ["NFC", "NFKC", "NFD", "NFKD"]:
raise ValueError(f"Invalid unicode format: {format}")
global _GLOBAL_UNICODE_FS_FORM
_GLOBAL_UNICODE_FS_FORM = format
def normalize_fs_path(path: PathType) -> PathType:
"""Normalize filesystem paths with unicode in them"""
form = get_unicode_fs_form()
if isinstance(path, pathlib.Path):
return pathlib.Path(unicodedata.normalize(form, str(path)))
else:
return unicodedata.normalize(form, path)
def normalize_unicode(value: UnicodeDataType) -> UnicodeDataType:
"""normalize unicode data"""
form = get_unicode_form()
if value is None:
return None
if isinstance(value, tuple):
return tuple(unicodedata.normalize(form, v) for v in value)
elif isinstance(value, list):
return [unicodedata.normalize(form, v) for v in value]
elif isinstance(value, str):
return unicodedata.normalize(form, value)
else:
return value

View File

@ -24,7 +24,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
from .utils import assert_macos, get_macos_version, is_macos from .platform import assert_macos, get_macos_version, is_macos
if is_macos: if is_macos:
import CoreServices import CoreServices

View File

@ -1,5 +1,7 @@
""" Utility functions used in osxphotos """ """ Utility functions used in osxphotos """
from __future__ import annotations
import datetime import datetime
import fnmatch import fnmatch
import hashlib import hashlib
@ -9,30 +11,29 @@ import logging
import os import os
import os.path import os.path
import pathlib import pathlib
import platform
import re import re
import subprocess import subprocess
import sys import sys
import unicodedata
import urllib.parse import urllib.parse
from plistlib import load as plistload from plistlib import load as plistload
from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union from typing import Callable, List, Optional, Tuple, TypeVar, Union
from uuid import UUID from uuid import UUID
import requests import requests
import shortuuid import shortuuid
from ._constants import UNICODE_FORMAT from osxphotos.platform import get_macos_version, is_macos
from osxphotos.unicode import normalize_fs_path
T = TypeVar("T", bound=Union[str, pathlib.Path])
logger = logging.getLogger("osxphotos") logger = logging.getLogger("osxphotos")
__all__ = [ __all__ = [
"is_macos",
"assert_macos",
"dd_to_dms_str", "dd_to_dms_str",
"expand_and_validate_filepath", "expand_and_validate_filepath",
"get_last_library_path", "get_last_library_path",
"get_macos_version",
"get_system_library_path", "get_system_library_path",
"hexdigest", "hexdigest",
"increment_filename_with_count", "increment_filename_with_count",
@ -43,8 +44,6 @@ __all__ = [
"load_function", "load_function",
"lock_filename", "lock_filename",
"noop", "noop",
"normalize_fs_path",
"normalize_unicode",
"pluralize", "pluralize",
"shortuuid_to_uuid", "shortuuid_to_uuid",
"uuid_to_shortuuid", "uuid_to_shortuuid",
@ -54,13 +53,6 @@ __all__ = [
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json" VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
is_macos = sys.platform == "darwin"
def assert_macos():
assert is_macos, "This feature only runs on macOS"
if is_macos: if is_macos:
import CoreFoundation import CoreFoundation
@ -78,25 +70,6 @@ def lineno(filename):
return f"{filename}: {line}" return f"{filename}: {line}"
def get_macos_version():
assert_macos()
# returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".")
if len(version) == 2:
(ver, major) = version
minor = "0"
elif len(version) == 3:
(ver, major, minor) = version
else:
raise (
ValueError(
f"Could not parse version string: {platform.mac_ver()} {version}"
)
)
return (ver, major, minor)
def _check_file_exists(filename): def _check_file_exists(filename):
"""returns true if file exists and is not a directory """returns true if file exists and is not a directory
otherwise returns false""" otherwise returns false"""
@ -280,16 +253,6 @@ def list_photo_libraries():
return lib_list return lib_list
T = TypeVar("T", bound=Union[str, pathlib.Path])
def normalize_fs_path(path: T) -> T:
"""Normalize filesystem paths with unicode in them"""
form = "NFD" if is_macos else "NFC"
if isinstance(path, pathlib.Path):
return pathlib.Path(unicodedata.normalize(form, str(path)))
else:
return unicodedata.normalize(form, path)
# def findfiles(pattern, path): # def findfiles(pattern, path):
@ -378,18 +341,6 @@ def list_directory(
return files return files
def normalize_unicode(value) -> Any:
"""normalize unicode data"""
if value is None:
return None
if isinstance(value, (tuple, list)):
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
elif isinstance(value, str):
return unicodedata.normalize(UNICODE_FORMAT, value)
else:
return value
def increment_filename_with_count( def increment_filename_with_count(
filepath: Union[str, pathlib.Path], count: int = 0, lock: bool = False filepath: Union[str, pathlib.Path], count: int = 0, lock: bool = False
) -> Tuple[str, int]: ) -> Tuple[str, int]:

View File

@ -23,7 +23,7 @@ One test for locale does not run on GitHub's automated workflow and will look fo
A couple of tests require interaction with Photos and configuring a specific test library. Currently these run only on Catalina. The tests must be specified by using a pytest flag. Only one of these interactive tests can be run at a time. The current flags are: A couple of tests require interaction with Photos and configuring a specific test library. Currently these run only on Catalina. The tests must be specified by using a pytest flag. Only one of these interactive tests can be run at a time. The current flags are:
--addalbum: test --add-to-album options --addalbum: test --add-to-album options (pytest -vv tests/test_photosalbum_unicode.py tests/test_cli_add_to_album.py --addalbum)
--timewarp: test `osxphotos timewarp` --timewarp: test `osxphotos timewarp`
--test-import: test `osxphotos import` --test-import: test `osxphotos import`
--test-sync: test `osxphotos sync` --test-sync: test `osxphotos sync`

View File

@ -7,7 +7,7 @@ import time
import pytest import pytest
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import photoscript import photoscript

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@ -15,7 +15,7 @@ import pytest
import osxphotos import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import get_macos_version, is_macos from osxphotos.platform import get_macos_version, is_macos
OS_VERSION = get_macos_version() if is_macos else (None, None, None) OS_VERSION = get_macos_version() if is_macos else (None, None, None)
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15" SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"

View File

@ -36,7 +36,9 @@ from osxphotos.cli import (
) )
from osxphotos.exiftool import ExifTool, get_exiftool_path from osxphotos.exiftool import ExifTool, get_exiftool_path
from osxphotos.fileutil import FileUtil from osxphotos.fileutil import FileUtil
from osxphotos.utils import is_macos, noop, normalize_fs_path, normalize_unicode from osxphotos.platform import is_macos
from osxphotos.unicode import normalize_fs_path, normalize_unicode
from osxphotos.utils import noop
if is_macos: if is_macos:
from osxmetadata import OSXMetaData, Tag from osxmetadata import OSXMetaData, Tag

View File

@ -3,7 +3,7 @@
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import photoscript import photoscript

View File

@ -5,7 +5,7 @@ import os
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import photoscript import photoscript

View File

@ -44,7 +44,7 @@ from osxphotos.cli import (
tutorial, tutorial,
version, version,
) )
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
from osxphotos.cli import uuid from osxphotos.cli import uuid

View File

@ -9,7 +9,7 @@ import pytest
from click.testing import CliRunner from click.testing import CliRunner
import osxphotos import osxphotos
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import photoscript import photoscript

View File

@ -9,6 +9,7 @@ import re
import shutil import shutil
import sqlite3 import sqlite3
import time import time
import unicodedata
from datetime import datetime from datetime import datetime
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Dict from typing import Dict
@ -21,7 +22,7 @@ from osxphotos import PhotosDB, QueryOptions
from osxphotos._constants import UUID_PATTERN from osxphotos._constants import UUID_PATTERN
from osxphotos.datetime_utils import datetime_remove_tz from osxphotos.datetime_utils import datetime_remove_tz
from osxphotos.exiftool import get_exiftool_path from osxphotos.exiftool import get_exiftool_path
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
from tests.conftest import get_os_version from tests.conftest import get_os_version
if is_macos: if is_macos:
@ -44,17 +45,17 @@ TEST_DATA = {
TEST_IMAGE_1: { TEST_IMAGE_1: {
"title": "Waves crashing on rocks", "title": "Waves crashing on rocks",
"description": "Used for testing osxphotos", "description": "Used for testing osxphotos",
"keywords": ["osxphotos"], "keywords": ["osxphotos", "Sümmer"],
"lat": 33.7150638888889, "lat": 33.7150638888889,
"lon": -118.319672222222, "lon": -118.319672222222,
"check_templates": [ "check_templates": [
"exiftool title: Waves crashing on rocks", "exiftool title: Waves crashing on rocks",
"exiftool description: Used for testing osxphotos", "exiftool description: Used for testing osxphotos",
"exiftool keywords: ['osxphotos']", "exiftool keywords: ['osxphotos', 'Sümmer']",
"exiftool location: (33.7150638888889, -118.319672222222)", "exiftool location: (33.7150638888889, -118.319672222222)",
"title: {exiftool:XMP:Title}: Waves crashing on rocks", "title: {exiftool:XMP:Title}: Waves crashing on rocks",
"description: {exiftool:IPTC:Caption-Abstract}: Used for testing osxphotos", "description: {exiftool:IPTC:Caption-Abstract}: Used for testing osxphotos",
"keyword: {exiftool:IPTC:Keywords}: ['osxphotos']", "keyword: {exiftool:IPTC:Keywords}: ['osxphotos', 'Sümmer']",
"album: {filepath.parent}: test-images", "album: {filepath.parent}: test-images",
], ],
}, },
@ -536,7 +537,44 @@ def test_import_keyword_merge():
photo_1 = Photo(uuid_1) photo_1 = Photo(uuid_1)
assert photo_1.filename == file_1 assert photo_1.filename == file_1
assert sorted(photo_1.keywords) == ["Bar", "Foo", "osxphotos"] assert sorted(photo_1.keywords) == sorted(list(set(["Bar", "Foo"] + TEST_DATA[TEST_IMAGE_1]["keywords"])))
@pytest.mark.skipif(exiftool_path is None, reason="exiftool not installed")
@pytest.mark.test_import
def test_import_keyword_merge_unicode():
"""Test import with --keyword and --merge-keywords with unicode keywords (#1085)"""
cwd = os.getcwd()
test_image_1 = os.path.join(cwd, TEST_IMAGE_1)
runner = CliRunner()
result = runner.invoke(
import_cli,
[
"--verbose",
"--clear-metadata",
"--exiftool",
"--keyword",
"Bar",
"--keyword",
"Foo",
"--keyword",
unicodedata.normalize("NFD", "Sümmer"),
"--keyword",
unicodedata.normalize("NFC", "Sümmer"),
"--merge-keywords",
test_image_1,
],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image_1).name
uuid_1 = import_data[file_1]
photo_1 = Photo(uuid_1)
assert photo_1.filename == file_1
assert sorted(photo_1.keywords) == sorted(list(set(["Bar", "Foo"] + TEST_DATA[TEST_IMAGE_1]["keywords"])))
@pytest.mark.test_import @pytest.mark.test_import

View File

@ -6,7 +6,7 @@ import os
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
import photoscript import photoscript

View File

@ -3,7 +3,7 @@ import os
import pytest import pytest
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.utils import get_macos_version, is_macos from osxphotos.platform import get_macos_version, is_macos
OS_VERSION = get_macos_version() if is_macos else (None, None, None) OS_VERSION = get_macos_version() if is_macos else (None, None, None)
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15" SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"

View File

@ -14,7 +14,7 @@ import pytest
import osxphotos import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import get_macos_version, is_macos from osxphotos.platform import get_macos_version, is_macos
OS_VERSION = get_macos_version() if is_macos else (None, None, None) OS_VERSION = get_macos_version() if is_macos else (None, None, None)
# SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17" # SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17"

View File

@ -6,7 +6,7 @@ import tempfile
import pytest import pytest
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
if is_macos: if is_macos:
from osxphotos.photokit import ( from osxphotos.photokit import (

View File

@ -0,0 +1,90 @@
"""Test unicode names in PhotoAlbum PhotoAlbumPhotoScript (#1085)"""
import pathlib
from unicodedata import normalize
import pytest
from osxphotos.platform import is_macos
if not is_macos:
pytest.skip("requires macOS", allow_module_level=True)
import osxphotos
from osxphotos.photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
from osxphotos.unicode import *
UNICODE_FOLDER_NFC = normalize("NFC", "FolderUnicode/føldêr2")
UNICODE_FOLDER_NFD = normalize("NFD", UNICODE_FOLDER_NFC)
UNICODE_ALBUM_NFC = normalize("NFC", "âlbüm")
UNICODE_ALBUM_NFD = normalize("NFD", UNICODE_ALBUM_NFC)
@pytest.mark.skipif(not is_macos, reason="requires macOS")
@pytest.mark.addalbum
def test_unicode_album(addalbum_library):
"""Test that unicode album name is handled correctly and a duplicate album is not created"""
# get some photos to add
photosdb = osxphotos.PhotosDB()
photos = photosdb.query(osxphotos.QueryOptions(person=["Katie"]))
# get the album
album_name_nfc = UNICODE_ALBUM_NFC
album_nfc = PhotosAlbum(album_name_nfc, split_folder=None)
album_nfc.add_list(photos)
# again with NFD
album_name_nfd = UNICODE_ALBUM_NFD
album_nfd = PhotosAlbum(album_name_nfd, split_folder=None)
album_nfd.add_list(photos)
assert album_nfc.album.uuid == album_nfd.album.uuid
@pytest.mark.skipif(not is_macos, reason="requires macOS")
@pytest.mark.addalbum
def test_unicode_folder_album_1(addalbum_library):
"""Test that unicode album name is handled correctly and a duplicate album is not created when album is in a folder"""
# get some photos to add
photosdb = osxphotos.PhotosDB()
photos = photosdb.query(osxphotos.QueryOptions(person=["Katie"]))
# get the album
album_name_nfc = f"{UNICODE_FOLDER_NFC}/{UNICODE_ALBUM_NFC}"
album_nfc = PhotosAlbum(album_name_nfc, split_folder="/")
album_nfc.add_list(photos)
# again with NFD
album_name_nfd = f"{UNICODE_FOLDER_NFD}/{UNICODE_ALBUM_NFD}"
album_nfd = PhotosAlbum(album_name_nfd, split_folder="/")
album_nfd.add_list(photos)
assert album_nfc.album.uuid == album_nfd.album.uuid
@pytest.mark.skipif(not is_macos, reason="requires macOS")
@pytest.mark.addalbum
def test_unicode_folder_album_2(addalbum_library):
"""Test that unicode album name is handled correctly and a duplicate album is not created when album is in a folder
This is a variation of test_unicode_folder_album_1 where the album is created in the same unicode folder as the previous album
"""
# get some photos to add
photosdb = osxphotos.PhotosDB()
photos = photosdb.query(osxphotos.QueryOptions(person=["Katie"]))
# get the album
album_name_nfc = f"{UNICODE_FOLDER_NFC}/{UNICODE_ALBUM_NFC}"
album_nfc = PhotosAlbum(album_name_nfc, split_folder="/")
album_nfc.add_list(photos)
# again with NFD
album_name_nfd = f"{UNICODE_FOLDER_NFC}/{UNICODE_ALBUM_NFD}"
album_nfd = PhotosAlbum(album_name_nfd, split_folder="/")
album_nfd.add_list(photos)
assert album_nfc.album.uuid == album_nfd.album.uuid

View File

@ -15,7 +15,7 @@ from osxphotos.phototemplate import (
PhotoTemplate, PhotoTemplate,
RenderOptions, RenderOptions,
) )
from osxphotos.utils import is_macos from osxphotos.platform import is_macos
from .locale_util import setlocale from .locale_util import setlocale
from .photoinfo_mock import PhotoInfoMock from .photoinfo_mock import PhotoInfoMock

110
tests/test_unicode.py Normal file
View File

@ -0,0 +1,110 @@
"""Test unicode utilities"""
import pathlib
from unicodedata import normalize
import pytest
from osxphotos.platform import is_macos
from osxphotos.unicode import *
UNICODE_PATH_NFC = normalize("NFC", "/path/to/ünicøde")
UNICODE_PATH_NFD = normalize("NFD", UNICODE_PATH_NFC)
UNICODE_STR_NFC = normalize("NFC", "âbc")
UNICODE_STR_NFD = normalize("NFD", UNICODE_STR_NFC)
UNICODE_LIST_NFC = [normalize("NFC", "âbc"), normalize("NFC", "")]
UNICODE_LIST_NFD = [normalize("NFD", "âbc"), normalize("NFD", "")]
def test_get_unicode_format():
set_unicode_form("NFC")
assert get_unicode_form() == "NFC"
def test_set_unicode_format():
set_unicode_form("NFD")
assert get_unicode_form() == "NFD"
set_unicode_form("NFC")
assert get_unicode_form() == "NFC"
# test invalid format
with pytest.raises(ValueError):
set_unicode_form("foo")
# Reset to correct format based
set_unicode_form(DEFAULT_UNICODE_FORM)
def test_set_unicode_fs_format():
set_unicode_fs_form("NFC")
assert get_unicode_fs_form() == "NFC"
set_unicode_fs_form("NFD")
assert get_unicode_fs_form() == "NFD"
# test invalid format
with pytest.raises(ValueError):
set_unicode_fs_form("foo")
# Reset to correct format based on platform
set_unicode_fs_form("NFD" if is_macos else "NFC")
def test_normalize_fs_path():
# Test with string path in NFC format
set_unicode_fs_form("NFC")
assert normalize_fs_path(UNICODE_PATH_NFD) == UNICODE_PATH_NFC
# Test with string path in NFD format
set_unicode_fs_form("NFD")
assert normalize_fs_path(UNICODE_PATH_NFC) == UNICODE_PATH_NFD
# Test with pathlib.Path object in NFC format
set_unicode_fs_form("NFC")
assert normalize_fs_path(pathlib.Path(UNICODE_PATH_NFD)) == pathlib.Path(
UNICODE_PATH_NFC
)
# Test with pathlib.Path object in NFD format
set_unicode_fs_form("NFD")
assert normalize_fs_path(pathlib.Path(UNICODE_PATH_NFC)) == pathlib.Path(
UNICODE_PATH_NFD
)
# Reset to correct format based on platform
set_unicode_fs_form("NFD" if is_macos else "NFC")
def test_normalize_unicode():
# Test with str in NFC format
set_unicode_form("NFC")
assert normalize_unicode(UNICODE_STR_NFD) == UNICODE_STR_NFC
# Test with str in NFD format
set_unicode_form("NFD")
assert normalize_unicode(UNICODE_STR_NFC) == UNICODE_STR_NFD
# Test with list of str in NFC format
set_unicode_form("NFC")
assert normalize_unicode(UNICODE_LIST_NFD) == UNICODE_LIST_NFC
# Test with list of str in NFD format
set_unicode_form("NFD")
assert normalize_unicode(UNICODE_LIST_NFC) == UNICODE_LIST_NFD
# Test with tuple of str in NFC format
set_unicode_form("NFC")
assert normalize_unicode(tuple(UNICODE_LIST_NFD)) == tuple(UNICODE_LIST_NFC)
# Test with tuple of str in NFD format
set_unicode_form("NFD")
assert normalize_unicode(tuple(UNICODE_LIST_NFC)) == tuple(UNICODE_LIST_NFD)
# Test with None
assert normalize_unicode(None) is None
# Reset to correct format based
set_unicode_form(DEFAULT_UNICODE_FORM)

View File

@ -3,12 +3,12 @@
import pytest import pytest
import osxphotos.uti import osxphotos.uti
from osxphotos.platform import is_macos
from osxphotos.uti import ( from osxphotos.uti import (
_get_uti_from_mdls, _get_uti_from_mdls,
get_preferred_uti_extension, get_preferred_uti_extension,
get_uti_for_extension, get_uti_for_extension,
) )
from osxphotos.utils import is_macos
EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"} EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"}
UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"} UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"}