diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1a8f500 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: build + +on: + push: + pull_request: + schedule: + - cron: 23 11 * * */14 + +jobs: + python: + name: Testing HugoPhotoSwipe + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ] + py: [ '3.6', '3.9' ] + steps: + - name: Install Python ${{ matrix.py }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.py }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Test + run: make test_direct diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3cb791c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + language_version: python3 diff --git a/Makefile b/Makefile index 3800c6e..2f379db 100644 --- a/Makefile +++ b/Makefile @@ -46,8 +46,15 @@ dist: ## Make Python source distribution .PHONY: test -test: venv ## Run nosetests using the default nosetests command - source $(VENV_DIR)/bin/activate && green -a -vv ./tests +test: venv ## Run unit tests in virtual environment + source $(VENV_DIR)/bin/activate && green -a -s 1 -vv ./tests + +test_direct: ## Run unit tests without virtual environment (typically for CI) + pip install .[tests] && python -m unittest discover -v ./tests + +cover: venv + source $(VENV_DIR)/bin/activate && green -a -r -s 1 -vv ./tests + ####################### # Virtual environment # @@ -55,10 +62,10 @@ test: venv ## Run nosetests using the default nosetests command .PHONY: venv -venv: $(VENV_DIR)/bin/activate +venv: $(VENV_DIR)/bin/activate ## Create a virtual environment $(VENV_DIR)/bin/activate: - test -d $(VENV_DIR) || virtualenv $(VENV_DIR) + test -d $(VENV_DIR) || python -m venv $(VENV_DIR) source $(VENV_DIR)/bin/activate && pip install -e .[dev] touch $(VENV_DIR)/bin/activate @@ -66,6 +73,9 @@ $(VENV_DIR)/bin/activate: # Clean up # ############ +clean_venv: ## Clean up the virtual environment + rm -rf $(VENV_DIR) + clean: ## Clean build dist and egg directories left after install rm -rf ./dist rm -rf ./build diff --git a/README.md b/README.md index 748adba..45ad87e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # HugoPhotoSwipe +[![build](https://github.com/GjjvdBurg/HugoPhotoSwipe/actions/workflows/build.yml/badge.svg)](https://github.com/GjjvdBurg/HugoPhotoSwipe/actions/workflows/build.yml) [![PyPI version](https://badge.fury.io/py/hugophotoswipe.svg)](https://pypi.org/project/hugophotoswipe) [![Downloads](https://pepy.tech/badge/hugophotoswipe)](https://pepy.tech/project/hugophotoswipe) diff --git a/hugophotoswipe/album.py b/hugophotoswipe/album.py index 9ac7123..03394dc 100644 --- a/hugophotoswipe/album.py +++ b/hugophotoswipe/album.py @@ -10,8 +10,6 @@ """ -from __future__ import print_function - import logging import os import shutil @@ -19,9 +17,11 @@ from tqdm import tqdm -from .conf import settings +from .config import settings from .photo import Photo -from .utils import yaml_field_to_file, modtime, question_yes_no, mkdirs +from .utils import modtime +from .utils import question_yes_no +from .utils import yaml_field_to_file class Album(object): @@ -73,8 +73,8 @@ def names_unique(self): @property def markdown_file(self): """ Path of the markdown file """ - md_dir = os.path.realpath(settings.markdown_dir) - mkdirs(md_dir) + md_dir = os.path.abspath(settings.markdown_dir) + os.makedirs(md_dir, exist_ok=True) return os.path.join(md_dir, self.name + ".md") @property @@ -89,10 +89,12 @@ def output_dir(self): # # ################ - def clean(self): - """ Clean up the processed images and the markdown file + def clean(self, force=False): + """Clean up the processed images and the markdown file Ask the user for confirmation and only remove if it exists + + If ``force = True``, don't ask for confirmation. """ output_dir = os.path.join(settings.output_dir, self.name) have_md = os.path.exists(self.markdown_file) @@ -106,7 +108,7 @@ def clean(self): q += ". Is this okay?" if (not have_md) and (not have_out): return - if not question_yes_no(q): + if not force and not question_yes_no(q): return if have_md: @@ -160,7 +162,7 @@ def create_markdown(self): fid.write("\n".join(txt)) print("Written markdown file: %s" % self.markdown_file) - def dump(self): + def dump(self, modification_time=None): """ Save the album configuration to a YAML file """ if self._album_file is None: raise ValueError("Album file is not defined.") @@ -184,8 +186,9 @@ def dump(self): yaml_field_to_file( fid, self.creation_time, "creation_time", force_string=True ) + modification_time = modification_time or modtime() yaml_field_to_file( - fid, modtime(), "modification_time", force_string=True + fid, modification_time, "modification_time", force_string=True ) fid.write("\n") @@ -204,7 +207,9 @@ def dump(self): for photo in self.photos: fid.write("\n") yaml_field_to_file(fid, photo.filename, "file", indent="- ") - yaml_field_to_file(fid, hash(photo), "hash", indent=" ") + yaml_field_to_file( + fid, "sha256:" + photo.sha256sum(), "hash", indent=" " + ) print("Updated album file: %s" % self._album_file) @classmethod @@ -216,8 +221,9 @@ def load(cls, album_dir): with open(album_file, "r") as fid: data.update(yaml.safe_load(fid)) else: - print("Skipping non-album directory: %s" % album_dir) + logging.warning("Skipping non-album directory: %s" % album_dir) return None + album = cls(**data) album.cover_path = os.path.join( settings.output_dir, album.name, settings.cover_filename @@ -226,12 +232,15 @@ def load(cls, album_dir): all_photos = [] for p in album.photos: photo_path = os.path.join(album_dir, settings.photo_dir, p["file"]) - caption = "" if p["caption"] is None else p["caption"].strip() + caption = ( + "" if p.get("caption", None) is None else p["caption"].strip() + ) + alt = "" if p.get("alt", None) is None else p["alt"].strip() photo = Photo( album_name=album.name, original_path=photo_path, name=p["name"], - alt=p["alt"], + alt=alt, caption=caption, copyright=album.copyright, ) @@ -240,16 +249,21 @@ def load(cls, album_dir): album.photos = [] for photo in all_photos: if photo.name is None: - print("No name defined for photo %r. Using filename." % photo) - continue + logging.warning( + "No name defined for photo %r. Using filename." % photo + ) + photo.name = os.path.basename(photo.original_path) album.photos.append(photo) return album - def update(self): + def update(self, modification_time=None): """ Update the processed images and the markdown file """ if not self.names_unique: - print("Photo names for this album aren't unique. Not processing.") + logging.error( + "Photo names for this album aren't unique. Not processing." + ) return + # Make sure the list of photos from the yaml is up to date with # the photos in the directory, simply add all the new photos to # self.photos @@ -258,13 +272,13 @@ def update(self): missing = [f for f in os.listdir(photo_dir) if not f in photo_files] missing.sort() for f in missing: - pho = Photo( + photo = Photo( album_name=self.name, original_path=os.path.join(photo_dir, f), name=f, copyright=self.copyright, ) - self.photos.append(pho) + self.photos.append(photo) logging.info( "[%s] Found %i photos from yaml and photos dir" % (self.name, len(self.photos)) @@ -296,7 +310,7 @@ def update(self): for photo in self.photos: hsh = next( ( - h["hash"] + str(h["hash"]).split(":")[-1] for h in self.hashes if h["file"] == photo.filename ), @@ -305,10 +319,12 @@ def update(self): photo_hashes[photo] = hsh to_process = [] - for p in self.photos: - if not (p.has_sizes() and (hash(p) == photo_hashes[p])): - to_process.append(p) - del p.original_image + for photo in self.photos: + if not photo.has_sizes(): + to_process.append(photo) + elif not photo.sha256sum() == photo_hashes[photo]: + to_process.append(photo) + photo.free() logging.info( "[%s] There are %i photos to process." @@ -317,12 +333,12 @@ def update(self): if to_process: iterator = ( iter(to_process) - if settings.verbose + if not settings.verbose else tqdm(to_process, desc="Progress") ) for photo in iterator: photo.create_sizes() - del photo.original_image + photo.free() # Overwrite the markdown file logging.info("[%s] Writing markdown file." % self.name) @@ -330,7 +346,7 @@ def update(self): # Overwrite the yaml file of the album logging.info("[%s] Saving album yaml." % self.name) - self.dump() + self.dump(modification_time=modification_time) #################### # # diff --git a/hugophotoswipe/conf.py b/hugophotoswipe/config.py similarity index 50% rename from hugophotoswipe/conf.py rename to hugophotoswipe/config.py index 260e38c..2d6a48b 100644 --- a/hugophotoswipe/conf.py +++ b/hugophotoswipe/config.py @@ -14,9 +14,10 @@ """ +import logging import os -import yaml import warnings +import yaml from . import __version__ from .utils import yaml_field_to_file @@ -50,6 +51,8 @@ "jpeg_progressive": False, "jpeg_optimize": False, "jpeg_quality": 75, + "fast": False, + "verbose": False, } DONT_DUMP = ["verbose", "fast"] @@ -59,44 +62,6 @@ class Settings(object): def __init__(self, **entries): self.__dict__.update(DEFAULTS) - if "dim_thumbnail" in entries: - warnings.warn( - "The 'dim_thumbnail' option has been replaced by " - "the 'dim_max_thumb' option in version 0.0.7. Your " - "hugophotoswipe.yml file will be updated.", - DeprecationWarning, - ) - entries["dim_max_thumb"] = entries["dim_thumbnail"] - del entries["dim_thumbnail"] - if "dim_coverimage" in entries: - warnings.warn( - "The 'dim_coverimage' option has been replaced by " - "the 'dim_max_cover' option in version 0.0.7. Your " - "hugophotoswipe.yml file will be updated.", - DeprecationWarning, - ) - entries["dim_max_cover"] = entries["dim_coverimage"] - del entries["dim_coverimage"] - - # remove deprecated square options - square_options = ["square_thumbnails", "square_coverimage"] - square_dim_opts = { - "square_thumbnails": "dim_max_thumb", - "square_coverimage": "dim_max_cover", - } - for opt in square_options: - if opt in entries: - warnings.warn( - "The '%s' option has been removed " - "because of the new size syntax of version 0.0.15. Your " - "hugophotoswipe.yml file will be updated." % opt, - DeprecationWarning, - ) - if entries[opt]: - dim = entries[square_dim_opts[opt]] - entries[square_dim_opts[opt]] = "%ix%i" % (dim, dim) - del entries[opt] - # ensure dim_max is always string for key in entries: if key.startswith("dim_max_"): @@ -104,37 +69,38 @@ def __init__(self, **entries): self.__dict__.update(entries) - def dump(self, dirname=None): + def dump(self, dirname=None, settings_filename=None): """ Write settings to yaml file """ - dirname = "" if dirname is None else dirname - pth = os.path.join(dirname, SETTINGS_FILENAME) - with open(pth, "w") as fid: - fid.write("---\n") + if settings_filename is None: + dirname = "" if dirname is None else dirname + settings_filename = os.path.join(dirname, SETTINGS_FILENAME) + with open(settings_filename, "w") as fp: + fp.write("---\n") for key in sorted(self.__dict__.keys()): if key in DONT_DUMP: continue - yaml_field_to_file(fid, getattr(self, key), key) + yaml_field_to_file(fp, getattr(self, key), key) def validate(self): """ Check settings for consistency """ prefix = "Error in settings file: " if self.markdown_dir is None: - print(prefix + "markdown_dir can't be empty") + logging.error(prefix + "markdown_dir can't be empty") return False if self.output_dir is None: - print(prefix + "output_dir can't be empty") + logging.error(prefix + "output_dir can't be empty") return False if self.use_smartcrop_js and self.smartcrop_js_path is None: - print(prefix + "smartcrop.js requested but path not set") + logging.error(prefix + "smartcrop.js requested but path not set") return False return True -def load_settings(): +def load_settings(settings_filename=SETTINGS_FILENAME): data = {} - if os.path.exists(SETTINGS_FILENAME): - with open(SETTINGS_FILENAME, "r") as fid: - data = yaml.safe_load(fid) + if os.path.exists(settings_filename): + with open(settings_filename, "r") as fp: + data = yaml.safe_load(fp) return Settings(**data) diff --git a/hugophotoswipe/hugophotoswipe.py b/hugophotoswipe/hugophotoswipe.py index 73db6c0..cf5e2ca 100644 --- a/hugophotoswipe/hugophotoswipe.py +++ b/hugophotoswipe/hugophotoswipe.py @@ -11,22 +11,17 @@ """ -from __future__ import print_function - import logging import os -import six from .album import Album -from .conf import settings -from .utils import mkdirs, modtime +from .config import settings +from .utils import modtime class HugoPhotoSwipe(object): def __init__(self, albums=None): - self._albums = albums - if self._albums is None: - self._albums = self._load_albums() + self._albums = self._load_albums() if albums is None else albums ################ # # @@ -37,15 +32,19 @@ def __init__(self, albums=None): def new(self, name=None): """ Create new album """ if name is None: - name = six.moves.input("Please provide a name for the new album: ") + name = input("Please provide a name for the new album: ") + album_dir = name.strip().rstrip("/").replace(" ", "_") if os.path.exists(album_dir): print("Can't create album with this name, it exists already.") - raise SystemExit + raise SystemExit(1) + logging.info("Creating album directory") - mkdirs(album_dir) + os.makedirs(album_dir, exist_ok=True) + logging.info("Creating album photos directory") - mkdirs(os.path.join(album_dir, settings.photo_dir)) + os.makedirs(os.path.join(album_dir, settings.photo_dir), exist_ok=True) + album = Album(album_dir=album_dir, creation_time=modtime()) logging.info("Saving album yaml") album.dump() @@ -53,34 +52,40 @@ def new(self, name=None): def update(self, name=None): """ Update all markdown and resizes for each album """ - if name is None: - for album in self._albums: - print("Updating album: %s" % album.name) - album.update() - print("All albums updated.") - else: - name = name.strip("/") - album = next((a for a in self._albums if a.name == name), None) - if album is None: - print("Couldn't find album with name %s. Stopping." % name) - return + self.update_all() if name is None else self.update_single(name) + + def update_all(self): + for album in self._albums: + print("Updating album: %s" % album.name) album.update() - print("Album %s updated." % album.name) + print("All albums updated.") + + def update_single(self, name): + name = name.strip("/") + album = next((a for a in self._albums if a.name == name), None) + if album is None: + print("Couldn't find album with name %s. Stopping." % name) + raise SystemExit(1) + album.update() + print("Album %s updated." % album.name) def clean(self, name=None): """ Clean up all markdown and resizes for each album """ - if name is None: - for album in self._albums: - album.clean() - print("All albums cleaned.") - else: - name = name.strip("/") - album = next((a for a in self._albums if a.name == name), None) - if album is None: - print("Couldn't find album with name %s. Stopping." % name) - return + self.clean_all() if name is None else self.clean_single(name) + + def clean_all(self): + for album in self._albums: album.clean() - print("Album %s cleaned." % album.name) + print("All albums cleaned.") + + def clean_single(self, name): + name = name.strip("/") + album = next((a for a in self._albums if a.name == name), None) + if album is None: + print("Couldn't find album with name %s. Stopping." % name) + raise SystemExit(1) + album.clean() + print("Album %s cleaned." % album.name) #################### # # diff --git a/hugophotoswipe/photo.py b/hugophotoswipe/photo.py index 82db6ba..e02ee09 100644 --- a/hugophotoswipe/photo.py +++ b/hugophotoswipe/photo.py @@ -11,41 +11,21 @@ """ -from __future__ import print_function, division - import hashlib import logging import os import smartcrop import tempfile -from PIL import Image, ExifTags +from PIL import Image +from PIL import ExifTags from functools import total_ordering from textwrap import wrap +from textwrap import indent from subprocess import check_output -from .conf import settings -from .utils import mkdirs, cached_property - -import six - -if six.PY2: - - def indent(text, prefix, predicate=None): - if predicate is None: - - def predicate(line): - return line.strip() - - def prefixed_lines(): - for line in text.splitlines(True): - yield (prefix + line if predicate(line) else line) - - return "".join(prefixed_lines()) - - -else: - from textwrap import indent +from .config import settings +from .utils import cached_property @total_ordering @@ -76,15 +56,26 @@ def __init__( self.copyright = copyright self.cover_path = None + # caching + self._original_img = None + ################ # # # User methods # # # ################ - @cached_property + @property def original_image(self): """ Open original image and if needed rotate it according to EXIF """ + if not self._original_img is None: + return self._original_img + + img = self._load_original_image() + self._original_img = img + return self._original_img + + def _load_original_image(self): img = Image.open(self.original_path) # if there is no exif data, simply return the image exif = img._getexif() @@ -114,6 +105,13 @@ def original_image(self): # fallback for unhandled rotation tags return img + def free(self): + """Manually clean up the cached image""" + if hasattr(self, "_original_img") and self._original_img: + self._original_img.close() + del self._original_img + self._original_img = None + def has_sizes(self): """ Check if all necessary sizes exist on disk """ if self.name is None: @@ -175,9 +173,8 @@ def create_rescaled(self, mode): def create_thumb(self, mode=None, pth=None): """ Create the image thumbnail """ if settings.use_smartcrop_js: - self.create_thumb_js(mode=mode, pth=pth) - else: - self.create_thumb_py(mode=mode, pth=pth) + return self.create_thumb_js(mode=mode, pth=pth) + return self.create_thumb_py(mode=mode, pth=pth) def create_thumb_py(self, mode=None, pth=None): """ Create the thumbnail using SmartCrop.py """ @@ -314,6 +311,16 @@ def resize_dims(self, mode): return nwidth, nheight + def sha256sum(self): + blocksize = 65536 + hasher = hashlib.sha256() + with open(self.original_path, "rb") as fp: + buf = fp.read(blocksize) + while buf: + hasher.update(buf) + buf = fp.read(blocksize) + return hasher.hexdigest() + @property def clean_name(self): """ The name of the image without extension and spaces """ @@ -322,50 +329,23 @@ def clean_name(self): @cached_property def large_path(self): - """ The path of the large resized image """ - thedir = os.path.join( - settings.output_dir, self.album_name, settings.dirname_large - ) - mkdirs(thedir) - width, height = self.resize_dims("large") - fname = "%s_%ix%i.%s" % ( - self.clean_name, - width, - height, - settings.output_format, - ) - return os.path.join(thedir, fname) + return self._get_path("large") @cached_property def small_path(self): - """ The path of the small resized image """ - thedir = os.path.join( - settings.output_dir, self.album_name, settings.dirname_small - ) - mkdirs(thedir) - width, height = self.resize_dims("small") - fname = "%s_%ix%i.%s" % ( - self.clean_name, - width, - height, - settings.output_format, - ) - return os.path.join(thedir, fname) + return self._get_path("small") @cached_property def thumb_path(self): - """ The path of the thumbnail image """ - thedir = os.path.join( - settings.output_dir, self.album_name, settings.dirname_thumb - ) - mkdirs(thedir) - width, height = self.resize_dims("thumb") - fname = "%s_%ix%i.%s" % ( - self.clean_name, - width, - height, - settings.output_format, - ) + return self._get_path("thumb") + + def _get_path(self, mode): + mode_dir = getattr(settings, f"dirname_{mode}") + thedir = os.path.join(settings.output_dir, self.album_name, mode_dir) + os.makedirs(thedir, exist_ok=True) + width, height = self.resize_dims(mode) + ext = settings.output_format + fname = f"{self.clean_name}_{width:d}x{height:d}.{ext}" return os.path.join(thedir, fname) @property @@ -400,6 +380,7 @@ def shortcode(self): thumb_dim = "%ix%i" % self.resize_dims("thumb") caption = "" if self.caption is None else self.caption.strip() copyright = "" if self.copyright is None else self.copyright.strip() + alt = "" if self.alt is None else self.alt.strip() shortcode = ( '{{{{< photo href="{large}" largeDim="{large_dim}" ' 'smallUrl="{small}" smallDim="{small_dim}" alt="{alt}" ' @@ -413,7 +394,7 @@ def shortcode(self): small_dim=small_dim, thumb=thumb_path, thumb_dim=thumb_dim, - alt=self.alt, + alt=alt, caption=caption, copyright=copyright, ) @@ -444,17 +425,13 @@ def __repr__(self): return s def __hash__(self): - blocksize = 65536 - hasher = hashlib.sha256() - with open(self.original_path, "rb") as fid: - buf = fid.read(blocksize) - while len(buf) > 0: - hasher.update(buf) - buf = fid.read(blocksize) - return int(float.fromhex(hasher.hexdigest())) + return int(float.fromhex(self.sha256sum())) def __lt__(self, other): return self.original_path < other.original_path def __eq__(self, other): return self.__key() == other.__key() + + def __del__(self): + self.free() diff --git a/hugophotoswipe/ui.py b/hugophotoswipe/ui.py index 7d3dfb3..dd5fb53 100644 --- a/hugophotoswipe/ui.py +++ b/hugophotoswipe/ui.py @@ -11,14 +11,14 @@ """ -from __future__ import print_function - import argparse import logging +import os from . import __version__ +from .config import SETTINGS_FILENAME +from .config import settings from .hugophotoswipe import HugoPhotoSwipe -from .conf import settings, SETTINGS_FILENAME def main(): @@ -31,6 +31,13 @@ def main(): print("Created settings file: %s" % SETTINGS_FILENAME) return + if not os.path.exists(SETTINGS_FILENAME): + print( + "Can't find %s, please run `hps init` to create it" + % SETTINGS_FILENAME + ) + raise SystemExit(1) + if not settings.validate(): return @@ -68,10 +75,13 @@ def parse_args(): "-f", "--fast", action="store_true", - help=("Fast mode " "(tries less potential crops)"), + help=("Fast mode (attempts fewer potential crops for thumbnails)"), ) parser.add_argument( - "-V", "--version", action="version", version=__version__, + "-V", + "--version", + action="version", + version=__version__, ) parser.add_argument( "command", diff --git a/hugophotoswipe/utils.py b/hugophotoswipe/utils.py index b982363..f6954b0 100644 --- a/hugophotoswipe/utils.py +++ b/hugophotoswipe/utils.py @@ -9,82 +9,51 @@ """ -from __future__ import print_function - -import errno -import os - -import six - from datetime import datetime - -if six.PY2: - from tzlocal import get_localzone - import pytz -else: - from datetime import timezone - - -def mkdirs(path): - """ Create directories recursively and don't complain when they exist """ - try: - os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): - pass - else: - raise +from datetime import timezone def modtime(): """ Get the current local time as a string in iso format """ - if six.PY2: - local_tz = get_localzone() - now = datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(local_tz) - else: - now = datetime.now(timezone.utc).astimezone() + now = datetime.now(timezone.utc).astimezone() nowstr = now.replace(microsecond=0).isoformat() return nowstr -def yaml_field_to_file(fid, data, field, indent="", force_string=False): +def yaml_field_to_file(fp, data, field, indent="", force_string=False): """ Handy function for writing pretty yaml """ if data is None: - fid.write("%s%s:\n" % (indent, field)) - else: - if force_string: - fid.write('%s%s: "%s"\n' % (indent, field, data)) - else: - fid.write("%s%s: %s\n" % (indent, field, data)) + return fp.write("%s%s:\n" % (indent, field)) + if isinstance(data, str) and len(data) == 0: + return fp.write("%s%s:\n" % (indent, field)) + fmt = '%s%s: "%s"\n' if force_string else "%s%s: %s\n" + fp.write(fmt % (indent, field, data)) def question_yes_no(question, default=True): """ Ask a yes/no question from the user and be persistent """ - if default: - extension = "[Y/n/q]" - else: - extension = "[y/N/q]" + extension = "[Y/n/q]" if default else "[y/N/q]" while True: - user_input = six.moves.input("%s %s " % (question, extension)) + user_input = input("%s %s " % (question, extension)) if user_input == "q": - raise SystemExit + raise SystemExit(0) + if user_input.lower() in ["y", "yes"]: return True elif user_input.lower() in ["n", "no"]: return False elif not user_input: return default - else: - print("No valid input, please try again.") + print("No valid input, please try again.") class cached_property(object): """Decorator for cached class properties - Decorator that converts a method with a single self argument into a + Decorator that converts a method with a single self argument into a property cached on the instance. - From Django: + From Django: https://github.com/django/django/blob/master/django/utils/functional.py """ diff --git a/tests/_constants.py b/tests/_constants.py new file mode 100644 index 0000000..89bed4e --- /dev/null +++ b/tests/_constants.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- + +TEST_ALBUM_MARKDOWN_1 = """\ ++++ +title = "dogs" +date = "" + +cover = "/hpstest/photos/dogs/coverimage.jpg" ++++ + +{{< wrap >}} +{{< photo href="/hpstest/photos/dogs/large/dog_1_1600x1066.jpg" largeDim="1600x1066" smallUrl="/hpstest/photos/dogs/small/dog_1_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_1_256x256.jpg" caption="Hello" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog_2_1600x1067.jpg" largeDim="1600x1067" smallUrl="/hpstest/photos/dogs/small/dog_2_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_2_256x256.jpg" caption="yes this is dog" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog-3_1600x1040.jpg" largeDim="1600x1040" smallUrl="/hpstest/photos/dogs/small/dog-3_800x520.jpg" smallDim="800x520" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog-3_256x256.jpg" caption="" copyright="copy" >}} + +{{< /wrap >}}""" + +TEST_ALBUM_MARKDOWN_2 = """\ ++++ +title = "dogs" +date = "" + +cover = "/hpstest/photos/dogs/coverimage.jpg" ++++ + +{{< wrap >}} +{{< photo href="/hpstest/photos/dogs/large/dog_1_1600x1066.jpg" largeDim="1600x1066" smallUrl="/hpstest/photos/dogs/small/dog_1_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_1_256x256.jpg" caption="Hello" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog_2_1600x1067.jpg" largeDim="1600x1067" smallUrl="/hpstest/photos/dogs/small/dog_2_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_2_256x256.jpg" caption="yes this is dog" copyright="copy" >}} + +{{< /wrap >}}""" + +TEST_ALBUM_MARKDOWN_3 = """\ ++++ +title = "dogs" +date = "" + +cover = "/hpstest/photos/dogs/coverimage.jpg" ++++ + +{{< wrap >}} +{{< photo href="/hpstest/photos/dogs/large/dog_1_1600x1066.jpg" largeDim="1600x1066" smallUrl="/hpstest/photos/dogs/small/dog_1_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_1_256x256.jpg" caption="Hello" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog_2_1600x1067.jpg" largeDim="1600x1067" smallUrl="/hpstest/photos/dogs/small/dog_2_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_2_256x256.jpg" caption="yes this is dog" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog-3_1600x1040.jpg" largeDim="1600x1040" smallUrl="/hpstest/photos/dogs/small/dog-3_800x520.jpg" smallDim="800x520" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog-3_256x256.jpg" caption="" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/cat-1_1600x1068.jpg" largeDim="1600x1068" smallUrl="/hpstest/photos/dogs/small/cat-1_800x534.jpg" smallDim="800x534" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/cat-1_256x256.jpg" caption="" copyright="copy" >}} + +{{< /wrap >}}""" + +TEST_ALBUM_MARKDOWN_4 = """\ ++++ +title = "dogs" +date = "" + +cover = "/hpstest/photos/dogs/coverimage.jpg" ++++ + +{{< wrap >}} +{{< photo href="/hpstest/photos/dogs/large/dog_1_1600x1066.jpg" largeDim="1600x1066" smallUrl="/hpstest/photos/dogs/small/dog_1_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_1_256x256.jpg" caption="Hello" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog_2_1600x1067.jpg" largeDim="1600x1067" smallUrl="/hpstest/photos/dogs/small/dog_2_800x533.jpg" smallDim="800x533" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog_2_256x256.jpg" caption="yes this is dog" copyright="copy" >}} + +{{< photo href="/hpstest/photos/dogs/large/dog-3_1600x1040.jpg" largeDim="1600x1040" smallUrl="/hpstest/photos/dogs/small/dog-3_800x520.jpg" smallDim="800x520" alt="" thumbSize="256x256" thumbUrl="/hpstest/photos/dogs/thumb/dog-3_256x256.jpg" caption="" copyright="copy" >}} + +{{< /wrap >}}""" + +#### + +TEST_ALBUM_YAML_1 = """\ +--- +title: dogs +album_date: +properties: +copyright: copy +coverimage: dog-1.jpg +creation_time: +modification_time: "2021-03-20T16:41:06+00:00" + +photos: +- file: dog-1.jpg + name: dog 1 + alt: + caption: > + Hello + +- file: dog-2.jpg + name: dog 2 + alt: + caption: > + yes this is dog + +- file: dog-3.jpg + name: dog-3.jpg + alt: + caption: + +hashes: +- file: dog-1.jpg + hash: sha256:c2fdf14c548a08032fd06e6036197fc7e9c262e6d06fac40e54ec5dd2ce6912f + +- file: dog-2.jpg + hash: sha256:b09c4ddbbcf053d521539a8a498f7b745313561371dcbb9500687951f2dc7b4e + +- file: dog-3.jpg + hash: sha256:bc6c7fb353d01edfbcd2f707e202d3d31150fdc3faf6f9580c36cb2e0e2a0b81 +""" + +TEST_ALBUM_YAML_2 = """\ +--- +title: dogs +album_date: +properties: +copyright: copy +coverimage: dog-1.jpg +creation_time: +modification_time: "2021-03-20T16:41:06+00:00" + +photos: +- file: dog-1.jpg + name: dog 1 + alt: + caption: > + Hello + +- file: dog-2.jpg + name: dog 2 + alt: + caption: > + yes this is dog + +hashes: +- file: dog-1.jpg + hash: sha256:c2fdf14c548a08032fd06e6036197fc7e9c262e6d06fac40e54ec5dd2ce6912f + +- file: dog-2.jpg + hash: sha256:b09c4ddbbcf053d521539a8a498f7b745313561371dcbb9500687951f2dc7b4e +""" + +TEST_ALBUM_YAML_3 = """\ +--- +title: dogs +album_date: +properties: +copyright: copy +coverimage: dog-1.jpg +creation_time: +modification_time: "2021-03-20T16:41:06+00:00" + +photos: +- file: dog-1.jpg + name: dog 1 + alt: + caption: > + Hello + +- file: dog-2.jpg + name: dog 2 + alt: + caption: > + yes this is dog + +- file: dog-3.jpg + name: dog-3.jpg + alt: + caption: + +- file: cat-1.jpg + name: cat-1.jpg + alt: + caption: + +hashes: +- file: dog-1.jpg + hash: sha256:c2fdf14c548a08032fd06e6036197fc7e9c262e6d06fac40e54ec5dd2ce6912f + +- file: dog-2.jpg + hash: sha256:b09c4ddbbcf053d521539a8a498f7b745313561371dcbb9500687951f2dc7b4e + +- file: dog-3.jpg + hash: sha256:bc6c7fb353d01edfbcd2f707e202d3d31150fdc3faf6f9580c36cb2e0e2a0b81 + +- file: cat-1.jpg + hash: sha256:628569ade5866f91a765409a37b602e3a87f09ddb3fd3bb7a0b1dfbeb4362669 +""" + +TEST_ALBUM_YAML_4 = """\ +--- +title: dogs +album_date: +properties: +copyright: copy +coverimage: dog-1.jpg +creation_time: +modification_time: "2021-03-20T16:41:06+00:00" + +photos: +- file: dog-1.jpg + name: dog 1 + alt: + caption: > + Hello + +- file: dog-2.jpg + name: dog 2 + alt: + caption: > + yes this is dog + +- file: dog-3.jpg + name: dog-3.jpg + alt: + caption: + +hashes: +- file: dog-1.jpg + hash: sha256:628569ade5866f91a765409a37b602e3a87f09ddb3fd3bb7a0b1dfbeb4362669 + +- file: dog-2.jpg + hash: sha256:b09c4ddbbcf053d521539a8a498f7b745313561371dcbb9500687951f2dc7b4e + +- file: dog-3.jpg + hash: sha256:bc6c7fb353d01edfbcd2f707e202d3d31150fdc3faf6f9580c36cb2e0e2a0b81 +""" diff --git a/tests/data/cats/cat-1.jpg b/tests/data/cats/cat-1.jpg new file mode 100644 index 0000000..2fcafda Binary files /dev/null and b/tests/data/cats/cat-1.jpg differ diff --git a/tests/data/cats/cat-2.jpg b/tests/data/cats/cat-2.jpg new file mode 100644 index 0000000..f97e650 Binary files /dev/null and b/tests/data/cats/cat-2.jpg differ diff --git a/tests/data/cats/cat-3.jpg b/tests/data/cats/cat-3.jpg new file mode 100644 index 0000000..04fe4fb Binary files /dev/null and b/tests/data/cats/cat-3.jpg differ diff --git a/tests/data/dogs/dog-1.jpg b/tests/data/dogs/dog-1.jpg new file mode 100644 index 0000000..dace8e8 Binary files /dev/null and b/tests/data/dogs/dog-1.jpg differ diff --git a/tests/data/dogs/dog-2.jpg b/tests/data/dogs/dog-2.jpg new file mode 100644 index 0000000..2752bc1 Binary files /dev/null and b/tests/data/dogs/dog-2.jpg differ diff --git a/tests/data/dogs/dog-3.jpg b/tests/data/dogs/dog-3.jpg new file mode 100644 index 0000000..51a8919 Binary files /dev/null and b/tests/data/dogs/dog-3.jpg differ diff --git a/tests/test.jpg b/tests/test.jpg deleted file mode 100644 index 8d22ac5..0000000 Binary files a/tests/test.jpg and /dev/null differ diff --git a/tests/test_album.py b/tests/test_album.py new file mode 100644 index 0000000..7a128f1 --- /dev/null +++ b/tests/test_album.py @@ -0,0 +1,309 @@ +""" +Unit tests for the Album class + +""" + +import os +import shutil +import tempfile +import unittest + +from hugophotoswipe.album import Album +from hugophotoswipe.config import settings +from hugophotoswipe.photo import Photo + +from _constants import TEST_ALBUM_MARKDOWN_1 +from _constants import TEST_ALBUM_MARKDOWN_2 +from _constants import TEST_ALBUM_MARKDOWN_3 +from _constants import TEST_ALBUM_MARKDOWN_4 +from _constants import TEST_ALBUM_YAML_1 +from _constants import TEST_ALBUM_YAML_2 +from _constants import TEST_ALBUM_YAML_3 +from _constants import TEST_ALBUM_YAML_4 + + +class AlbumTestCase(unittest.TestCase): + def setUp(self): + self._here = os.path.dirname(os.path.realpath(__file__)) + self._tmpdir = tempfile.mkdtemp(prefix="hps_album_") + self._album_dir = os.path.join(self._tmpdir, "dogs") + + self._output_dir = os.path.join(self._tmpdir, "output") + os.makedirs(self._album_dir) + + self._markdown_dir = os.path.join(self._tmpdir, "markdown") + os.makedirs(self._markdown_dir) + + settings.__init__(**dict()) + setattr(settings, "output_dir", self._output_dir) + setattr(settings, "markdown_dir", self._markdown_dir) + setattr(settings, "url_prefix", "/hpstest/photos") + + def tearDown(self): + shutil.rmtree(self._tmpdir) + + def _make_test_album(self, album_dir): + album_file = os.path.join(album_dir, settings.album_file) + with open(album_file, "w") as fp: + fp.write("---\n") + fp.write("title: dogs\n") + fp.write("copyright: copy\n") + fp.write("coverimage: dog-1.jpg\n") + fp.write("\n") + fp.write("photos:\n") + fp.write("- file: dog-1.jpg\n") + fp.write(" name: dog 1\n") + fp.write(" caption: Hello\n") + fp.write("- file: dog-2.jpg\n") + fp.write(" name: dog 2\n") + fp.write(" caption: yes this is dog\n") + fp.write("- file: dog-3.jpg\n") + fp.write(" name:\n") + photos_dir = os.path.join(self._album_dir, "photos") + os.makedirs(photos_dir) + + test_dog_dir = os.path.join(self._here, "data", "dogs") + test_dog_files = os.listdir(test_dog_dir) + test_dogs = [os.path.join(test_dog_dir, d) for d in test_dog_files] + for d in test_dogs: + shutil.copy(d, photos_dir) + + def test_name(self): + album = Album(album_dir=self._album_dir) + self.assertEqual(album.name, "dogs") + + def test_markdown_file(self): + album = Album(album_dir=self._album_dir) + exp_md = os.path.join(self._markdown_dir, "dogs.md") + self.assertEqual(album.markdown_file, exp_md) + + def test_load_empty(self): + album_file = os.path.join(self._album_dir, settings.album_file) + with open(album_file, "w") as fp: + fp.write("---\n") + fp.write("title: dogs\n") + album = Album.load(self._album_dir) + self.assertEqual(album._album_dir, self._album_dir) + self.assertEqual(album.title, "dogs") + self.assertEqual( + album.cover_path, + os.path.join(self._output_dir, "dogs", "coverimage.jpg"), + ) + self.assertEqual(album.photos, []) + + def test_load_album(self): + self._make_test_album(self._album_dir) + + album = Album.load(self._album_dir) + self.assertEqual(len(album.photos), 3) + for i, p in enumerate(album.photos): + self.assertIsInstance(p, Photo) + self.assertEqual(p.album_name, "dogs") + if i == 2: + # Check if missing name is handled properly + self.assertEqual(p.name, "dog-3.jpg") + else: + self.assertEqual(p.name, f"dog {i+1}") + exp_path = os.path.join( + self._album_dir, "photos", f"dog-{i+1}.jpg" + ) + self.assertEqual(p.original_path, exp_path) + self.assertEqual(p.copyright, "copy") + + def test_clean(self): + self._make_test_album(self._album_dir) + + album = Album.load(self._album_dir) + album.update() + + md = album.markdown_file + self.assertTrue(os.path.exists(md)) + out_album = os.path.join(self._output_dir, "dogs") + out_large = os.path.join(out_album, "large") + out_small = os.path.join(out_album, "small") + out_thumb = os.path.join(out_album, "thumb") + self.assertTrue(os.path.exists(out_album)) + self.assertTrue(os.path.exists(out_large)) + self.assertTrue(os.path.exists(out_small)) + self.assertTrue(os.path.exists(out_thumb)) + self.assertEqual(len(os.listdir(out_large)), 3) + self.assertEqual(len(os.listdir(out_small)), 3) + self.assertEqual(len(os.listdir(out_thumb)), 3) + + album.clean(force=True) + self.assertFalse(os.path.exists(md)) + self.assertFalse(os.path.exists(out_album)) + + def test_create_markdown(self): + self._make_test_album(self._album_dir) + album = Album.load(self._album_dir) + album.create_markdown() + self.assertTrue(os.path.exists(album.markdown_file)) + with open(album.markdown_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_MARKDOWN_1) + + def test_dump(self): + self._make_test_album(self._album_dir) + album = Album.load(self._album_dir) + album.dump(modification_time="2021-03-20T16:41:06+00:00") + + album_file = os.path.join(self._album_dir, settings.album_file) + self.assertTrue(os.path.exists(album_file + ".bak")) + with open(album_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_YAML_1) + + def test_update_1(self): + self._make_test_album(self._album_dir) + album = Album.load(self._album_dir) + album.update(modification_time="2021-03-20T16:41:06+00:00") + + # Check markdown and album files exist + with open(album.markdown_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_MARKDOWN_1) + with open(album._album_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_YAML_1) + + # Check Output files exist + album_out = os.path.join(self._output_dir, "dogs") + cover = os.path.join(album_out, "coverimage.jpg") + resized_files = { + "large": [ + "dog_1_1600x1066.jpg", + "dog_2_1600x1067.jpg", + "dog-3_1600x1040.jpg", + ], + "small": [ + "dog_1_800x533.jpg", + "dog_2_800x533.jpg", + "dog-3_800x520.jpg", + ], + "thumb": [ + "dog_1_256x256.jpg", + "dog_2_256x256.jpg", + "dog-3_256x256.jpg", + ], + } + for size in resized_files: + for file in resized_files[size]: + filename = os.path.join(album_out, size, file) + self.assertTrue(os.path.exists(filename)) + self.assertTrue(os.path.exists(cover)) + + def test_update_2(self): + self._make_test_album(self._album_dir) + os.unlink(os.path.join(self._album_dir, "photos", "dog-3.jpg")) + album = Album.load(self._album_dir) + album.update(modification_time="2021-03-20T16:41:06+00:00") + + # Check markdown and album files exist + with open(album.markdown_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_MARKDOWN_2) + with open(album._album_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_YAML_2) + + # Check Output files exist + album_out = os.path.join(self._output_dir, "dogs") + cover = os.path.join(album_out, "coverimage.jpg") + resized_files = { + "large": [ + "dog_1_1600x1066.jpg", + "dog_2_1600x1067.jpg", + ], + "small": [ + "dog_1_800x533.jpg", + "dog_2_800x533.jpg", + ], + "thumb": [ + "dog_1_256x256.jpg", + "dog_2_256x256.jpg", + ], + } + for size in resized_files: + for file in resized_files[size]: + filename = os.path.join(album_out, size, file) + self.assertTrue(os.path.exists(filename)) + self.assertTrue(os.path.exists(cover)) + + def test_update_3(self): + self._make_test_album(self._album_dir) + cat_image = os.path.join(self._here, "data", "cats", "cat-1.jpg") + shutil.copy(cat_image, os.path.join(self._album_dir, "photos")) + + album = Album.load(self._album_dir) + album.update(modification_time="2021-03-20T16:41:06+00:00") + + # Check markdown and album files exist + with open(album.markdown_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_MARKDOWN_3) + with open(album._album_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_YAML_3) + + # Check Output files exist + album_out = os.path.join(self._output_dir, "dogs") + cover = os.path.join(album_out, "coverimage.jpg") + resized_files = { + "large": [ + "dog_1_1600x1066.jpg", + "dog_2_1600x1067.jpg", + "dog-3_1600x1040.jpg", + "cat-1_1600x1068.jpg", + ], + "small": [ + "dog_1_800x533.jpg", + "dog_2_800x533.jpg", + "dog-3_800x520.jpg", + "cat-1_800x534.jpg", + ], + "thumb": [ + "dog_1_256x256.jpg", + "dog_2_256x256.jpg", + "dog-3_256x256.jpg", + "cat-1_256x256.jpg", + ], + } + for size in resized_files: + for file in resized_files[size]: + filename = os.path.join(album_out, size, file) + self.assertTrue(os.path.exists(filename)) + self.assertTrue(os.path.exists(cover)) + + def test_update_4(self): + self._make_test_album(self._album_dir) + album = Album.load(self._album_dir) + album.update(modification_time="2021-03-20T16:41:06+00:00") + cat1 = os.path.join(self._here, "data", "cats", "cat-1.jpg") + dog1 = os.path.join(self._album_dir, "photos", "dog-1.jpg") + shutil.copy(cat1, dog1) + album.update(modification_time="2021-03-20T16:41:06+00:00") + + # Check markdown and album files exist + with open(album.markdown_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_MARKDOWN_4) + with open(album._album_file, "r") as fp: + self.assertEqual(fp.read(), TEST_ALBUM_YAML_4) + + # Check Output files exist + album_out = os.path.join(self._output_dir, "dogs") + cover = os.path.join(album_out, "coverimage.jpg") + resized_files = { + "large": [ + "dog_1_1600x1066.jpg", + "dog_2_1600x1067.jpg", + "dog-3_1600x1040.jpg", + ], + "small": [ + "dog_1_800x533.jpg", + "dog_2_800x533.jpg", + "dog-3_800x520.jpg", + ], + "thumb": [ + "dog_1_256x256.jpg", + "dog_2_256x256.jpg", + "dog-3_256x256.jpg", + ], + } + for size in resized_files: + for file in resized_files[size]: + filename = os.path.join(album_out, size, file) + self.assertTrue(os.path.exists(filename)) + self.assertTrue(os.path.exists(cover)) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..3b2eb49 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +import os +import tempfile +import unittest + +from hugophotoswipe.config import Settings +from hugophotoswipe.config import load_settings + + +class ConfigTestCase(unittest.TestCase): + def setUp(self): + temp_fd, self._tempfile = tempfile.mkstemp("test.yml") + os.close(temp_fd) + + def tearDown(self): + os.unlink(self._tempfile) + + def test_config_1(self): + settings = Settings() + settings.dump(settings_filename=self._tempfile) + + line = lambda fp: fp.readline().strip() + with open(self._tempfile, "r") as fp: + self.assertEqual(line(fp), "---") + self.assertEqual(line(fp), "album_file: album.yml") + self.assertEqual(line(fp), "cover_filename: coverimage.jpg") + self.assertEqual(line(fp), "dim_max_cover: 600x600") + self.assertEqual(line(fp), "dim_max_large: 1600") + self.assertEqual(line(fp), "dim_max_small: 800") + self.assertEqual(line(fp), "dim_max_thumb: 256x256") + self.assertEqual(line(fp), "dirname_large: large") + self.assertEqual(line(fp), "dirname_small: small") + self.assertEqual(line(fp), "dirname_thumb: thumb") + self.assertEqual(line(fp), "jpeg_optimize: False") + self.assertEqual(line(fp), "jpeg_progressive: False") + self.assertEqual(line(fp), "jpeg_quality: 75") + self.assertEqual(line(fp), "markdown_dir:") + self.assertEqual(line(fp), "output_dir:") + self.assertEqual(line(fp), "output_format: jpg") + self.assertEqual(line(fp), "photo_dir: photos") + self.assertEqual(line(fp), "smartcrop_js_path:") + self.assertEqual(line(fp), "url_prefix:") + self.assertEqual(line(fp), "use_smartcrop_js: False") + + def test_config_2(self): + settings = Settings( + **dict( + dim_max_small=500, + jpeg_progressive=True, + photo_dir="photo_files", + ) + ) + settings.dump(settings_filename=self._tempfile) + + line = lambda fp: fp.readline().strip() + with open(self._tempfile, "r") as fp: + self.assertEqual(line(fp), "---") + self.assertEqual(line(fp), "album_file: album.yml") + self.assertEqual(line(fp), "cover_filename: coverimage.jpg") + self.assertEqual(line(fp), "dim_max_cover: 600x600") + self.assertEqual(line(fp), "dim_max_large: 1600") + self.assertEqual(line(fp), "dim_max_small: 500") + self.assertEqual(line(fp), "dim_max_thumb: 256x256") + self.assertEqual(line(fp), "dirname_large: large") + self.assertEqual(line(fp), "dirname_small: small") + self.assertEqual(line(fp), "dirname_thumb: thumb") + self.assertEqual(line(fp), "jpeg_optimize: False") + self.assertEqual(line(fp), "jpeg_progressive: True") + self.assertEqual(line(fp), "jpeg_quality: 75") + self.assertEqual(line(fp), "markdown_dir:") + self.assertEqual(line(fp), "output_dir:") + self.assertEqual(line(fp), "output_format: jpg") + self.assertEqual(line(fp), "photo_dir: photo_files") + self.assertEqual(line(fp), "smartcrop_js_path:") + self.assertEqual(line(fp), "url_prefix:") + self.assertEqual(line(fp), "use_smartcrop_js: False") + + def test_config_3(self): + settings = Settings() + self.assertFalse(settings.validate()) + + def test_config_4(self): + settings = Settings( + **dict( + markdown_dir="/path/to/markdown", + output_dir="/path/to/output", + use_smartcrop_js=True, + smartcrop_js_path="/path/to/smartcrop.js", + ) + ) + self.assertTrue(settings.validate()) + + def test_config_5(self): + self.maxDiff = None + + with open(self._tempfile, "w") as fp: + fp.write("---\n") + fp.write("album_file: album.yml\n") + fp.write("cover_filename: coverimage.jpg\n") + fp.write("dim_max_cover: 600x600\n") + fp.write("dim_max_large: 1600\n") + fp.write("dim_max_small: 500\n") + fp.write("dim_max_thumb: 256x256\n") + fp.write("dirname_large: large\n") + fp.write("dirname_small: small\n") + fp.write("dirname_thumb: thumb\n") + fp.write("jpeg_optimize: False\n") + fp.write("jpeg_progressive: True\n") + fp.write("jpeg_quality: 90\n") + fp.write("markdown_dir: /path/to/markdown\n") + fp.write("output_dir: /path/to/output\n") + fp.write("output_format: jpg\n") + fp.write("photo_dir: photo_files\n") + fp.write("smartcrop_js_path:\n") + fp.write("url_prefix:\n") + fp.write("use_smartcrop_js: True\n") + + settings = load_settings(settings_filename=self._tempfile) + self.assertEqual(settings.album_file, "album.yml") + self.assertEqual(settings.cover_filename, "coverimage.jpg") + self.assertEqual(settings.dim_max_cover, "600x600") + self.assertEqual(settings.dim_max_large, "1600") + self.assertEqual(settings.dim_max_small, "500") + self.assertEqual(settings.dim_max_thumb, "256x256") + self.assertEqual(settings.dirname_large, "large") + self.assertEqual(settings.dirname_small, "small") + self.assertEqual(settings.dirname_thumb, "thumb") + self.assertEqual(settings.jpeg_optimize, False) + self.assertEqual(settings.jpeg_progressive, True) + self.assertEqual(settings.jpeg_quality, 90) + self.assertEqual(settings.markdown_dir, "/path/to/markdown") + self.assertEqual(settings.output_dir, "/path/to/output") + self.assertEqual(settings.output_format, "jpg") + self.assertEqual(settings.photo_dir, "photo_files") + self.assertEqual(settings.smartcrop_js_path, None) + self.assertEqual(settings.url_prefix, None) + self.assertEqual(settings.use_smartcrop_js, True) + + self.assertEqual(settings.verbose, False) + self.assertEqual(settings.fast, False) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_hugophotoswipe.py b/tests/test_hugophotoswipe.py new file mode 100644 index 0000000..d860bdf --- /dev/null +++ b/tests/test_hugophotoswipe.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +import os +import shutil +import tempfile +import unittest + +from hugophotoswipe.hugophotoswipe import HugoPhotoSwipe + + +class HugoPhotoSwipeTestCase(unittest.TestCase): + def setUp(self): + self._here = os.path.dirname(os.path.realpath(__file__)) + self._tmpdir = tempfile.mkdtemp(prefix="hps_test_") + os.chdir(self._tmpdir) + + def tearDown(self): + os.chdir(self._here) + shutil.rmtree(self._tmpdir) + + def test_new(self): + hps = HugoPhotoSwipe(albums=[]) + hps.new(name="test_album") + self.assertTrue(os.path.exists("test_album")) + self.assertTrue(os.path.isdir("test_album")) + + photos_dir = os.path.join("test_album", "photos") + self.assertTrue(os.path.exists(photos_dir)) + self.assertTrue(os.path.isdir(photos_dir)) + + album_file = os.path.join("test_album", "album.yml") + self.assertTrue(os.path.exists(album_file)) + with open(album_file, "r") as fp: + self.assertEqual(fp.readline(), "---\n") + self.assertEqual(fp.readline(), "title:\n") + self.assertEqual(fp.readline(), "album_date:\n") + self.assertEqual(fp.readline(), "properties:\n") + self.assertEqual(fp.readline(), "copyright:\n") + self.assertEqual(fp.readline(), "coverimage:\n") + self.assertTrue(fp.readline().startswith("creation_time: ")) + self.assertTrue(fp.readline().startswith("modification_time: ")) + self.assertEqual(fp.readline(), "\n") + self.assertEqual(fp.readline(), "photos:\n") + self.assertEqual(fp.readline(), "hashes:") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_photo.py b/tests/test_photo.py index 8a7b77e..33453de 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -3,59 +3,201 @@ """ -from __future__ import print_function, division - import os +import shutil +import tempfile import unittest +from PIL import Image + +from hugophotoswipe.config import settings from hugophotoswipe.photo import Photo -from hugophotoswipe.conf import settings -class PhotoTestCase(unittest.TestCase): +class PhotoTestCase(unittest.TestCase): def setUp(self): - pth = os.path.realpath(__file__) - dr = os.path.dirname(pth) - tst = os.path.join(dr, 'test.jpg') - self.photo = Photo(album_name='test_album', original_path=tst, - name='test_image', alt='Alt text', caption='caption text', - copyright=None) + here = os.path.dirname(os.path.realpath(__file__)) + test_file = os.path.join(here, "data", "dogs", "dog-1.jpg") + self.photo = Photo( + album_name="test_album", + original_path=test_file, + name="dog_1", + alt="Alt text", + caption="caption text", + copyright=None, + ) + self._tmpdir = tempfile.mkdtemp(prefix="hps_photo_") + # Reset settings after each test + settings.__init__(**dict()) + + def tearDown(self): + shutil.rmtree(self._tmpdir) def test_resize_dims(self): - """ [Photo]: Test resize dimensions """ + """Test resize dimensions """ # testing all possible modes pairs = [ - ('dim_max_large', 'large'), - ('dim_max_small', 'small'), - ('dim_max_thumb', 'thumb'), - ('dim_max_cover', 'cover') - ] + ("dim_max_large", "large"), + ("dim_max_small", "small"), + ("dim_max_thumb", "thumb"), + ("dim_max_cover", "cover"), + ] for setting_name, mode in pairs: - # 1040 = 1600 / 1800 * 1170 - setattr(settings, setting_name, '1600') + # 1600 * 1429 / 2144 = 1066 + setattr(settings, setting_name, "1600") dims = self.photo.resize_dims(mode) - self.assertEqual(dims, (1600, 1040)) + self.assertEqual(dims, (1600, 1066)) self.assertIsInstance(dims[0], int) self.assertIsInstance(dims[1], int) - # 1500 / 1800 * 1170 = 975 - setattr(settings, setting_name, '1500x') + # 1500 * 1429 / 2144 = 1000 + setattr(settings, setting_name, "1500x") dims = self.photo.resize_dims(mode) - self.assertEqual(dims, (1500, 975)) + self.assertEqual(dims, (1500, 1000)) self.assertIsInstance(dims[0], int) self.assertIsInstance(dims[1], int) - # 1000 / 1170 * 1800 = 1538 - setattr(settings, setting_name, 'x1000') + # 900 * 2144 / 1429 = 1500 + setattr(settings, setting_name, "x900") dims = self.photo.resize_dims(mode) - self.assertEqual(dims, (1538, 1000)) + self.assertEqual(dims, (1350, 900)) self.assertIsInstance(dims[0], int) self.assertIsInstance(dims[1], int) # exact dimensions - setattr(settings, setting_name, '800x600') + setattr(settings, setting_name, "800x600") dims = self.photo.resize_dims(mode) self.assertEqual(dims, (800, 600)) self.assertIsInstance(dims[0], int) self.assertIsInstance(dims[1], int) + + def test_clean_name(self): + self.assertEqual(self.photo.clean_name, "dog_1") + + def test_paths(self): + modes = ["large", "small", "thumb"] + fnames = [ + "dog_1_1600x1066.jpg", + "dog_1_800x533.jpg", + "dog_1_256x256.jpg", + ] + + output_dir = os.path.join(self._tmpdir, "output") + setattr(settings, "output_dir", output_dir) + + for mode, fname in zip(modes, fnames): + with self.subTest(mode=mode): + exp = os.path.join(output_dir, "test_album", mode, fname) + self.assertEqual(getattr(self.photo, mode + "_path"), exp) + + def test_filename(self): + self.assertEqual(self.photo.filename, "dog-1.jpg") + + def test_extension(self): + self.assertEqual(self.photo.extension, ".jpg") + + def test_clean_caption(self): + self.assertEqual(self.photo.clean_caption, ">\n caption text") + + def test_shortcode(self): + output_dir = os.path.join(self._tmpdir, "output") + url_prefix = "https://example.com/albums/" + album_prefix = "/".join([url_prefix, "test_album"]) + + setattr(settings, "output_dir", output_dir) + setattr(settings, "url_prefix", url_prefix) + setattr(settings, "dim_max_large", "1600") + setattr(settings, "dim_max_small", "800") + setattr(settings, "dim_max_thumb", "256x256") + + self.maxDiff = None + + exp_large = "/".join([album_prefix, "large", "dog_1_1600x1066.jpg"]) + exp_small = "/".join([album_prefix, "small", "dog_1_800x533.jpg"]) + exp_thumb = "/".join([album_prefix, "thumb", "dog_1_256x256.jpg"]) + exp_alt = "Alt text" + exp_cap = "caption text" + exp_copy = "" + + expected = ( + f'{{{{< photo href="{exp_large}" largeDim="1600x1066" ' + f'smallUrl="{exp_small}" smallDim="800x533" alt="{exp_alt}" ' + f'thumbSize="256x256" thumbUrl="{exp_thumb}" ' + f'caption="{exp_cap}" copyright="{exp_copy}" >}}}}' + ) + self.assertEqual(self.photo.shortcode, expected) + + def test_width(self): + self.assertEqual(self.photo.width, 2144) + + def test_height(self): + self.assertEqual(self.photo.height, 1429) + + def test_rescaled(self): + output_dir = os.path.join(self._tmpdir, "output") + setattr(settings, "output_dir", output_dir) + setattr(settings, "dim_max_large", "1600") + setattr(settings, "dim_max_small", "800") + + modes = ["large", "small"] + sizes = [(1600, 1066), (800, 533)] + + for mode, size in zip(modes, sizes): + with self.subTest(mode=mode): + out_path = self.photo.create_rescaled(mode) + # open the image and check the size + img = Image.open(out_path) + self.assertEqual(img.width, size[0]) + self.assertEqual(img.height, size[1]) + img.close() + + def test_rescaled_png(self): + output_dir = os.path.join(self._tmpdir, "output") + setattr(settings, "output_format", "png") + setattr(settings, "output_dir", output_dir) + setattr(settings, "dim_max_large", "1600") + setattr(settings, "dim_max_small", "800") + + modes = ["large", "small"] + sizes = [(1600, 1066), (800, 533)] + + for mode, size in zip(modes, sizes): + with self.subTest(mode=mode): + out_path = self.photo.create_rescaled(mode) + # open the image and check the size + img = Image.open(out_path) + self.assertEqual(img.format, "PNG") + self.assertEqual(img.width, size[0]) + self.assertEqual(img.height, size[1]) + img.close() + + def test_thumb(self): + output_dir = os.path.join(self._tmpdir, "output") + os.makedirs(output_dir) + setattr(settings, "output_dir", output_dir) + setattr(settings, "dim_max_thumb", "128x128") + setattr(settings, "dim_max_cover", "400x400") + setattr(settings, "use_smartcrop_js", False) + + modes = ["thumb", "cover"] + sizes = [(128, 128), (400, 400)] + names = ["thumbnail.jpg", "cover_image.jpg"] + + for mode, size, name in zip(modes, sizes, names): + with self.subTest(mode=mode): + pth = os.path.join(output_dir, name) + out_path = self.photo.create_thumb(mode=mode, pth=pth) + self.assertEqual(pth, out_path) + img = Image.open(out_path) + self.assertEqual(img.width, size[0]) + self.assertEqual(img.height, size[1]) + img.close() + + def test_sha256sum(self): + self.assertEqual(self.photo.sha256sum(), +"c2fdf14c548a08032fd06e6036197fc7e9c262e6d06fac40e54ec5dd2ce6912f") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..5fd4cec --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import os +import tempfile +import unittest + +from hugophotoswipe.utils import yaml_field_to_file + + +class UtilsTestCase(unittest.TestCase): + def _write_read_yaml_field(self, *args, **kwargs): + temp_fd, temp_filename = tempfile.mkstemp("test.yml") + with os.fdopen(temp_fd, "w") as fp: + yaml_field_to_file(fp, *args, **kwargs) + with open(temp_filename, "r") as fp: + content = fp.read() + return content + + def test_yaml_field_to_file(self): + out = self._write_read_yaml_field(None, "key") + self.assertEqual("key:\n", out) + + out = self._write_read_yaml_field(None, "key", indent=" ") + self.assertEqual(" key:\n", out) + + out = self._write_read_yaml_field("", "key") + self.assertEqual("key:\n", out) + + out = self._write_read_yaml_field("abc", "key") + self.assertEqual("key: abc\n", out) + + out = self._write_read_yaml_field("abc", "field", indent=" ") + self.assertEqual(" field: abc\n", out) + + out = self._write_read_yaml_field(123, "key") + self.assertEqual("key: 123\n", out) + + out = self._write_read_yaml_field( + 123, "key", indent=" ", force_string=True + ) + self.assertEqual(' key: "123"\n', out) + + +if __name__ == "__main__": + unittest.main()