Browse Source

Normalize strings

pull/492/head
Simon Conseil 3 years ago
parent
commit
66bd01106b
  1. 26
      docs/changelog.py
  2. 44
      docs/conf.py
  3. 1
      pyproject.toml
  4. 122
      src/sigal/__init__.py
  5. 266
      src/sigal/gallery.py
  6. 160
      src/sigal/image.py
  7. 20
      src/sigal/log.py
  8. 8
      src/sigal/plugins/adjust.py
  9. 46
      src/sigal/plugins/compress_assets.py
  10. 16
      src/sigal/plugins/copyright.py
  11. 12
      src/sigal/plugins/encrypt/encrypt.py
  12. 42
      src/sigal/plugins/extended_caching.py
  13. 36
      src/sigal/plugins/feeds.py
  14. 82
      src/sigal/plugins/nonmedia_files.py
  15. 14
      src/sigal/plugins/titleregexp.py
  16. 24
      src/sigal/plugins/watermark.py
  17. 20
      src/sigal/plugins/zip_gallery.py
  18. 160
      src/sigal/settings.py
  19. 20
      src/sigal/signals.py
  20. 4
      src/sigal/templates/sigal.conf.py
  21. 24
      src/sigal/utils.py
  22. 6
      src/sigal/video.py
  23. 10
      src/sigal/writer.py
  24. 8
      tests/conftest.py
  25. 40
      tests/test_cli.py
  26. 24
      tests/test_compress_assets_plugin.py
  27. 20
      tests/test_encrypt.py
  28. 24
      tests/test_extended_caching.py
  29. 426
      tests/test_gallery.py
  30. 162
      tests/test_image.py
  31. 22
      tests/test_plugins.py
  32. 32
      tests/test_settings.py
  33. 58
      tests/test_utils.py
  34. 58
      tests/test_video.py
  35. 40
      tests/test_zip.py

26
docs/changelog.py

@ -52,15 +52,15 @@ A total of %d pull requests were merged for this release.
def get_authors(revision_range):
pat = '^.*\\t(.*)$'
lst_release, cur_release = (r.strip() for r in revision_range.split('..'))
pat = "^.*\\t(.*)$"
lst_release, cur_release = (r.strip() for r in revision_range.split(".."))
# authors, in current release and previous to current release.
cur = set(re.findall(pat, this_repo.git.shortlog('-s', revision_range), re.M))
pre = set(re.findall(pat, this_repo.git.shortlog('-s', lst_release), re.M))
cur = set(re.findall(pat, this_repo.git.shortlog("-s", revision_range), re.M))
pre = set(re.findall(pat, this_repo.git.shortlog("-s", lst_release), re.M))
# Append '+' to new authors.
authors = [s + ' +' for s in cur - pre] + [s for s in cur & pre]
authors = [s + " +" for s in cur - pre] + [s for s in cur & pre]
authors.sort()
return authors
@ -69,7 +69,7 @@ def get_pull_requests(repo, revision_range):
prnums = []
# From regular merges
merges = this_repo.git.log('--oneline', '--merges', revision_range)
merges = this_repo.git.log("--oneline", "--merges", revision_range)
issues = re.findall("Merge pull request \\#(\\d*)", merges)
prnums.extend(int(s) for s in issues)
@ -79,9 +79,9 @@ def get_pull_requests(repo, revision_range):
# From fast forward squash-merges
commits = this_repo.git.log(
'--oneline', '--no-merges', '--first-parent', revision_range
"--oneline", "--no-merges", "--first-parent", revision_range
)
issues = re.findall('^.*\\(\\#(\\d+)\\)$', commits, re.M)
issues = re.findall("^.*\\(\\#(\\d+)\\)$", commits, re.M)
prnums.extend(int(s) for s in issues)
# get PR data from github repo
@ -91,10 +91,10 @@ def get_pull_requests(repo, revision_range):
def main(token, revision_range):
lst_release, cur_release = (r.strip() for r in revision_range.split('..'))
lst_release, cur_release = (r.strip() for r in revision_range.split(".."))
github = Github(token)
github_repo = github.get_repo('saimn/sigal')
github_repo = github.get_repo("saimn/sigal")
# document authors
authors = get_authors(revision_range)
@ -105,7 +105,7 @@ def main(token, revision_range):
print(author_msg % len(authors))
for s in authors:
print('* ' + s)
print("* " + s)
# document pull requests
pull_requests = get_pull_requests(github_repo, revision_range)
@ -132,7 +132,7 @@ if __name__ == "__main__":
from argparse import ArgumentParser
parser = ArgumentParser(description="Generate author/pr lists for release")
parser.add_argument('token', help='github access token')
parser.add_argument('revision_range', help='<revision>..<revision>')
parser.add_argument("token", help="github access token")
parser.add_argument("revision_range", help="<revision>..<revision>")
args = parser.parse_args()
main(args.token, args.revision_range)

44
docs/conf.py

@ -7,7 +7,7 @@ from pkg_resources import get_distribution
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('..'))
sys.path.append(os.path.abspath(".."))
# -- General configuration ----------------------------------------------------
@ -16,34 +16,34 @@ sys.path.append(os.path.abspath('..'))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'alabaster']
extensions = ["sphinx.ext.autodoc", "sphinx.ext.extlinks", "alabaster"]
extlinks = {'issue': ('https://github.com/saimn/sigal/issues/%s', '#%s')}
extlinks = {"issue": ("https://github.com/saimn/sigal/issues/%s", "#%s")}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = 'Sigal'
copyright = '2012-2020, Simon Conseil'
project = "Sigal"
copyright = "2012-2020, Simon Conseil"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
release = get_distribution('sigal').version
release = get_distribution("sigal").version
# for example take major/minor
version = '.'.join(release.split('.')[:2])
version = ".".join(release.split(".")[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
@ -58,7 +58,7 @@ version = '.'.join(release.split('.')[:2])
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
@ -72,7 +72,7 @@ exclude_patterns = ['_build']
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@ -81,20 +81,20 @@ pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
html_theme_path = [alabaster.get_path()]
html_theme = 'alabaster'
html_theme = "alabaster"
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'searchbox.html',
'donate.html',
"**": [
"about.html",
"navigation.html",
"searchbox.html",
"donate.html",
]
}
html_theme_options = {
# 'logo': 'logo.png',
'github_user': 'saimn',
'github_repo': 'sigal',
'description': "Yet another simple static gallery generator.",
"github_user": "saimn",
"github_repo": "sigal",
"description": "Yet another simple static gallery generator.",
# 'analytics_id': 'UA-18486793-2',
# 'travis_button': True,
}
@ -165,4 +165,4 @@ html_theme_options = {
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Sigaldoc'
htmlhelp_basename = "Sigaldoc"

1
pyproject.toml

@ -8,4 +8,3 @@ write_to = "src/sigal/version.py"
[tool.black]
line-length = 88
target-version = ['py38']
skip-string-normalization = true

122
src/sigal/__init__.py

@ -41,9 +41,9 @@ except ImportError:
# package is not installed
__version__ = None
__url__ = 'https://github.com/saimn/sigal'
__url__ = "https://github.com/saimn/sigal"
_DEFAULT_CONFIG_FILE = 'sigal.conf.py'
_DEFAULT_CONFIG_FILE = "sigal.conf.py"
@click.group()
@ -59,7 +59,7 @@ def main():
@main.command()
@argument('path', default=_DEFAULT_CONFIG_FILE)
@argument("path", default=_DEFAULT_CONFIG_FILE)
def init(path):
"""Copy a sample config file in the current directory (default to
'sigal.conf.py'), or use the provided 'path'."""
@ -70,44 +70,44 @@ def init(path):
from pkg_resources import resource_string
conf = resource_string(__name__, 'templates/sigal.conf.py')
conf = resource_string(__name__, "templates/sigal.conf.py")
with open(path, 'w', encoding='utf-8') as f:
f.write(conf.decode('utf8'))
with open(path, "w", encoding="utf-8") as f:
f.write(conf.decode("utf8"))
print(f"Sample config file created: {path}")
@main.command()
@argument('source', required=False)
@argument('destination', required=False)
@option('-f', '--force', is_flag=True, help="Force the reprocessing of existing images")
@option('-v', '--verbose', is_flag=True, help="Show all messages")
@argument("source", required=False)
@argument("destination", required=False)
@option("-f", "--force", is_flag=True, help="Force the reprocessing of existing images")
@option("-v", "--verbose", is_flag=True, help="Show all messages")
@option(
'-d',
'--debug',
"-d",
"--debug",
is_flag=True,
help=(
"Show all messages, including debug messages. Also raise "
"exception if an error happen when processing files."
),
)
@option('-q', '--quiet', is_flag=True, help="Show only error messages")
@option("-q", "--quiet", is_flag=True, help="Show only error messages")
@option(
'-c',
'--config',
"-c",
"--config",
default=_DEFAULT_CONFIG_FILE,
show_default=True,
help="Configuration file",
)
@option(
'-t',
'--theme',
"-t",
"--theme",
help=(
"Specify a theme directory, or a theme name for the themes included with Sigal"
),
)
@option('--title', help="Title of the gallery (overrides the title setting.")
@option('-n', '--ncpu', help="Number of cpu to use (default: all)")
@option("--title", help="Title of the gallery (overrides the title setting.")
@option("-n", "--ncpu", help="Number of cpu to use (default: all)")
def build(
source, destination, debug, verbose, quiet, force, config, theme, title, ncpu
):
@ -118,7 +118,7 @@ def build(
"""
if sum([debug, verbose, quiet]) > 1:
sys.exit('Only one option of debug, verbose and quiet should be used')
sys.exit("Only one option of debug, verbose and quiet should be used")
if debug:
level = logging.DEBUG
@ -139,14 +139,14 @@ def build(
start_time = time.time()
settings = read_settings(config)
for key in ('source', 'destination', 'theme'):
for key in ("source", "destination", "theme"):
arg = locals()[key]
if arg is not None:
settings[key] = os.path.abspath(arg)
logger.info("%12s : %s", key.capitalize(), settings[key])
if not settings['source'] or not os.path.isdir(settings['source']):
logger.error("Input directory not found: %s", settings['source'])
if not settings["source"] or not os.path.isdir(settings["source"]):
logger.error("Input directory not found: %s", settings["source"])
sys.exit(1)
# on windows os.path.relpath raises a ValueError if the two paths are on
@ -155,8 +155,8 @@ def build(
relative_check = True
try:
relative_check = os.path.relpath(
settings['destination'], settings['source']
).startswith('..')
settings["destination"], settings["source"]
).startswith("..")
except ValueError:
pass
@ -165,75 +165,75 @@ def build(
sys.exit(1)
if title:
settings['title'] = title
settings["title"] = title
locale.setlocale(locale.LC_ALL, settings['locale'])
locale.setlocale(locale.LC_ALL, settings["locale"])
init_plugins(settings)
gal = Gallery(settings, ncpu=ncpu, quiet=quiet)
gal.build(force=force)
# copy extra files
for src, dst in settings['files_to_copy']:
src = os.path.join(settings['source'], src)
dst = os.path.join(settings['destination'], dst)
logger.debug('Copy %s to %s', src, dst)
copy(src, dst, symlink=settings['orig_link'], rellink=settings['rel_link'])
for src, dst in settings["files_to_copy"]:
src = os.path.join(settings["source"], src)
dst = os.path.join(settings["destination"], dst)
logger.debug("Copy %s to %s", src, dst)
copy(src, dst, symlink=settings["orig_link"], rellink=settings["rel_link"])
stats = gal.stats
def format_stats(_type):
opt = [
"{} {}".format(stats[_type + '_' + subtype], subtype)
for subtype in ('skipped', 'failed')
if stats[_type + '_' + subtype] > 0
"{} {}".format(stats[_type + "_" + subtype], subtype)
for subtype in ("skipped", "failed")
if stats[_type + "_" + subtype] > 0
]
opt = ' ({})'.format(', '.join(opt)) if opt else ''
return f'{stats[_type]} {_type}s{opt}'
opt = " ({})".format(", ".join(opt)) if opt else ""
return f"{stats[_type]} {_type}s{opt}"
if not quiet:
stats_str = ''
types = sorted({t.rsplit('_', 1)[0] for t in stats})
stats_str = ""
types = sorted({t.rsplit("_", 1)[0] for t in stats})
for t in types[:-1]:
stats_str += f'{format_stats(t)} and '
stats_str += f'{format_stats(types[-1])}'
stats_str += f"{format_stats(t)} and "
stats_str += f"{format_stats(types[-1])}"
end_time = time.time() - start_time
print(f'Done, processed {stats_str} in {end_time:.2f} seconds.')
print(f"Done, processed {stats_str} in {end_time:.2f} seconds.")
def init_plugins(settings):
"""Load plugins and call register()."""
logger = logging.getLogger(__name__)
logger.debug('Plugin paths: %s', settings['plugin_paths'])
logger.debug("Plugin paths: %s", settings["plugin_paths"])
for path in settings['plugin_paths']:
for path in settings["plugin_paths"]:
sys.path.insert(0, path)
for plugin in settings['plugins']:
for plugin in settings["plugins"]:
try:
if isinstance(plugin, str):
mod = importlib.import_module(plugin)
mod.register(settings)
else:
plugin.register(settings)
logger.debug('Registered plugin %s', plugin)
logger.debug("Registered plugin %s", plugin)
except Exception as e:
logger.error('Failed to load plugin %s: %r', plugin, e)
logger.error("Failed to load plugin %s: %r", plugin, e)
for path in settings['plugin_paths']:
for path in settings["plugin_paths"]:
sys.path.remove(path)
@main.command()
@argument('destination', default='_build')
@option('-p', '--port', help="Port to use", default=8000)
@argument("destination", default="_build")
@option("-p", "--port", help="Port to use", default=8000)
@option(
'-c',
'--config',
"-c",
"--config",
default=_DEFAULT_CONFIG_FILE,
show_default=True,
help='Configuration file',
help="Configuration file",
)
def serve(destination, port, config):
"""Run a simple web server."""
@ -241,7 +241,7 @@ def serve(destination, port, config):
pass
elif os.path.exists(config):
settings = read_settings(config)
destination = settings.get('destination')
destination = settings.get("destination")
if not os.path.exists(destination):
sys.stderr.write(
f"The '{destination}' directory doesn't exist, maybe try building"
@ -255,7 +255,7 @@ def serve(destination, port, config):
)
sys.exit(2)
print(f'DESTINATION : {destination}')
print(f"DESTINATION : {destination}")
os.chdir(destination)
Handler = server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", port), Handler, False)
@ -267,14 +267,14 @@ def serve(destination, port, config):
httpd.server_activate()
httpd.serve_forever()
except KeyboardInterrupt:
print('\nAll done!')
print("\nAll done!")
@main.command()
@argument('target')
@argument('keys', nargs=-1)
@argument("target")
@argument("keys", nargs=-1)
@option(
'-o', '--overwrite', default=False, is_flag=True, help='Overwrite existing .md file'
"-o", "--overwrite", default=False, is_flag=True, help="Overwrite existing .md file"
)
def set_meta(target, keys, overwrite=False):
"""Write metadata keys to .md file.
@ -294,9 +294,9 @@ def set_meta(target, keys, overwrite=False):
sys.exit(1)
if os.path.isdir(target):
descfile = os.path.join(target, 'index.md')
descfile = os.path.join(target, "index.md")
else:
descfile = os.path.splitext(target)[0] + '.md'
descfile = os.path.splitext(target)[0] + ".md"
if os.path.exists(descfile) and not overwrite:
sys.stderr.write(
f"Description file '{descfile}' already exists. "

266
src/sigal/gallery.py

@ -73,7 +73,7 @@ class Media:
"""
type = ''
type = ""
"""Type of media, e.g. ``"image"`` or ``"video"``."""
def __init__(self, filename, path, settings):
@ -91,7 +91,7 @@ class Media:
self.src_ext = os.path.splitext(filename)[1].lower()
"""Input extension."""
self.src_path = join(settings['source'], path, self.src_filename)
self.src_path = join(settings["source"], path, self.src_filename)
self.thumb_name = get_thumb(self.settings, self.dst_filename)
@ -108,7 +108,7 @@ class Media:
def __getstate__(self):
state = self.__dict__.copy()
# remove un-pickable objects
state['logger'] = None
state["logger"] = None
return state
def __setstate__(self, state):
@ -118,11 +118,11 @@ class Media:
@property
def dst_path(self):
return join(self.settings['destination'], self.path, self.dst_filename)
return join(self.settings["destination"], self.path, self.dst_filename)
@property
def thumb_path(self):
return join(self.settings['destination'], self.path, self.thumb_name)
return join(self.settings["destination"], self.path, self.thumb_name)
@property
def url(self):
@ -134,22 +134,22 @@ class Media:
"""Path to the original image, if ``keep_orig`` is set (relative to the
album directory). Copy the file if needed.
"""
if self.settings['keep_orig']:
if self.settings["keep_orig"]:
s = self.settings
if s['use_orig']:
if s["use_orig"]:
# The image *is* the original, just use it
return self.src_filename
orig_path = join(s['destination'], self.path, s['orig_dir'])
orig_path = join(s["destination"], self.path, s["orig_dir"])
check_or_create_dir(orig_path)
big_path = join(orig_path, self.src_filename)
if not isfile(big_path):
copy(
self.src_path,
big_path,
symlink=s['orig_link'],
rellink=self.settings['rel_link'],
symlink=s["orig_link"],
rellink=self.settings["rel_link"],
)
return join(s['orig_dir'], self.src_filename)
return join(s["orig_dir"], self.src_filename)
@property
def big_url(self):
@ -162,47 +162,47 @@ class Media:
"""Path to the thumbnail image (relative to the album directory)."""
if not isfile(self.thumb_path):
self.logger.debug('Generating thumbnail for %r', self)
self.logger.debug("Generating thumbnail for %r", self)
path = self.dst_path if os.path.exists(self.dst_path) else self.src_path
try:
# if thumbnail is missing (if settings['make_thumbs'] is False)
s = self.settings
if self.type == 'image':
if self.type == "image":
image.generate_thumbnail(
path, self.thumb_path, s['thumb_size'], fit=s['thumb_fit']
path, self.thumb_path, s["thumb_size"], fit=s["thumb_fit"]
)
elif self.type == 'video':
elif self.type == "video":
video.generate_thumbnail(
path,
self.thumb_path,
s['thumb_size'],
s['thumb_video_delay'],
fit=s['thumb_fit'],
converter=s['video_converter'],
black_retries=s['thumb_video_black_retries'],
black_offset=s['thumb_video_black_retry_offset'],
black_max_colors=s['thumb_video_black_max_colors'],
s["thumb_size"],
s["thumb_video_delay"],
fit=s["thumb_fit"],
converter=s["video_converter"],
black_retries=s["thumb_video_black_retries"],
black_offset=s["thumb_video_black_retry_offset"],
black_max_colors=s["thumb_video_black_max_colors"],
)
except Exception as e:
self.logger.error('Failed to generate thumbnail: %s', e)
self.logger.error("Failed to generate thumbnail: %s", e)
return
return url_from_path(self.thumb_name)
@cached_property
def description(self):
"""Description extracted from the Markdown <imagename>.md file."""
return self.markdown_metadata.get('description', '')
return self.markdown_metadata.get("description", "")
@cached_property
def title(self):
"""Title extracted from the metadata, or defaults to the filename."""
title = self.markdown_metadata.get('title', '')
title = self.markdown_metadata.get("title", "")
return title if title else self.basename
@cached_property
def meta(self):
"""Other metadata extracted from the Markdown <imagename>.md file."""
return self.markdown_metadata.get('meta', {})
return self.markdown_metadata.get("meta", {})
@cached_property
def markdown_metadata(self):
@ -211,11 +211,11 @@ class Media:
@property
def markdown_metadata_filepath(self):
return splitext(self.src_path)[0] + '.md'
return splitext(self.src_path)[0] + ".md"
def _get_markdown_metadata(self):
"""Get metadata from filename.md."""
meta = {'title': '', 'description': '', 'meta': {}}
meta = {"title": "", "description": "", "meta": {}}
if isfile(self.markdown_metadata_filepath):
meta.update(read_markdown(self.markdown_metadata_filepath))
return meta
@ -232,11 +232,11 @@ class Media:
class Image(Media):
"""Gather all informations on an image file."""
type = 'image'
type = "image"
def __init__(self, filename, path, settings):
super().__init__(filename, path, settings)
imgformat = settings.get('img_format')
imgformat = settings.get("img_format")
# Register all formats
PILImage.init()
@ -252,17 +252,17 @@ class Image(Media):
def date(self):
"""The date from the EXIF DateTimeOriginal metadata if available, or
from the file date."""
return self.exif and self.exif.get('dateobj', None) or self._get_file_date()
return self.exif and self.exif.get("dateobj", None) or self._get_file_date()
@cached_property
def exif(self):
"""If not `None` contains a dict with the most common tags. For more
information, see :ref:`simple-exif-data`.
"""
datetime_format = self.settings['datetime_format']
datetime_format = self.settings["datetime_format"]
return (
get_exif_tags(self.raw_exif, datetime_format=datetime_format)
if self.raw_exif and self.src_ext in ('.jpg', '.jpeg')
if self.raw_exif and self.src_ext in (".jpg", ".jpeg")
else None
)
@ -277,18 +277,18 @@ class Image(Media):
# If a title or description hasn't been obtained by other means, look
# for the information in IPTC fields
if not meta['title']:
meta['title'] = self.file_metadata['iptc'].get('title', '')
if not meta['description']:
meta['description'] = self.file_metadata['iptc'].get('description', '')
if not meta["title"]:
meta["title"] = self.file_metadata["iptc"].get("title", "")
if not meta["description"]:
meta["description"] = self.file_metadata["iptc"].get("description", "")
return meta
@cached_property
def raw_exif(self):
"""If not `None`, contains the raw EXIF tags."""
if self.src_ext in ('.jpg', '.jpeg'):
return self.file_metadata['exif']
if self.src_ext in (".jpg", ".jpeg"):
return self.file_metadata["exif"]
@cached_property
def size(self):
@ -307,20 +307,20 @@ class Image(Media):
def has_location(self):
"""True if location information is available for EXIF GPSInfo."""
return self.exif is not None and 'gps' in self.exif
return self.exif is not None and "gps" in self.exif
class Video(Media):
"""Gather all informations on a video file."""
type = 'video'
type = "video"
def __init__(self, filename, path, settings):
super().__init__(filename, path, settings)
if not settings['use_orig'] or not is_valid_html5_video(self.src_ext):
video_format = settings['video_format']
ext = '.' + video_format
if not settings["use_orig"] or not is_valid_html5_video(self.src_ext):
video_format = settings["video_format"]
ext = "." + video_format
self.dst_filename = self.basename + ext
self.mime = get_mime(ext)
else:
@ -329,12 +329,12 @@ class Video(Media):
@cached_property
def date(self):
"""The date from the Date metadata if available, or from the file date."""
if 'date' in self.meta:
if "date" in self.meta:
try:
self.logger.debug(
"Reading date from image metadata : %s", self.src_filename
)
return datetime.fromisoformat(self.meta['date'][0])
return datetime.fromisoformat(self.meta["date"][0])
except Exception:
self.logger.debug(
"Reading date from image metadata failed : %s", self.src_filename
@ -368,24 +368,24 @@ class Album:
self.gallery = gallery
self.settings = settings
self.subdirs = dirnames
self.output_file = settings['output_filename']
self.output_file = settings["output_filename"]
self._thumbnail = None
if path == '.':
self.src_path = settings['source']
self.dst_path = settings['destination']
if path == ".":
self.src_path = settings["source"]
self.dst_path = settings["destination"]
else:
self.src_path = join(settings['source'], path)
self.dst_path = join(settings['destination'], path)
self.src_path = join(settings["source"], path)
self.dst_path = join(settings["destination"], path)
self.logger = logging.getLogger(__name__)
# optionally add index.html to the URLs
self.url_ext = self.output_file if settings['index_in_url'] else ''
self.url_ext = self.output_file if settings["index_in_url"] else ""
self.index_url = (
url_from_path(os.path.relpath(settings['destination'], self.dst_path))
+ '/'
url_from_path(os.path.relpath(settings["destination"], self.dst_path))
+ "/"
+ self.url_ext
)
@ -397,9 +397,9 @@ class Album:
for f in filenames:
ext = splitext(f)[1]
media = None
if ext.lower() in settings['img_extensions']:
if ext.lower() in settings["img_extensions"]:
media = Image(f, self.path, settings)
elif ext.lower() in settings['video_extensions']:
elif ext.lower() in settings["video_extensions"]:
media = Video(f, self.path, settings)
# Allow modification of the media, including overriding the class
@ -421,8 +421,8 @@ class Album:
)
def __str__(self):
return f'{self.path} : ' + ', '.join(
f'{count} {_type}s' for _type, count in self.medias_count.items()
return f"{self.path} : " + ", ".join(
f"{count} {_type}s" for _type, count in self.medias_count.items()
)
def __len__(self):
@ -434,27 +434,27 @@ class Album:
@cached_property
def description(self):
"""Description extracted from the Markdown index.md file."""
return self.markdown_metadata.get('description', '')
return self.markdown_metadata.get("description", "")
@cached_property
def title(self):
"""Title extracted from the Markdown index.md file."""
title = self.markdown_metadata.get('title', '')
path = self.path if self.path != '.' else self.src_path
title = self.markdown_metadata.get("title", "")
path = self.path if self.path != "." else self.src_path
return title if title else os.path.basename(path)
@cached_property
def meta(self):
"""Other metadata extracted from the Markdown index.md file."""
return self.markdown_metadata.get('meta', {})
return self.markdown_metadata.get("meta", {})
@cached_property
def author(self):
"""Author extracted from the Markdown index.md file or settings."""
try:
return self.meta['author'][0]
return self.meta["author"][0]
except KeyError:
return self.settings.get('author')
return self.settings.get("author")
@property
def markdown_metadata_filepath(self):
@ -463,7 +463,7 @@ class Album:
@cached_property
def markdown_metadata(self):
"""Get metadata from filename.md: title, description, meta."""
meta = {'title': '', 'description': '', 'meta': {}}
meta = {"title": "", "description": "", "meta": {}}
if isfile(self.markdown_metadata_filepath):
meta.update(read_markdown(self.markdown_metadata_filepath))
return meta
@ -473,27 +473,27 @@ class Album:
check_or_create_dir(self.dst_path)
if self.medias:
check_or_create_dir(join(self.dst_path, self.settings['thumb_dir']))
check_or_create_dir(join(self.dst_path, self.settings["thumb_dir"]))
if self.medias and self.settings['keep_orig']:
self.orig_path = join(self.dst_path, self.settings['orig_dir'])
if self.medias and self.settings["keep_orig"]:
self.orig_path = join(self.dst_path, self.settings["orig_dir"])
check_or_create_dir(self.orig_path)
def sort_subdirs(self, albums_sort_attr):
if self.subdirs:
if not albums_sort_attr:
albums_sort_attr = self.settings['albums_sort_attr']
reverse = self.settings['albums_sort_reverse']
albums_sort_attr = self.settings["albums_sort_attr"]
reverse = self.settings["albums_sort_reverse"]
if 'sort' in self.meta:
albums_sort_attr = self.meta['sort'][0]
if albums_sort_attr[0] == '-':
if "sort" in self.meta:
albums_sort_attr = self.meta["sort"][0]
if albums_sort_attr[0] == "-":
albums_sort_attr = albums_sort_attr[1:]
reverse = True
else:
reverse = False
root_path = self.path if self.path != '.' else ''
root_path = self.path if self.path != "." else ""
def sort_key(s):
sort_attr = albums_sort_attr
@ -513,7 +513,7 @@ class Album:
continue
except TypeError:
continue
return ''
return ""
key = natsort_keygen(key=sort_key, alg=ns.LOCALE)
self.subdirs.sort(key=key, reverse=reverse)
@ -522,22 +522,22 @@ class Album:
def sort_medias(self, medias_sort_attr):
if self.medias:
if medias_sort_attr == 'filename':
medias_sort_attr = 'dst_filename'
if medias_sort_attr == "filename":
medias_sort_attr = "dst_filename"
if medias_sort_attr == 'date':
if medias_sort_attr == "date":
key = lambda s: s.date or datetime.now()
elif medias_sort_attr.startswith('meta.'):
elif medias_sort_attr.startswith("meta."):
meta_key = medias_sort_attr.split(".", 1)[1]
key = natsort_keygen(
key=lambda s: s.meta.get(meta_key, [''])[0], alg=ns.LOCALE
key=lambda s: s.meta.get(meta_key, [""])[0], alg=ns.LOCALE
)
else:
key = natsort_keygen(
key=lambda s: getattr(s, medias_sort_attr), alg=ns.LOCALE
)
self.medias.sort(key=key, reverse=self.settings['medias_sort_reverse'])
self.medias.sort(key=key, reverse=self.settings["medias_sort_reverse"])
signals.medias_sorted.send(self)
@ -545,14 +545,14 @@ class Album:
def images(self):
"""List of images (:class:`~sigal.gallery.Image`)."""
for media in self.medias:
if media.type == 'image':
if media.type == "image":
yield media
@property
def videos(self):
"""List of videos (:class:`~sigal.gallery.Video`)."""
for media in self.medias:
if media.type == 'video':
if media.type == "video":
yield media
@property
@ -560,7 +560,7 @@ class Album:
"""List of :class:`~sigal.gallery.Album` objects for each
sub-directory.
"""
root_path = self.path if self.path != '.' else ''
root_path = self.path if self.path != "." else ""
return [self.gallery.albums[join(root_path, path)] for path in self.subdirs]
@property
@ -570,8 +570,8 @@ class Album:
@property
def url(self):
"""URL of the album, relative to its parent."""
url = self.name.encode('utf-8')
return url_quote(url) + '/' + self.url_ext
url = self.name.encode("utf-8")
return url_quote(url) + "/" + self.url_ext
@property
def thumbnail(self):
@ -582,7 +582,7 @@ class Album:
return self._thumbnail
# Test the thumbnail from the Markdown file.
thumbnail = self.meta.get('thumbnail', [''])[0]
thumbnail = self.meta.get("thumbnail", [""])[0]
if thumbnail and isfile(join(self.src_path, thumbnail)):
self._thumbnail = url_from_path(
@ -594,18 +594,18 @@ class Album:
# find and return the first landscape image
for f in self.medias:
ext = splitext(f.dst_filename)[1]
if ext.lower() not in self.settings['img_extensions']:
if ext.lower() not in self.settings["img_extensions"]:
continue
# Use f.size if available as it is quicker (in cache), but
# fallback to the size of src_path if dst_path is missing
size = f.input_size
if size is None:
size = f.file_metadata['size']
size = f.file_metadata["size"]
if size['width'] > size['height']:
if size["width"] > size["height"]:
try:
self._thumbnail = url_quote(self.name) + '/' + f.thumbnail
self._thumbnail = url_quote(self.name) + "/" + f.thumbnail
except Exception as e:
self.logger.info(
"Failed to get thumbnail for %s: %s", f.dst_filename, e
@ -624,7 +624,7 @@ class Album:
if media.thumbnail is not None:
try:
self._thumbnail = (
url_quote(self.name) + '/' + media.thumbnail
url_quote(self.name) + "/" + media.thumbnail
)
except Exception as e:
self.logger.info(
@ -647,7 +647,7 @@ class Album:
if not self._thumbnail:
for path, album in self.gallery.get_albums(self.path):
if album.thumbnail:
self._thumbnail = url_quote(self.name) + '/' + album.thumbnail
self._thumbnail = url_quote(self.name) + "/" + album.thumbnail
self.logger.debug(
"Using thumbnail from sub-directory for %r : %s",
self,
@ -655,7 +655,7 @@ class Album:
)
return self._thumbnail
self.logger.error('Thumbnail not found for %r', self)
self.logger.error("Thumbnail not found for %r", self)
@property
def random_thumbnail(self):
@ -669,18 +669,18 @@ class Album:
"""List of ``(url, title)`` tuples defining the current breadcrumb
path.
"""
if self.path == '.':
if self.path == ".":
return []
path = self.path
breadcrumb = [((self.url_ext or '.'), self.title)]
breadcrumb = [((self.url_ext or "."), self.title)]
while True:
path = os.path.normpath(os.path.join(path, '..'))
if path == '.':
path = os.path.normpath(os.path.join(path, ".."))
if path == ".":
break
url = url_from_path(os.path.relpath(path, self.path)) + '/' + self.url_ext
url = url_from_path(os.path.relpath(path, self.path)) + "/" + self.url_ext
breadcrumb.append((url, self.gallery.albums[path].title))
breadcrumb.reverse()
@ -704,17 +704,17 @@ class Gallery:
self.logger = logging.getLogger(__name__)
self.stats = defaultdict(int)
self.init_pool(ncpu)
check_or_create_dir(settings['destination'])
check_or_create_dir(settings["destination"])
if settings['max_img_pixels']:
PILImage.MAX_IMAGE_PIXELS = settings['max_img_pixels']
if settings["max_img_pixels"]:
PILImage.MAX_IMAGE_PIXELS = settings["max_img_pixels"]
# Build the list of directories with images
albums = self.albums = {}
src_path = self.settings['source']
src_path = self.settings["source"]
ignore_dirs = settings['ignore_directories']
ignore_files = settings['ignore_files']
ignore_dirs = settings["ignore_directories"]
ignore_files = settings["ignore_files"]
progressChars = cycle(["/", "-", "\\", "|"])
show_progress = (
@ -733,7 +733,7 @@ class Gallery:
if ignore_dirs and any(
fnmatch.fnmatch(relpath, ignore) for ignore in ignore_dirs
):
self.logger.info('Ignoring %s', relpath)
self.logger.info("Ignoring %s", relpath)
continue
# Remove files that match the ignore_files settings
@ -742,22 +742,22 @@ class Gallery:
for ignore in ignore_files:
files_path -= set(fnmatch.filter(files_path, ignore))
self.logger.debug('Files before filtering: %r', files)
self.logger.debug("Files before filtering: %r", files)
files = [os.path.split(f)[1] for f in files_path]
self.logger.debug('Files after filtering: %r', files)
self.logger.debug("Files after filtering: %r", files)
# Remove sub-directories that have been ignored in a previous
# iteration (as topdown=False, sub-directories are processed before
# their parent
for d in dirs[:]:
path = join(relpath, d) if relpath != '.' else d
path = join(relpath, d) if relpath != "." else d
if path not in albums.keys():
dirs.remove(d)
album = Album(relpath, settings, dirs, files, self)
if not album.medias and not album.albums:
self.logger.info('Skip empty album: %r', album)
self.logger.info("Skip empty album: %r", album)
else:
album.create_output_directories()
albums[relpath] = album
@ -771,7 +771,7 @@ class Gallery:
file=self.progressbar_target,
) as progress_albums:
for album in progress_albums:
album.sort_subdirs(settings['albums_sort_attr'])
album.sort_subdirs(settings["albums_sort_attr"])
with progressbar(
albums.values(),
@ -779,15 +779,15 @@ class Gallery:
file=self.progressbar_target,
) as progress_albums:
for album in progress_albums:
album.sort_medias(settings['medias_sort_attr'])
album.sort_medias(settings["medias_sort_attr"])
self.logger.debug('Albums:\n%r', albums.values())
self.logger.debug("Albums:\n%r", albums.values())
signals.gallery_initialized.send(self)
@property
def title(self):
"""Title of the gallery."""
return self.settings['title'] or self.albums['.'].title
return self.settings["title"] or self.albums["."].title
def init_pool(self, ncpu):
try:
@ -801,7 +801,7 @@ class Gallery:
try:
ncpu = int(ncpu)
except ValueError:
self.logger.error('ncpu should be an integer value')
self.logger.error("ncpu should be an integer value")
ncpu = cpu_count
self.logger.info("Using %s cores", ncpu)
@ -809,7 +809,7 @@ class Gallery:
self.pool = multiprocessing.Pool(
processes=ncpu,
initializer=pool_init,
initargs=(self.settings['max_img_pixels'],),
initargs=(self.settings["max_img_pixels"],),
)
else:
self.pool = None
@ -850,12 +850,12 @@ class Gallery:
f for album in albums for f in self.process_dir(album, force=force)
]
except KeyboardInterrupt:
sys.exit('Interrupted')
sys.exit("Interrupted")
bar_opt = {
'label': "Processing files",
'show_pos': True,
'file': self.progressbar_target,
"label": "Processing files",
"show_pos": True,
"file": self.progressbar_target,
}
if self.pool:
@ -867,7 +867,7 @@ class Gallery:
bar.update(1)
except KeyboardInterrupt:
self.pool.terminate()
sys.exit('Interrupted')
sys.exit("Interrupted")
except pickle.PicklingError:
self.logger.critical(
"Failed to process files with the multiprocessing feature."
@ -875,7 +875,7 @@ class Gallery:
"defined in the settings file, which can't be serialized.",
exc_info=True,
)
sys.exit('Abort')
sys.exit("Abort")
finally:
self.pool.close()
self.pool.join()
@ -889,7 +889,7 @@ class Gallery:
]
self.remove_files(failed_files)
if self.settings['write_html']:
if self.settings["write_html"]:
album_writer = AlbumPageWriter(self.settings, index_title=self.title)
album_list_writer = AlbumListPageWriter(
self.settings, index_title=self.title
@ -914,23 +914,23 @@ class Gallery:
album_list_writer.write(album)
else:
album_writer.write(album)
print('')
print("")
signals.gallery_build.send(self)
def remove_files(self, medias):
self.logger.error('Some files have failed to be processed:')
self.logger.error("Some files have failed to be processed:")
for media in medias:
self.logger.error(' - %s', media.dst_filename)
self.logger.error(" - %s", media.dst_filename)
album = self.albums[media.path]
for f in album.medias:
if f.dst_filename == media.dst_filename:
self.stats[f.type + '_failed'] += 1
self.stats[f.type + "_failed"] += 1
album.medias.remove(f)
break
self.logger.error(
'You can run "sigal build" in verbose (--verbose) or'
' debug (--debug) mode to get more details.'
" debug (--debug) mode to get more details."
)
def process_dir(self, album, force=False):
@ -938,7 +938,7 @@ class Gallery:
for f in album:
if isfile(f.dst_path) and not force:
self.logger.info("%s exists - skipping", f.dst_filename)
self.stats[f.type + '_skipped'] += 1
self.stats[f.type + "_skipped"] += 1
else:
self.stats[f.type] += 1
yield f
@ -951,9 +951,9 @@ def pool_init(max_img_pixels):
def process_file(media):
processor = None
if media.type == 'image':
if media.type == "image":
processor = process_image
elif media.type == 'video':
elif media.type == "video":
processor = process_video
# Allow overriding of the processor
@ -965,7 +965,7 @@ def process_file(media):
if processor:
return processor(media)
else:
logging.warning('Processor not found for media %s', media.path)
logging.warning("Processor not found for media %s", media.path)
return Status.FAILURE

160
src/sigal/image.py

@ -57,7 +57,7 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True
def _has_exif_tags(img):
return hasattr(img, 'info') and 'exif' in img.info
return hasattr(img, "info") and "exif" in img.info
def _read_image(file_path):
@ -72,8 +72,8 @@ def _read_image(file_path):
for w in caught_warnings:
logger.warning(
f'PILImage reported a warning for file {file_path}\n'
f'{w.category}: {w.message}'
f"PILImage reported a warning for file {file_path}\n"
f"{w.category}: {w.message}"
)
return im
@ -90,14 +90,14 @@ def generate_image(source, outname, settings, options=None):
logger = logging.getLogger(__name__)
if settings['use_orig'] or source.endswith('.gif'):
utils.copy(source, outname, symlink=settings['orig_link'])
if settings["use_orig"] or source.endswith(".gif"):
utils.copy(source, outname, symlink=settings["orig_link"])
return
img = _read_image(source)
original_format = img.format
if settings['copy_exif_data'] and settings['autorotate_images']:
if settings["copy_exif_data"] and settings["autorotate_images"]:
logger.warning(
"The 'autorotate_images' and 'copy_exif_data' settings "
"are not compatible because Sigal can't save the "
@ -105,30 +105,30 @@ def generate_image(source, outname, settings, options=None):
)
# Preserve EXIF data
if settings['copy_exif_data'] and _has_exif_tags(img):
if settings["copy_exif_data"] and _has_exif_tags(img):
if options is not None:
options = deepcopy(options)
else:
options = {}
options['exif'] = img.info['exif']
options["exif"] = img.info["exif"]
# Rotate the img, and catch IOError when PIL fails to read EXIF
if settings['autorotate_images']:
if settings["autorotate_images"]:
try:
img = Transpose().process(img)
except (OSError, IndexError):
pass
# Resize the image
if settings['img_processor']:
if settings["img_processor"]:
try:
logger.debug('Processor: %s', settings['img_processor'])
processor_cls = getattr(pilkit.processors, settings['img_processor'])
logger.debug("Processor: %s", settings["img_processor"])
processor_cls = getattr(pilkit.processors, settings["img_processor"])
except AttributeError:
logger.error('Wrong processor name: %s', settings['img_processor'])
logger.error("Wrong processor name: %s", settings["img_processor"])
sys.exit()
width, height = settings['img_size']
width, height = settings["img_size"]
if img.size[0] < img.size[1]:
# swap target size if image is in portrait mode
@ -144,9 +144,9 @@ def generate_image(source, outname, settings, options=None):
# first, use hard-coded output format, or PIL format, or original image
# format, or fall back to JPEG
outformat = settings.get('img_format') or img.format or original_format or 'JPEG'
outformat = settings.get("img_format") or img.format or original_format or "JPEG"
logger.debug('Save resized image to %s (%s)', outname, outformat)
logger.debug("Save resized image to %s (%s)", outname, outformat)
save_image(img, outname, outformat, options=options, autoconvert=True)
@ -171,8 +171,8 @@ def generate_thumbnail(
else:
img.thumbnail(box, method)
outformat = img.format or original_format or 'JPEG'
logger.debug('Save thumnail image: %s (%s)', outname, outformat)
outformat = img.format or original_format or "JPEG"
logger.debug("Save thumnail image: %s (%s)", outname, outformat)
save_image(img, outname, outformat, options=options, autoconvert=True)
@ -180,24 +180,24 @@ def process_image(media):
"""Process one image: resize, create thumbnail."""
logger = logging.getLogger(__name__)
logger.info('Processing %s', media.src_path)
logger.info("Processing %s", media.src_path)
if media.src_ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
options = media.settings['jpg_options']
elif media.src_ext == '.png':
options = {'optimize': True}
if media.src_ext in (".jpg", ".jpeg", ".JPG", ".JPEG"):
options = media.settings["jpg_options"]
elif media.src_ext == ".png":
options = {"optimize": True}
else:
options = {}
with utils.raise_if_debug() as status:
generate_image(media.src_path, media.dst_path, media.settings, options=options)
if media.settings['make_thumbs']:
if media.settings["make_thumbs"]:
generate_thumbnail(
media.dst_path,
media.thumb_path,
media.settings['thumb_size'],
fit=media.settings['thumb_fit'],
media.settings["thumb_size"],
fit=media.settings["thumb_fit"],
options=options,
thumb_fit_centering=media.settings["thumb_fit_centering"],
)
@ -214,7 +214,7 @@ def get_size(file_path):
logger.error("Could not read size of %s due to %r", file_path, e)
else:
width, height = im.size
return {'width': width, 'height': height}
return {"width": width, "height": height}
def get_exif_data(filename):
@ -228,25 +228,25 @@ def get_exif_data(filename):
with warnings.catch_warnings(record=True) as caught_warnings:
exif = img._getexif() or {}
except ZeroDivisionError:
logger.warning('Failed to read EXIF data.')
logger.warning("Failed to read EXIF data.")
return None
for w in caught_warnings:
fname = filename.filename if isinstance(filename, PILImage.Image) else filename
logger.warning(
f'PILImage reported a warning for file {fname}\n{w.category}: {w.message}'
f"PILImage reported a warning for file {fname}\n{w.category}: {w.message}"
)
data = {TAGS.get(tag, tag): value for tag, value in exif.items()}
if 'GPSInfo' in data:
if "GPSInfo" in data:
try:
data['GPSInfo'] = {
GPSTAGS.get(tag, tag): value for tag, value in data['GPSInfo'].items()
data["GPSInfo"] = {
GPSTAGS.get(tag, tag): value for tag, value in data["GPSInfo"].items()
}
except AttributeError:
logger.info('Failed to get GPS Info')
del data['GPSInfo']
logger.info("Failed to get GPS Info")
del data["GPSInfo"]
return data
@ -266,21 +266,21 @@ def get_iptc_data(filename):
img = _read_image(filename)
raw_iptc = IptcImagePlugin.getiptcinfo(img)
except SyntaxError:
logger.info('IPTC Error in %s', filename)
logger.info("IPTC Error in %s", filename)
# IPTC fields are catalogued in:
# https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata
# 2:05 is the IPTC title property
if raw_iptc and (2, 5) in raw_iptc:
iptc_data["title"] = raw_iptc[(2, 5)].decode('utf-8', errors='replace')
iptc_data["title"] = raw_iptc[(2, 5)].decode("utf-8", errors="replace")
# 2:120 is the IPTC description property
if raw_iptc and (2, 120) in raw_iptc:
iptc_data["description"] = raw_iptc[(2, 120)].decode('utf-8', errors='replace')
iptc_data["description"] = raw_iptc[(2, 120)].decode("utf-8", errors="replace")
# 2:105 is the IPTC headline property
if raw_iptc and (2, 105) in raw_iptc:
iptc_data["headline"] = raw_iptc[(2, 105)].decode('utf-8', errors='replace')
iptc_data["headline"] = raw_iptc[(2, 105)].decode("utf-8", errors="replace")
return iptc_data
@ -292,25 +292,25 @@ def get_image_metadata(filename):
try:
img = _read_image(filename)
except Exception as e:
logger.error('Could not open image %s metadata: %s', filename, e)
logger.error("Could not open image %s metadata: %s", filename, e)
else:
try:
if os.path.splitext(filename)[1].lower() in ('.jpg', '.jpeg'):
if os.path.splitext(filename)[1].lower() in (".jpg", ".jpeg"):
exif = get_exif_data(img)
except Exception as e:
logger.warning('Could not read EXIF data from %s: %s', filename, e)
logger.warning("Could not read EXIF data from %s: %s", filename, e)
try:
iptc = get_iptc_data(img)
except Exception as e:
logger.warning('Could not read IPTC data from %s: %s', filename, e)
logger.warning("Could not read IPTC data from %s: %s", filename, e)
try:
size = get_size(img)
except Exception as e:
logger.warning('Could not read size from %s: %s', filename, e)
logger.warning("Could not read size from %s: %s", filename, e)
return {'exif': exif, 'iptc': iptc, 'size': size}
return {"exif": exif, "iptc": iptc, "size": size}
def dms_to_degrees(v):
@ -327,81 +327,81 @@ def dms_to_degrees(v):
return d + (m / 60.0) + (s / 3600.0)
def get_exif_tags(data, datetime_format='%c'):
def get_exif_tags(data, datetime_format="%c"):
"""Make a simplified version with common tags from raw EXIF data."""
logger = logging.getLogger(__name__)
simple = {}
for tag in ('Model', 'Make', 'LensModel'):
for tag in ("Model", "Make", "LensModel"):
if tag in data:
val = data[tag][0] if isinstance(data[tag], tuple) else data[tag]
simple[tag] = str(val).strip()
if 'FNumber' in data:
fnumber = data['FNumber']
if "FNumber" in data:
fnumber = data["FNumber"]
try:
if IFDRational and isinstance(fnumber, IFDRational):
simple['fstop'] = float(fnumber)
simple["fstop"] = float(fnumber)
else:
simple['fstop'] = float(fnumber[0]) / fnumber[1]
simple["fstop"] = float(fnumber[0]) / fnumber[1]
except Exception:
logger.debug('Skipped invalid FNumber: %r', fnumber, exc_info=True)
logger.debug("Skipped invalid FNumber: %r", fnumber, exc_info=True)
if 'FocalLength' in data:
focal = data['FocalLength']
if "FocalLength" in data:
focal = data["FocalLength"]
try:
if IFDRational and isinstance(focal, IFDRational):
simple['focal'] = round(float(focal))
simple["focal"] = round(float(focal))
else:
simple['focal'] = round(float(focal[0]) / focal[1])
simple["focal"] = round(float(focal[0]) / focal[1])
except Exception:
logger.debug('Skipped invalid FocalLength: %r', focal, exc_info=True)
logger.debug("Skipped invalid FocalLength: %r", focal, exc_info=True)
if 'ExposureTime' in data:
exptime = data['ExposureTime']
if "ExposureTime" in data:
exptime = data["ExposureTime"]
if IFDRational and isinstance(exptime, IFDRational):
simple['exposure'] = f'{exptime.numerator}/{exptime.denominator}'
simple["exposure"] = f"{exptime.numerator}/{exptime.denominator}"
elif isinstance(exptime, tuple):
try:
simple['exposure'] = str(fractions.Fraction(exptime[0], exptime[1]))
simple["exposure"] = str(fractions.Fraction(exptime[0], exptime[1]))
except ZeroDivisionError:
logger.info('Invalid ExposureTime: %r', exptime)
logger.info("Invalid ExposureTime: %r", exptime)
elif isinstance(exptime, int):
simple['exposure'] = str(exptime)
simple["exposure"] = str(exptime)
else:
logger.info('Unknown format for ExposureTime: %r', exptime)
logger.info("Unknown format for ExposureTime: %r", exptime)
if data.get('ISOSpeedRatings'):
simple['iso'] = data['ISOSpeedRatings']
if data.get("ISOSpeedRatings"):
simple["iso"] = data["ISOSpeedRatings"]
if 'DateTimeOriginal' in data:
if "DateTimeOriginal" in data:
# Remove null bytes at the end if necessary
date = data['DateTimeOriginal'].rsplit('\x00')[0]
date = data["DateTimeOriginal"].rsplit("\x00")[0]
try:
simple['dateobj'] = datetime.strptime(date, '%Y:%m:%d %H:%M:%S')
simple['datetime'] = simple['dateobj'].strftime(datetime_format)
simple["dateobj"] = datetime.strptime(date, "%Y:%m:%d %H:%M:%S")
simple["datetime"] = simple["dateobj"].strftime(datetime_format)
except (ValueError, TypeError) as e:
logger.info('Could not parse DateTimeOriginal: %s', e)
logger.info("Could not parse DateTimeOriginal: %s", e)
if 'GPSInfo' in data:
info = data['GPSInfo']
lat_info = info.get('GPSLatitude')
lon_info = info.get('GPSLongitude')
lat_ref_info = info.get('GPSLatitudeRef')
lon_ref_info = info.get('GPSLongitudeRef')
if "GPSInfo" in data:
info = data["GPSInfo"]
lat_info = info.get("GPSLatitude")
lon_info = info.get("GPSLongitude")
lat_ref_info = info.get("GPSLatitudeRef")
lon_ref_info = info.get("GPSLongitudeRef")
if lat_info and lon_info and lat_ref_info and lon_ref_info:
try:
lat = dms_to_degrees(lat_info)
lon = dms_to_degrees(lon_info)
except (ZeroDivisionError, ValueError, TypeError):
logger.info('Failed to read GPS info')
logger.info("Failed to read GPS info")
else:
simple['gps'] = {
'lat': -lat if lat_ref_info != 'N' else lat,
'lon': -lon if lon_ref_info != 'E' else lon,
simple["gps"] = {
"lat": -lat if lat_ref_info != "N" else lat,
"lon": -lon if lon_ref_info != "E" else lon,
}
return simple

20
src/sigal/log.py

@ -28,11 +28,11 @@ from logging import Formatter
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = (30 + i for i in range(8))
COLORS = {
'DEBUG': BLUE,
'INFO': GREEN,
'WARNING': YELLOW,
'ERROR': RED,
'CRITICAL': MAGENTA,
"DEBUG": BLUE,
"INFO": GREEN,
"WARNING": YELLOW,
"ERROR": RED,
"CRITICAL": MAGENTA,
}
# These are the sequences need to get colored ouput
@ -48,7 +48,7 @@ def colored(text, color):
class ColoredFormatter(Formatter):
def format(self, record):
level = record.levelname
return colored(level, COLORS[level]) + ': ' + record.getMessage()
return colored(level, COLORS[level]) + ": " + record.getMessage()
def init_logging(name, level=logging.INFO):
@ -61,15 +61,15 @@ def init_logging(name, level=logging.INFO):
logger.setLevel(level)
try:
if os.isatty(sys.stdout.fileno()) and not sys.platform.startswith('win'):
if os.isatty(sys.stdout.fileno()) and not sys.platform.startswith("win"):
formatter = ColoredFormatter()
elif level == logging.DEBUG:
formatter = Formatter('%(levelname)s - %(message)s')
formatter = Formatter("%(levelname)s - %(message)s")
else:
formatter = Formatter('%(message)s')
formatter = Formatter("%(message)s")
except Exception:
# This fails when running tests with click (test_build)
formatter = Formatter('%(message)s')
formatter = Formatter("%(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)

8
src/sigal/plugins/adjust.py

@ -26,12 +26,12 @@ logger = logging.getLogger(__name__)
def adjust(img, settings=None):
logger.debug('Adjust image %r', img)
return Adjust(**settings['adjust_options']).process(img)
logger.debug("Adjust image %r", img)
return Adjust(**settings["adjust_options"]).process(img)
def register(settings):
if settings.get('adjust_options'):
if settings.get("adjust_options"):
signals.img_resized.connect(adjust)
else:
logger.warning('Adjust options are not set')
logger.warning("Adjust options are not set")

46
src/sigal/plugins/compress_assets.py

@ -39,8 +39,8 @@ from sigal import signals
logger = logging.getLogger(__name__)
DEFAULT_SETTINGS = {
'suffixes': ['htm', 'html', 'css', 'js', 'svg'],
'method': 'gzip',
"suffixes": ["htm", "html", "css", "js", "svg"],
"method": "gzip",
}
@ -49,7 +49,7 @@ class BaseCompressor:
def __init__(self, settings):
self.suffixes_to_compress = settings.get(
'suffixes', DEFAULT_SETTINGS['suffixes']
"suffixes", DEFAULT_SETTINGS["suffixes"]
)
def do_compress(self, filename, compressed_filename):
@ -85,7 +85,7 @@ class BaseCompressor:
file_stats = None
compressed_stats = None
compressed_filename = f'{filename}.{self.suffix}'
compressed_filename = f"{filename}.{self.suffix}"
try:
file_stats = os.stat(filename)
compressed_stats = os.stat(compressed_filename)
@ -103,63 +103,63 @@ class BaseCompressor:
class GZipCompressor(BaseCompressor):
suffix = 'gz'
suffix = "gz"
def do_compress(self, filename, compressed_filename):
with open(filename, 'rb') as f_in, gzip.open(
compressed_filename, 'wb'
with open(filename, "rb") as f_in, gzip.open(
compressed_filename, "wb"
) as f_out:
shutil.copyfileobj(f_in, f_out)
class ZopfliCompressor(BaseCompressor):
suffix = 'gz'
suffix = "gz"
def do_compress(self, filename, compressed_filename):
import zopfli.gzip
with open(filename, 'rb') as f_in, open(compressed_filename, 'wb') as f_out:
with open(filename, "rb") as f_in, open(compressed_filename, "wb") as f_out:
f_out.write(zopfli.gzip.compress(f_in.read()))
class BrotliCompressor(BaseCompressor):
suffix = 'br'
suffix = "br"
def do_compress(self, filename, compressed_filename):
import brotli
with open(filename, 'rb') as f_in, open(compressed_filename, 'wb') as f_out:
with open(filename, "rb") as f_in, open(compressed_filename, "wb") as f_out:
f_out.write(brotli.compress(f_in.read(), mode=brotli.MODE_TEXT))
def get_compressor(settings):
name = settings.get('method', DEFAULT_SETTINGS['method'])
if name == 'gzip':
name = settings.get("method", DEFAULT_SETTINGS["method"])
if name == "gzip":
return GZipCompressor(settings)
elif name == 'zopfli':
elif name == "zopfli":
try:
import zopfli.gzip # noqa
return ZopfliCompressor(settings)
except ImportError:
logging.error('Unable to import zopfli module')
logging.error("Unable to import zopfli module")
elif name == 'brotli':
elif name == "brotli":
try:
import brotli # noqa
return BrotliCompressor(settings)
except ImportError:
logger.error('Unable to import brotli module')
logger.error("Unable to import brotli module")
else:
logger.error(f'No such compressor {name}')
logger.error(f"No such compressor {name}")
def compress_gallery(gallery):
logging.info('Compressing assets for %s', gallery.title)
logging.info("Compressing assets for %s", gallery.title)
compress_settings = gallery.settings.get(
'compress_assets_options', DEFAULT_SETTINGS
"compress_assets_options", DEFAULT_SETTINGS
)
compressor = get_compressor(compress_settings)
@ -169,13 +169,13 @@ def compress_gallery(gallery):
# Collecting theme assets
theme_assets = []
for current_directory, _, filenames in os.walk(
os.path.join(gallery.settings['destination'], 'static')
os.path.join(gallery.settings["destination"], "static")
):
for filename in filenames:
theme_assets.append(os.path.join(current_directory, filename))
with progressbar(
length=len(gallery.albums) + len(theme_assets), label='Compressing static files'
length=len(gallery.albums) + len(theme_assets), label="Compressing static files"
) as bar:
for album in gallery.albums.values():
compressor.compress(os.path.join(album.dst_path, album.output_file))
@ -187,5 +187,5 @@ def compress_gallery(gallery):
def register(settings):
if settings['write_html']:
if settings["write_html"]:
signals.gallery_build.connect(compress_gallery)

16
src/sigal/plugins/copyright.py

@ -25,13 +25,13 @@ logger = logging.getLogger(__name__)
def add_copyright(img, settings=None):
logger.debug('Adding copyright to %r', img)
logger.debug("Adding copyright to %r", img)
draw = ImageDraw.Draw(img)
text = settings['copyright']
font = settings.get('copyright_text_font', None)
font_size = settings.get('copyright_text_font_size', 10)
text = settings["copyright"]
font = settings.get("copyright_text_font", None)
font_size = settings.get("copyright_text_font_size", 10)
assert font_size >= 0
color = settings.get('copyright_text_color', (0, 0, 0))
color = settings.get("copyright_text_color", (0, 0, 0))
bottom_margin = 3 # bottom margin for text
text_height = bottom_margin + 12 # default text height (of 15)
if font:
@ -43,13 +43,13 @@ def add_copyright(img, settings=None):
font = ImageFont.load_default()
else:
font = ImageFont.load_default()
left, top = settings.get('copyright_text_position', (5, img.size[1] - text_height))
left, top = settings.get("copyright_text_position", (5, img.size[1] - text_height))
draw.text((left, top), text, fill=color, font=font)
return img
def register(settings):
if settings.get('copyright'):
if settings.get("copyright"):
signals.img_resized.connect(add_copyright)
else:
logger.warning('Copyright text is not set')
logger.warning("Copyright text is not set")

12
src/sigal/plugins/encrypt/encrypt.py

@ -37,7 +37,7 @@ from .endec import encrypt, kdf_gen_key
logger = logging.getLogger(__name__)
ASSETS_PATH = os.path.normpath(
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static')
os.path.join(os.path.abspath(os.path.dirname(__file__)), "static")
)
@ -62,7 +62,7 @@ def get_options(settings, cache):
except KeyError:
options = settings["encrypt_options"]
table = str.maketrans({'"': r'\"', '\\': r'\\'})
table = str.maketrans({'"': r"\"", "\\": r"\\"})
if (
"password" not in settings["encrypt_options"]
or len(settings["encrypt_options"]["password"]) == 0
@ -227,7 +227,7 @@ def encrypt_files(settings, config, cache, albums, progressbar_target):
save_cache(settings, cache)
raise Abort
key_check_path = os.path.join(settings["destination"], 'static', 'keycheck.txt')
key_check_path = os.path.join(settings["destination"], "static", "keycheck.txt")
encrypt_file("keycheck.txt", key_check_path, key, gcm_tag)
@ -251,7 +251,7 @@ def encrypt_file(filename, full_path, key, gcm_tag):
def copy_assets(settings):
theme_path = os.path.join(settings["destination"], 'static')
theme_path = os.path.join(settings["destination"], "static")
copy(
os.path.join(ASSETS_PATH, "decrypt.js"),
theme_path,
@ -273,8 +273,8 @@ def copy_assets(settings):
def inject_scripts(context):
cache = load_cache(context['settings'])
context["encrypt_options"] = get_options(context['settings'], cache)
cache = load_cache(context["settings"])
context["encrypt_options"] = get_options(context["settings"], cache)
def register(settings):

42
src/sigal/plugins/extended_caching.py

@ -45,7 +45,7 @@ def load_metadata(album):
cache = album.gallery.metadataCache
# load album metadata
key = os.path.join(album.path, '_index')
key = os.path.join(album.path, "_index")
if key in cache:
data = cache[key]
@ -55,10 +55,10 @@ def load_metadata(album):
except FileNotFoundError:
pass
else:
if data.get('mod_date', -1) >= mod_date:
if data.get("mod_date", -1) >= mod_date:
# cache is good
if 'markdown_metadata' in data:
album.markdown_metadata = data['markdown_metadata']
if "markdown_metadata" in data:
album.markdown_metadata = data["markdown_metadata"]
# load media metadata
for media in album.medias:
@ -71,23 +71,23 @@ def load_metadata(album):
mod_date = int(get_mod_date(media.src_path))
except FileNotFoundError:
continue
if data.get('mod_date', -1) < mod_date:
if data.get("mod_date", -1) < mod_date:
continue # file_metadata needs updating
if 'file_metadata' in data:
media.file_metadata = data['file_metadata']
if 'exif' in data:
media.exif = data['exif']
if "file_metadata" in data:
media.file_metadata = data["file_metadata"]
if "exif" in data:
media.exif = data["exif"]
try:
mod_date = int(get_mod_date(media.markdown_metadata_filepath))
except FileNotFoundError:
continue
if data.get('meta_mod_date', -1) < mod_date:
if data.get("meta_mod_date", -1) < mod_date:
continue # markdown_metadata needs updating
if 'markdown_metadata' in data:
media.markdown_metadata = data['markdown_metadata']
if "markdown_metadata" in data:
media.markdown_metadata = data["markdown_metadata"]
def _restore_cache(gallery):
@ -116,10 +116,10 @@ def save_cache(gallery):
for album in gallery.albums.values():
try:
data = {
'mod_date': int(get_mod_date(album.markdown_metadata_filepath)),
'markdown_metadata': album.markdown_metadata,
"mod_date": int(get_mod_date(album.markdown_metadata_filepath)),
"markdown_metadata": album.markdown_metadata,
}
cache[os.path.join(album.path, '_index')] = data
cache[os.path.join(album.path, "_index")] = data
except FileNotFoundError:
pass
@ -130,18 +130,18 @@ def save_cache(gallery):
except FileNotFoundError:
continue
else:
data['mod_date'] = mod_date
data['file_metadata'] = media.file_metadata
if hasattr(media, 'exif'):
data['exif'] = media.exif
data["mod_date"] = mod_date
data["file_metadata"] = media.file_metadata
if hasattr(media, "exif"):
data["exif"] = media.exif
try:
meta_mod_date = int(get_mod_date(media.markdown_metadata_filepath))
except FileNotFoundError:
pass
else:
data['meta_mod_date'] = meta_mod_date
data['markdown_metadata'] = media.markdown_metadata
data["meta_mod_date"] = meta_mod_date
data["markdown_metadata"] = media.markdown_metadata
cache[os.path.join(media.path, media.dst_filename)] = data

36
src/sigal/plugins/feeds.py

@ -38,34 +38,34 @@ def generate_feeds(gallery):
medias.sort(key=lambda m: m.date, reverse=True)
settings = gallery.settings
if settings.get('rss_feed'):
generate_feed(gallery, medias, feed_type='rss', **settings['rss_feed'])
if settings.get('atom_feed'):
generate_feed(gallery, medias, feed_type='atom', **settings['atom_feed'])
if settings.get("rss_feed"):
generate_feed(gallery, medias, feed_type="rss", **settings["rss_feed"])
if settings.get("atom_feed"):
generate_feed(gallery, medias, feed_type="atom", **settings["atom_feed"])
def generate_feed(gallery, medias, feed_type=None, feed_url='', nb_items=0):
def generate_feed(gallery, medias, feed_type=None, feed_url="", nb_items=0):
from feedgenerator import Atom1Feed, Rss201rev2Feed
root_album = gallery.albums['.']
cls = Rss201rev2Feed if feed_type == 'rss' else Atom1Feed
root_album = gallery.albums["."]
cls = Rss201rev2Feed if feed_type == "rss" else Atom1Feed
feed = cls(
title=Markup.escape(root_album.title),
link='/',
link="/",
feed_url=feed_url,
description=Markup.escape(root_album.description).striptags(),
)
theme = gallery.settings['theme']
theme = gallery.settings["theme"]
nb_medias = len(medias)
nb_items = min(nb_items, nb_medias) if nb_items > 0 else nb_medias
base_url = feed_url.rsplit('/', maxsplit=1)[0]
base_url = feed_url.rsplit("/", maxsplit=1)[0]
for item in medias[:nb_items]:
if theme == 'galleria':
link = f'{base_url}/{item.path}/#{item.url}'
if theme == "galleria":
link = f"{base_url}/{item.path}/#{item.url}"
else:
link = f'{base_url}/{item.path}/'
link = f"{base_url}/{item.path}/"
feed.add_item(
title=Markup.escape(item.title or item.url),
@ -77,14 +77,14 @@ def generate_feed(gallery, medias, feed_type=None, feed_url='', nb_items=0):
base_url, item.path, item.thumbnail
),
# categories=item.tags if hasattr(item, 'tags') else None,
author_name=getattr(item, 'author', ''),
author_name=getattr(item, "author", ""),
pubdate=item.date or datetime.now(),
)
output_file = os.path.join(root_album.dst_path, feed_url.split('/')[-1])
logger.info('Generate %s feeds: %s', feed_type.upper(), output_file)
with open(output_file, 'w', encoding='utf8') as f:
feed.write(f, 'utf-8')
output_file = os.path.join(root_album.dst_path, feed_url.split("/")[-1])
logger.info("Generate %s feeds: %s", feed_type.upper(), output_file)
with open(output_file, "w", encoding="utf8") as f:
feed.write(f, "utf-8")
def register(settings):

82
src/sigal/plugins/nonmedia_files.py

@ -37,37 +37,37 @@ logger = logging.getLogger(__name__)
DEFAULT_CONFIG = {
'ext_as_thumb': True,
'ignore_ext': ['.md'],
'thumb_bg_color': (255, 255, 255),
'thumb_font': None,
'thumb_font_color': (0, 0, 0),
'thumb_font_size': 40,
"ext_as_thumb": True,
"ignore_ext": [".md"],
"thumb_bg_color": (255, 255, 255),
"thumb_font": None,
"thumb_font_color": (0, 0, 0),
"thumb_font_size": 40,
}
COMMON_MIME_TYPES = {
'.azw': 'application/vnd.amazon.ebook',
'.csv': 'text/csv',
'.epub': 'application/epub+zip',
'.pdf': 'application/pdf',
'.svg': 'image/svg+xml',
'.txt': 'text/plain',
'.zip': 'application/zip',
".azw": "application/vnd.amazon.ebook",
".csv": "text/csv",
".epub": "application/epub+zip",
".pdf": "application/pdf",
".svg": "image/svg+xml",
".txt": "text/plain",
".zip": "application/zip",
}
class NonMedia(Media):
"""Gather all informations on a non-media file."""
type = 'nonmedia'
type = "nonmedia"
def __init__(self, filename, path, settings):
super().__init__(filename, path, settings)
self.thumb_name = os.path.splitext(self.thumb_name)[0] + '.jpg'
self.thumb_name = os.path.splitext(self.thumb_name)[0] + ".jpg"
self.date = self._get_file_date()
self.mime = COMMON_MIME_TYPES.get(self.src_ext, 'application/octet-stream')
logger.debug('mime type %s', self.mime)
self.mime = COMMON_MIME_TYPES.get(self.src_ext, "application/octet-stream")
logger.debug("mime type %s", self.mime)
@property
def thumbnail(self):
@ -81,55 +81,55 @@ def generate_thumbnail(
text,
outname,
box,
bg_color=DEFAULT_CONFIG['thumb_bg_color'],
font=DEFAULT_CONFIG['thumb_font'],
font_color=DEFAULT_CONFIG['thumb_font_color'],
font_size=DEFAULT_CONFIG['thumb_font_size'],
bg_color=DEFAULT_CONFIG["thumb_bg_color"],
font=DEFAULT_CONFIG["thumb_font"],
font_color=DEFAULT_CONFIG["thumb_font_color"],
font_size=DEFAULT_CONFIG["thumb_font_size"],
options=None,
):
"""Create a thumbnail image."""
kwargs = {}
if font:
kwargs['font'] = ImageFont.truetype(font, font_size)
kwargs["font"] = ImageFont.truetype(font, font_size)
if font_color:
kwargs['fill'] = font_color
kwargs["fill"] = font_color
img = PILImage.new("RGB", box, bg_color)
anchor = (box[0] // 2, box[1] // 2)
d = ImageDraw.Draw(img)
logger.info(f"kwargs: {kwargs}")
d.text(anchor, text, anchor='mm', **kwargs)
d.text(anchor, text, anchor="mm", **kwargs)
outformat = 'JPEG'
logger.info('Save thumnail image: %s (%s)', outname, outformat)
outformat = "JPEG"
logger.info("Save thumnail image: %s (%s)", outname, outformat)
save_image(img, outname, outformat, options=options, autoconvert=True)
def process_thumb(media):
settings = media.settings
plugin_settings = settings.get('nonmedia_files_options', {})
utils.copy(media.src_path, media.dst_path, symlink=settings['orig_link'])
plugin_settings = settings.get("nonmedia_files_options", {})
utils.copy(media.src_path, media.dst_path, symlink=settings["orig_link"])
if plugin_settings.get('ext_as_thumb', DEFAULT_CONFIG['ext_as_thumb']):
logger.info('plugin_settings: %r', plugin_settings)
if plugin_settings.get("ext_as_thumb", DEFAULT_CONFIG["ext_as_thumb"]):
logger.info("plugin_settings: %r", plugin_settings)
kwargs = {}
for key in ('bg_color', 'font', 'font_color', 'font_size'):
if f'thumb_{key}' in plugin_settings:
kwargs[key] = plugin_settings[f'thumb_{key}']
for key in ("bg_color", "font", "font_color", "font_size"):
if f"thumb_{key}" in plugin_settings:
kwargs[key] = plugin_settings[f"thumb_{key}"]
generate_thumbnail(
media.src_ext[1:].upper(),
media.thumb_path,
settings['thumb_size'],
options=settings['jpg_options'],
settings["thumb_size"],
options=settings["jpg_options"],
**kwargs,
)
def process_nonmedia(media):
"""Process a non-media file: copy and create thumbnail."""
logger.info('Processing non-media file: %s', media.dst_filename)
logger.info("Processing non-media file: %s", media.dst_filename)
with utils.raise_if_debug() as status:
process_thumb(media)
@ -140,18 +140,18 @@ def process_nonmedia(media):
def album_file(album, filename, media=None):
if not media:
ext = os.path.splitext(filename)[1]
ext_ignore = album.settings.get('nonmedia_files_options', {}).get(
'ignore_ext', DEFAULT_CONFIG['ignore_ext']
ext_ignore = album.settings.get("nonmedia_files_options", {}).get(
"ignore_ext", DEFAULT_CONFIG["ignore_ext"]
)
if ext in ext_ignore:
logger.info('Ignoring non-media file: %s', filename)
logger.info("Ignoring non-media file: %s", filename)
else:
logger.info('Registering non-media file: %s', filename)
logger.info("Registering non-media file: %s", filename)
return NonMedia(filename, album.path, album.settings)
def process_file(media, processor=None):
if media.type == 'nonmedia':
if media.type == "nonmedia":
return process_nonmedia

14
src/sigal/plugins/titleregexp.py

@ -67,24 +67,24 @@ logger = logging.getLogger(__name__)
def titleregexp(album):
"""Create a title by regexping name"""
cfg = album.settings.get('titleregexp')
cfg = album.settings.get("titleregexp")
n = 0
total = 0
album_title_org = album.title
for r in cfg.get('regexp'):
for r in cfg.get("regexp"):
album.title, n = re.subn(
r.get('search'), r.get('replace'), album.title, r.get('count', 0)
r.get("search"), r.get("replace"), album.title, r.get("count", 0)
)
total += n
if n > 0:
for s in r.get('substitute', []):
for s in r.get("substitute", []):
album.title = album.title.replace(s[0], s[1])
if r.get('break', '') != '':
if r.get("break", "") != "":
break
for r in cfg.get('substitute', []):
for r in cfg.get("substitute", []):
album.title = album.title.replace(r[0], r[1])
if total > 0:
@ -92,7 +92,7 @@ def titleregexp(album):
def register(settings):
if settings.get('titleregexp'):
if settings.get("titleregexp"):
signals.album_initialized.connect(titleregexp)
else:
logger.warning("'titleregexp' setting not available!")

24
src/sigal/plugins/watermark.py

@ -47,8 +47,8 @@ from sigal import signals
def reduce_opacity(im, opacity):
"""Returns an image with reduced opacity."""
assert opacity >= 0 and opacity <= 1
if im.mode != 'RGBA':
im = im.convert('RGBA')
if im.mode != "RGBA":
im = im.convert("RGBA")
else:
im = im.copy()
alpha = im.split()[3]
@ -61,16 +61,16 @@ def watermark(im, mark, position, opacity=1):
"""Adds a watermark to an image."""
if opacity < 1:
mark = reduce_opacity(mark, opacity)
if im.mode != 'RGBA':
im = im.convert('RGBA')
if im.mode != "RGBA":
im = im.convert("RGBA")
# create a transparent layer the size of the image and draw the
# watermark in that layer.
layer = Image.new('RGBA', im.size, (0, 0, 0, 0))
if position == 'tile':
layer = Image.new("RGBA", im.size, (0, 0, 0, 0))
if position == "tile":
for y in range(0, im.size[1], mark.size[1]):
for x in range(0, im.size[0], mark.size[0]):
layer.paste(mark, (x, y))
elif position == 'scale':
elif position == "scale":
# scale, but preserve the aspect ratio
ratio = min(float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1])
w = int(mark.size[0] * ratio)
@ -85,16 +85,16 @@ def watermark(im, mark, position, opacity=1):
def add_watermark(img, settings=None):
logger = logging.getLogger(__name__)
logger.debug('Adding watermark to %r', img)
mark = Image.open(settings['watermark'])
position = settings.get('watermark_position', 'scale')
logger.debug("Adding watermark to %r", img)
mark = Image.open(settings["watermark"])
position = settings.get("watermark_position", "scale")
opacity = settings.get("watermark_opacity", 1)
return watermark(img, mark, position, opacity)
def register(settings):
logger = logging.getLogger(__name__)
if settings.get('watermark'):
if settings.get("watermark"):
signals.img_resized.connect(add_watermark)
else:
logger.warning('Watermark image is not set')
logger.warning("Watermark image is not set")

20
src/sigal/plugins/zip_gallery.py

@ -56,18 +56,18 @@ def _generate_album_zip(album):
archive with all original images of the corresponding directory.
"""
zip_gallery = album.settings['zip_gallery']
zip_gallery = album.settings["zip_gallery"]
if zip_gallery and len(album) > 0:
zip_gallery = zip_gallery.format(album=album)
archive_path = join(album.dst_path, zip_gallery)
if album.settings.get('zip_skip_if_exists', False) and isfile(archive_path):
if album.settings.get("zip_skip_if_exists", False) and isfile(archive_path):
logger.debug("Archive %s already created, passing", archive_path)
return zip_gallery
archive = zipfile.ZipFile(archive_path, 'w', allowZip64=True)
archive = zipfile.ZipFile(archive_path, "w", allowZip64=True)
attr = (
'src_path' if album.settings['zip_media_format'] == 'orig' else 'dst_path'
"src_path" if album.settings["zip_media_format"] == "orig" else "dst_path"
)
for p in album:
@ -75,10 +75,10 @@ def _generate_album_zip(album):
try:
archive.write(path, os.path.split(path)[1])
except OSError as e:
logger.warn('Failed to add %s to the ZIP: %s', p, e)
logger.warn("Failed to add %s to the ZIP: %s", p, e)
archive.close()
logger.debug('Created ZIP archive %s', archive_path)
logger.debug("Created ZIP archive %s", archive_path)
return zip_gallery
return False
@ -108,15 +108,15 @@ def generate_album_zip(album):
def nozip_gallery_file(album, settings=None):
"""Filesystem based switch to disable ZIP generation for an Album"""
Album.zip = cached_property(generate_album_zip)
Album.zip.__set_name__(Album, 'zip')
Album.zip.__set_name__(Album, "zip")
def check_settings(gallery):
if gallery.settings['zip_gallery'] and not isinstance(
gallery.settings['zip_gallery'], str
if gallery.settings["zip_gallery"] and not isinstance(
gallery.settings["zip_gallery"], str
):
logger.error("'zip_gallery' should be set to a filename")
gallery.settings['zip_gallery'] = False
gallery.settings["zip_gallery"] = False
def register(settings):

160
src/sigal/settings.py

@ -27,72 +27,72 @@ from os.path import abspath, isabs, join, normpath
from pprint import pformat
_DEFAULT_CONFIG = {
'albums_sort_attr': 'name',
'albums_sort_reverse': False,
'autorotate_images': True,
'colorbox_column_size': 3,
'copy_exif_data': False,
'datetime_format': '%c',
'destination': '_build',
'files_to_copy': (),
'galleria_theme': 'classic',
'google_analytics': '',
'google_tag_manager': '',
'ignore_directories': [],
'ignore_files': [],
'img_extensions': ['.jpg', '.jpeg', '.png', '.gif', '.tif', '.tiff', '.webp'],
'img_processor': 'ResizeToFit',
'img_size': (640, 480),
'img_format': None,
'index_in_url': False,
'jpg_options': {'quality': 85, 'optimize': True, 'progressive': True},
'keep_orig': False,
'html_language': 'en',
'leaflet_provider': 'OpenStreetMap.Mapnik',
'links': '',
'locale': '',
'make_thumbs': True,
'max_img_pixels': None,
'map_height': '500px',
'medias_sort_attr': 'filename',
'medias_sort_reverse': False,
'mp4_options': ['-crf', '23', '-strict', '-2'],
'mp4_options_second_pass': None,
'orig_dir': 'original',
'orig_link': False,
'rel_link': False,
'output_filename': 'index.html',
'piwik': {'tracker_url': '', 'site_id': 0},
'plugin_paths': [],
'plugins': [],
'site_logo': '',
'show_map': False,
'source': '',
'theme': 'colorbox',
'thumb_dir': 'thumbnails',
'thumb_fit': True,
'thumb_fit_centering': (0.5, 0.5),
'thumb_prefix': '',
'thumb_size': (200, 150),
'thumb_suffix': '',
'thumb_video_delay': 0,
'thumb_video_black_retries': 0,
'thumb_video_black_retry_offset': 1,
'thumb_video_black_max_colors': 4,
'title': '',
'use_orig': False,
'user_css': None,
'video_converter': 'ffmpeg',
'video_extensions': ['.3gp', '.avi', '.mkv', '.mov', '.mp4', '.ogv', '.webm'],
'video_format': 'webm',
'video_always_convert': False,
'video_size': (480, 360),
'watermark': '',
'webm_options': ['-crf', '10', '-b:v', '1.6M', '-qmin', '4', '-qmax', '63'],
'webm_options_second_pass': None,
'write_html': True,
'zip_gallery': False,
'zip_media_format': 'resized',
"albums_sort_attr": "name",
"albums_sort_reverse": False,
"autorotate_images": True,
"colorbox_column_size": 3,
"copy_exif_data": False,
"datetime_format": "%c",
"destination": "_build",
"files_to_copy": (),
"galleria_theme": "classic",
"google_analytics": "",
"google_tag_manager": "",
"ignore_directories": [],
"ignore_files": [],
"img_extensions": [".jpg", ".jpeg", ".png", ".gif", ".tif", ".tiff", ".webp"],
"img_processor": "ResizeToFit",
"img_size": (640, 480),
"img_format": None,
"index_in_url": False,
"jpg_options": {"quality": 85, "optimize": True, "progressive": True},
"keep_orig": False,
"html_language": "en",
"leaflet_provider": "OpenStreetMap.Mapnik",
"links": "",
"locale": "",
"make_thumbs": True,
"max_img_pixels": None,
"map_height": "500px",
"medias_sort_attr": "filename",
"medias_sort_reverse": False,
"mp4_options": ["-crf", "23", "-strict", "-2"],
"mp4_options_second_pass": None,
"orig_dir": "original",
"orig_link": False,
"rel_link": False,
"output_filename": "index.html",
"piwik": {"tracker_url": "", "site_id": 0},
"plugin_paths": [],
"plugins": [],
"site_logo": "",
"show_map": False,
"source": "",
"theme": "colorbox",
"thumb_dir": "thumbnails",
"thumb_fit": True,
"thumb_fit_centering": (0.5, 0.5),
"thumb_prefix": "",
"thumb_size": (200, 150),
"thumb_suffix": "",
"thumb_video_delay": 0,
"thumb_video_black_retries": 0,
"thumb_video_black_retry_offset": 1,
"thumb_video_black_max_colors": 4,
"title": "",
"use_orig": False,
"user_css": None,
"video_converter": "ffmpeg",
"video_extensions": [".3gp", ".avi", ".mkv", ".mov", ".mp4", ".ogv", ".webm"],
"video_format": "webm",
"video_always_convert": False,
"video_size": (480, 360),
"watermark": "",
"webm_options": ["-crf", "10", "-b:v", "1.6M", "-qmin", "4", "-qmax", "63"],
"webm_options_second_pass": None,
"write_html": True,
"zip_gallery": False,
"zip_media_format": "resized",
}
@ -119,12 +119,12 @@ def get_thumb(settings, filename):
path, filen = os.path.split(filename)
name, ext = os.path.splitext(filen)
if ext.lower() in settings['video_extensions']:
ext = '.jpg'
if ext.lower() in settings["video_extensions"]:
ext = ".jpg"
return join(
path,
settings['thumb_dir'],
settings['thumb_prefix'] + name + settings['thumb_suffix'] + ext,
settings["thumb_dir"],
settings["thumb_prefix"] + name + settings["thumb_suffix"] + ext,
)
@ -141,20 +141,20 @@ def read_settings(filename=None):
tempdict = {}
with open(filename) as f:
code = compile(f.read(), filename, 'exec')
code = compile(f.read(), filename, "exec")
exec(code, tempdict)
settings.update(
(k, v) for k, v in tempdict.items() if k not in ['__builtins__']
(k, v) for k, v in tempdict.items() if k not in ["__builtins__"]
)
# Make the paths relative to the settings file
paths = ['source', 'destination', 'watermark']
paths = ["source", "destination", "watermark"]
if os.path.isdir(join(settings_path, settings['theme'])) and os.path.isdir(
join(settings_path, settings['theme'], 'templates')
if os.path.isdir(join(settings_path, settings["theme"])) and os.path.isdir(
join(settings_path, settings["theme"], "templates")
):
paths.append('theme')
paths.append("theme")
for p in paths:
path = settings[p]
@ -162,7 +162,7 @@ def read_settings(filename=None):
settings[p] = abspath(normpath(join(settings_path, path)))
logger.debug("Rewrite %s : %s -> %s", p, path, settings[p])
for key in ('img_size', 'thumb_size', 'video_size'):
for key in ("img_size", "thumb_size", "video_size"):
if settings[key]:
w, h = settings[key]
if h > w:
@ -172,10 +172,10 @@ def read_settings(filename=None):
key,
)
if not settings['img_processor']:
logger.info('No Processor, images will not be resized')
if not settings["img_processor"]:
logger.info("No Processor, images will not be resized")
logger.debug('Settings:\n%s', pformat(settings, width=120))
logger.debug("Settings:\n%s", pformat(settings, width=120))
return settings

20
src/sigal/signals.py

@ -1,13 +1,13 @@
from blinker import signal
img_resized = signal('img_resized')
img_resized = signal("img_resized")
album_initialized = signal('album_initialized')
gallery_initialized = signal('gallery_initialized')
gallery_build = signal('gallery_build')
media_initialized = signal('media_initialized')
albums_sorted = signal('albums_sorted')
medias_sorted = signal('medias_sorted')
before_render = signal('before_render')
album_file = signal('album_file')
process_file = signal('process_file')
album_initialized = signal("album_initialized")
gallery_initialized = signal("gallery_initialized")
gallery_build = signal("gallery_build")
media_initialized = signal("media_initialized")
albums_sorted = signal("albums_sorted")
medias_sorted = signal("medias_sorted")
before_render = signal("before_render")
album_file = signal("album_file")
process_file = signal("process_file")

4
src/sigal/templates/sigal.conf.py

@ -13,7 +13,7 @@
# Source directory. Can be set here or as the first argument of the `sigal
# build` command
source = 'pictures'
source = "pictures"
# Destination directory. Can be set here or as the second argument of the
# `sigal build` command (default: '_build')
@ -22,7 +22,7 @@ source = 'pictures'
# Theme :
# - colorbox (default), galleria, photoswipe, or the path to a custom theme
# directory
theme = 'galleria'
theme = "galleria"
# Theme for galleria (https://galleriajs.github.io/themes/)
# galleria_theme = 'classic'

24
src/sigal/utils.py

@ -31,7 +31,7 @@ from sigal.settings import Status
logger = logging.getLogger(__name__)
MD = None
VIDEO_MIMES = {'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogv': 'video/ogg'}
VIDEO_MIMES = {".mp4": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg"}
class Devnull:
@ -77,8 +77,8 @@ def get_mod_date(path):
def url_from_path(path):
"""Transform path to url, converting backslashes to slashes if needed."""
if os.sep != '/':
path = '/'.join(path.split(os.sep))
if os.sep != "/":
path = "/".join(path.split(os.sep))
return quote(path)
@ -90,17 +90,17 @@ def read_markdown(filename):
# Use utf-8-sig codec to remove BOM if it is present. This is only possible
# this way prior to feeding the text to the markdown parser (which would
# also default to pure utf-8)
with open(filename, encoding='utf-8-sig') as f:
with open(filename, encoding="utf-8-sig") as f:
text = f.read()
if MD is None:
MD = Markdown(
extensions=[
'markdown.extensions.extra',
'markdown.extensions.meta',
'markdown.extensions.tables',
"markdown.extensions.extra",
"markdown.extensions.meta",
"markdown.extensions.tables",
],
output_format='html5',
output_format="html5",
)
else:
MD.reset()
@ -109,16 +109,16 @@ def read_markdown(filename):
MD.Meta = {}
# Mark HTML with Markup to prevent jinja2 autoescaping
output = {'description': Markup(MD.convert(text))}
output = {"description": Markup(MD.convert(text))}
try:
meta = MD.Meta.copy()
except AttributeError:
pass
else:
output['meta'] = meta
output["meta"] = meta
try:
output['title'] = MD.Meta['title'][0]
output["title"] = MD.Meta["title"][0]
except KeyError:
pass
@ -144,7 +144,7 @@ class raise_if_debug:
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
logger.info('Failed to process: %r', exc_value)
logger.info("Failed to process: %r", exc_value)
if logger.getEffectiveLevel() == logging.DEBUG:
# propagate the exception
return False

6
src/sigal/video.py

@ -256,9 +256,9 @@ def process_video(media):
fit=settings["thumb_fit"],
options=settings["jpg_options"],
converter=settings["video_converter"],
black_retries=settings['thumb_video_black_retries'],
black_offset=settings['thumb_video_black_retry_offset'],
black_max_colors=settings['thumb_video_black_max_colors'],
black_retries=settings["thumb_video_black_retries"],
black_offset=settings["thumb_video_black_retry_offset"],
black_max_colors=settings["thumb_video_black_max_colors"],
)
return status.value

10
src/sigal/writer.py

@ -76,10 +76,10 @@ class AbstractWriter:
# handle optional filters.py
filters_py = os.path.join(self.theme, "filters.py")
if os.path.exists(filters_py):
self.logger.info('Loading filters file: %s', filters_py)
module_spec = importlib.util.spec_from_file_location('filters', filters_py)
self.logger.info("Loading filters file: %s", filters_py)
module_spec = importlib.util.spec_from_file_location("filters", filters_py)
mod = importlib.util.module_from_spec(module_spec)
sys.modules['filters'] = mod
sys.modules["filters"] = mod
module_spec.loader.exec_module(mod)
for name in dir(mod):
if isinstance(getattr(mod, name), types.FunctionType):
@ -101,8 +101,8 @@ class AbstractWriter:
shutil.rmtree(self.theme_path)
for static_path in (
os.path.join(THEMES_PATH, 'default', 'static'),
os.path.join(self.theme, 'static'),
os.path.join(THEMES_PATH, "default", "static"),
os.path.join(self.theme, "static"),
):
shutil.copytree(static_path, self.theme_path, dirs_exist_ok=True)

8
tests/conftest.py

@ -9,10 +9,10 @@ from sigal import signals
from sigal.settings import read_settings
CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
BUILD_DIR = os.path.join(CURRENT_DIR, 'sample', '_build')
BUILD_DIR = os.path.join(CURRENT_DIR, "sample", "_build")
@pytest.fixture(scope='session', autouse=True)
@pytest.fixture(scope="session", autouse=True)
def remove_build():
"""Ensure that build directory does not exists before each test."""
if os.path.exists(BUILD_DIR):
@ -22,7 +22,7 @@ def remove_build():
@pytest.fixture
def settings():
"""Read the sample config file."""
return read_settings(os.path.join(CURRENT_DIR, 'sample', 'sigal.conf.py'))
return read_settings(os.path.join(CURRENT_DIR, "sample", "sigal.conf.py"))
@pytest.fixture()
@ -30,7 +30,7 @@ def disconnect_signals():
# Reset plugins
yield None
for name in dir(signals):
if not name.startswith('_'):
if not name.startswith("_"):
try:
sig = getattr(signals, name)
if isinstance(sig, blinker.Signal):

40
tests/test_cli.py

@ -6,15 +6,15 @@ from click.testing import CliRunner
from sigal import build, init, serve, set_meta
TESTGAL = join(os.path.abspath(os.path.dirname(__file__)), 'sample')
TESTGAL = join(os.path.abspath(os.path.dirname(__file__)), "sample")
def test_init(tmpdir):
config_file = str(tmpdir.join('sigal.conf.py'))
config_file = str(tmpdir.join("sigal.conf.py"))
runner = CliRunner()
result = runner.invoke(init, [config_file])
assert result.exit_code == 0
assert result.output.startswith('Sample config file created:')
assert result.output.startswith("Sample config file created:")
assert os.path.isfile(config_file)
result = runner.invoke(init, [config_file])
@ -26,29 +26,29 @@ def test_init(tmpdir):
def test_build(tmpdir, disconnect_signals):
runner = CliRunner()
config_file = str(tmpdir.join('sigal.conf.py'))
tmpdir.mkdir('pictures')
config_file = str(tmpdir.join("sigal.conf.py"))
tmpdir.mkdir("pictures")
tmpdir = str(tmpdir)
cwd = os.getcwd()
try:
result = runner.invoke(init, [config_file])
assert result.exit_code == 0
os.symlink(join(TESTGAL, 'watermark.png'), join(tmpdir, 'watermark.png'))
os.symlink(join(TESTGAL, "watermark.png"), join(tmpdir, "watermark.png"))
os.symlink(
join(TESTGAL, 'pictures', 'dir2', 'KeckObservatory20071020.jpg'),
join(tmpdir, 'pictures', 'KeckObservatory20071020.jpg'),
join(TESTGAL, "pictures", "dir2", "KeckObservatory20071020.jpg"),
join(tmpdir, "pictures", "KeckObservatory20071020.jpg"),
)
result = runner.invoke(build, ['-n', 1, '--debug'])
result = runner.invoke(build, ["-n", 1, "--debug"])
assert result.exit_code == 1
os.chdir(tmpdir)
result = runner.invoke(build, ['foo', '-n', 1, '--debug'])
result = runner.invoke(build, ["foo", "-n", 1, "--debug"])
assert result.exit_code == 1
result = runner.invoke(build, ['pictures', 'pictures/out', '-n', 1, '--debug'])
result = runner.invoke(build, ["pictures", "pictures/out", "-n", 1, "--debug"])
assert result.exit_code == 1
with open(config_file) as f:
@ -70,29 +70,29 @@ rss_feed = {'feed_url': 'http://example.org/feed.rss', 'nb_items': 10}
atom_feed = {'feed_url': 'http://example.org/feed.atom', 'nb_items': 10}
"""
with open(config_file, 'w') as f:
with open(config_file, "w") as f:
f.write(text)
result = runner.invoke(
build, ['pictures', 'build', '--title', 'Testing build', '-n', 1, '--debug']
build, ["pictures", "build", "--title", "Testing build", "-n", 1, "--debug"]
)
assert result.exit_code == 0
assert os.path.isfile(
join(tmpdir, 'build', 'thumbnails', 'KeckObservatory20071020.jpg')
join(tmpdir, "build", "thumbnails", "KeckObservatory20071020.jpg")
)
assert os.path.isfile(join(tmpdir, 'build', 'feed.atom'))
assert os.path.isfile(join(tmpdir, 'build', 'feed.rss'))
assert os.path.isfile(join(tmpdir, 'build', 'watermark.png'))
assert os.path.isfile(join(tmpdir, "build", "feed.atom"))
assert os.path.isfile(join(tmpdir, "build", "feed.rss"))
assert os.path.isfile(join(tmpdir, "build", "watermark.png"))
finally:
os.chdir(cwd)
# Reset logger
logger = logging.getLogger('sigal')
logger = logging.getLogger("sigal")
logger.handlers[:] = []
logger.setLevel(logging.INFO)
def test_serve(tmpdir):
config_file = str(tmpdir.join('sigal.conf.py'))
config_file = str(tmpdir.join("sigal.conf.py"))
runner = CliRunner()
result = runner.invoke(init, [config_file])
assert result.exit_code == 0
@ -100,7 +100,7 @@ def test_serve(tmpdir):
result = runner.invoke(serve)
assert result.exit_code == 2
result = runner.invoke(serve, ['-c', config_file])
result = runner.invoke(serve, ["-c", config_file])
assert result.exit_code == 1

24
tests/test_compress_assets_plugin.py

@ -12,18 +12,18 @@ CURRENT_DIR = os.path.dirname(__file__)
def make_gallery(settings, tmpdir, method):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
# Really speed up testing
settings['use_orig'] = True
settings["use_orig"] = True
if "sigal.plugins.compress_assets" not in settings["plugins"]:
settings['plugins'] += ["sigal.plugins.compress_assets"]
settings["plugins"] += ["sigal.plugins.compress_assets"]
# Set method
settings.setdefault('compress_assets_options', {})['method'] = method
settings.setdefault("compress_assets_options", {})["method"] = method
compress_options = compress_assets.DEFAULT_SETTINGS.copy()
# The key was created by the previous setdefault if needed
compress_options.update(settings['compress_assets_options'])
compress_options.update(settings["compress_assets_options"])
init_plugins(settings)
gal = Gallery(settings)
@ -36,7 +36,7 @@ def walk_destination(destination, suffixes, compress_suffix):
for path, dirs, files in os.walk(destination):
for file in files:
original_filename = os.path.join(path, file)
compressed_filename = '{}.{}'.format(
compressed_filename = "{}.{}".format(
os.path.join(path, file), compress_suffix
)
path_exists = os.path.exists(compressed_filename)
@ -53,7 +53,7 @@ def walk_destination(destination, suffixes, compress_suffix):
@pytest.mark.parametrize(
"method,compress_suffix,test_import",
[('gzip', 'gz', None), ('zopfli', 'gz', 'zopfli.gzip'), ('brotli', 'br', 'brotli')],
[("gzip", "gz", None), ("zopfli", "gz", "zopfli.gzip"), ("brotli", "br", "brotli")],
)
def test_compress(
disconnect_signals, settings, tmpdir, method, compress_suffix, test_import
@ -65,16 +65,16 @@ def test_compress(
for _ in range(2):
compress_options = make_gallery(settings, tmpdir, method)
walk_destination(
settings['destination'], compress_options['suffixes'], compress_suffix
settings["destination"], compress_options["suffixes"], compress_suffix
)
@pytest.mark.parametrize(
"method,compress_suffix,mask",
[
('zopfli', 'gz', 'zopfli.gzip'),
('brotli', 'br', 'brotli'),
('__does_not_exist__', 'br', None),
("zopfli", "gz", "zopfli.gzip"),
("brotli", "br", "brotli"),
("__does_not_exist__", "br", None),
],
)
def test_failed_compress(
@ -84,5 +84,5 @@ def test_failed_compress(
with mock.patch.dict(sys.modules, {mask: None}):
make_gallery(settings, tmpdir, method)
walk_destination(
settings['destination'], [], compress_suffix # No file should be compressed
settings["destination"], [], compress_suffix # No file should be compressed
)

20
tests/test_encrypt.py

@ -22,9 +22,9 @@ def get_key_tag(settings):
def test_encrypt(settings, tmpdir, disconnect_signals, caplog):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
if "sigal.plugins.encrypt" not in settings["plugins"]:
settings['plugins'] += ["sigal.plugins.encrypt"]
settings["plugins"] += ["sigal.plugins.encrypt"]
init_plugins(settings)
gal = Gallery(settings)
@ -32,17 +32,17 @@ def test_encrypt(settings, tmpdir, disconnect_signals, caplog):
with pytest.raises(ValueError, match="no encrypt_options in settings"):
gal.build()
settings['encrypt_options'] = {}
settings["encrypt_options"] = {}
gal = Gallery(settings)
with pytest.raises(ValueError, match="no password provided"):
gal.build()
settings['encrypt_options'] = {
'password': 'password',
'ask_password': True,
'encrypt_symlinked_originals': False,
settings["encrypt_options"] = {
"password": "password",
"ask_password": True,
"encrypt_symlinked_originals": False,
}
gal = Gallery(settings)
@ -80,20 +80,20 @@ def test_encrypt(settings, tmpdir, disconnect_signals, caplog):
endec.decrypt(key, infile, outfile, tag)
# check static files have been copied
static = os.path.join(settings["destination"], 'static')
static = os.path.join(settings["destination"], "static")
assert os.path.isfile(os.path.join(static, "decrypt.js"))
assert os.path.isfile(os.path.join(static, "keycheck.txt"))
assert os.path.isfile(os.path.join(settings["destination"], "sw.js"))
# check keycheck file
with open(
os.path.join(settings["destination"], 'static', "keycheck.txt"), "rb"
os.path.join(settings["destination"], "static", "keycheck.txt"), "rb"
) as infile:
with BytesIO() as outfile:
endec.decrypt(key, infile, outfile, tag)
caplog.clear()
caplog.set_level('DEBUG')
caplog.set_level("DEBUG")
gal = Gallery(settings)
gal.build()
# Doesn't work on Actions ...

24
tests/test_extended_caching.py

@ -8,11 +8,11 @@ CURRENT_DIR = os.path.dirname(__file__)
def test_save_cache(settings, tmpdir):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
gal = Gallery(settings, ncpu=1)
extended_caching.save_cache(gal)
cachePath = os.path.join(settings['destination'], ".metadata_cache")
cachePath = os.path.join(settings["destination"], ".metadata_cache")
assert os.path.isfile(cachePath)
@ -23,17 +23,17 @@ def test_save_cache(settings, tmpdir):
album = gal.albums["exifTest"]
cache_img = cache["exifTest/21.jpg"]
assert cache_img["exif"] == album.medias[0].exif
assert 'markdown_metadata' not in cache_img
assert "markdown_metadata" not in cache_img
assert cache_img["file_metadata"] == album.medias[0].file_metadata
cache_img = cache["exifTest/22.jpg"]
assert cache_img["exif"] == album.medias[1].exif
assert 'markdown_metadata' not in cache_img
assert "markdown_metadata" not in cache_img
assert cache_img["file_metadata"] == album.medias[1].file_metadata
cache_img = cache["exifTest/noexif.png"]
assert cache_img["exif"] == album.medias[2].exif
assert 'markdown_metadata' not in cache_img
assert "markdown_metadata" not in cache_img
assert cache_img["file_metadata"] == album.medias[2].file_metadata
# test iptc and md
@ -42,7 +42,7 @@ def test_save_cache(settings, tmpdir):
cache_img = cache["iptcTest/1.jpg"]
assert cache_img["file_metadata"] == album.medias[0].file_metadata
assert 'markdown_metadata' not in cache_img
assert "markdown_metadata" not in cache_img
cache_img = cache["iptcTest/2.jpg"]
assert cache_img["markdown_metadata"] == album.medias[1].markdown_metadata
@ -56,7 +56,7 @@ def test_save_cache(settings, tmpdir):
def test_restore_cache(settings, tmpdir):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
gal1 = Gallery(settings, ncpu=1)
gal2 = Gallery(settings, ncpu=1)
extended_caching.save_cache(gal1)
@ -64,16 +64,16 @@ def test_restore_cache(settings, tmpdir):
assert gal1.metadataCache == gal2.metadataCache
# test bad cache
cachePath = os.path.join(settings['destination'], ".metadata_cache")
with open(cachePath, 'w') as f:
f.write('bad pickle file')
cachePath = os.path.join(settings["destination"], ".metadata_cache")
with open(cachePath, "w") as f:
f.write("bad pickle file")
extended_caching._restore_cache(gal2)
assert gal2.metadataCache == {}
def test_load_exif(settings, tmpdir):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
gal1 = Gallery(settings, ncpu=1)
gal1.albums["exifTest"].medias[2].exif = "blafoo"
# set mod_date in future, to force these values
@ -99,7 +99,7 @@ def test_load_exif(settings, tmpdir):
def test_load_metadata_missing(settings, tmpdir):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
gal = Gallery(settings, ncpu=1)
extended_caching.save_cache(gal)
assert gal.metadataCache

426
tests/test_gallery.py

@ -13,86 +13,86 @@ from sigal.video import SubprocessException
CURRENT_DIR = os.path.dirname(__file__)
REF = {
'dir1': {
'title': 'An example gallery',
'name': 'dir1',
'thumbnail': 'dir1/test1/thumbnails/11.tn.jpg',
'subdirs': ['test1', 'test2', 'test3'],
'medias': [],
"dir1": {
"title": "An example gallery",
"name": "dir1",
"thumbnail": "dir1/test1/thumbnails/11.tn.jpg",
"subdirs": ["test1", "test2", "test3"],
"medias": [],
},
'dir1/test1': {
'title': 'An example sub-category',
'name': 'test1',
'thumbnail': 'test1/thumbnails/11.tn.jpg',
'subdirs': [],
'medias': [
'11.jpg',
'CMB_Timeline300_no_WMAP.jpg',
'flickr_jerquiaga_2394751088_cc-by-nc.jpg',
'example.gif',
"dir1/test1": {
"title": "An example sub-category",
"name": "test1",
"thumbnail": "test1/thumbnails/11.tn.jpg",
"subdirs": [],
"medias": [
"11.jpg",
"CMB_Timeline300_no_WMAP.jpg",
"flickr_jerquiaga_2394751088_cc-by-nc.jpg",
"example.gif",
],
},
'dir1/test2': {
'title': 'test2',
'name': 'test2',
'thumbnail': 'test2/thumbnails/21.tn.tiff',
'subdirs': [],
'medias': ['21.tiff', '22.jpg', 'CMB_Timeline300_no_WMAP.jpg'],
"dir1/test2": {
"title": "test2",
"name": "test2",
"thumbnail": "test2/thumbnails/21.tn.tiff",
"subdirs": [],
"medias": ["21.tiff", "22.jpg", "CMB_Timeline300_no_WMAP.jpg"],
},
'dir1/test3': {
'title': '01 First title alphabetically',
'name': 'test3',
'thumbnail': 'test3/thumbnails/3.tn.jpg',
'subdirs': [],
'medias': ['3.jpg'],
"dir1/test3": {
"title": "01 First title alphabetically",
"name": "test3",
"thumbnail": "test3/thumbnails/3.tn.jpg",
"subdirs": [],
"medias": ["3.jpg"],
},
'dir2': {
'title': 'Another example gallery with a very long name',
'name': 'dir2',
'thumbnail': 'dir2/thumbnails/m57_the_ring_nebula-587px.tn.jpg',
'subdirs': [],
'medias': [
'KeckObservatory20071020.jpg',
'Hubble Interacting Galaxy NGC 5257.jpg',
'Hubble ultra deep field.jpg',
'm57_the_ring_nebula-587px.jpg',
"dir2": {
"title": "Another example gallery with a very long name",
"name": "dir2",
"thumbnail": "dir2/thumbnails/m57_the_ring_nebula-587px.tn.jpg",
"subdirs": [],
"medias": [
"KeckObservatory20071020.jpg",
"Hubble Interacting Galaxy NGC 5257.jpg",
"Hubble ultra deep field.jpg",
"m57_the_ring_nebula-587px.jpg",
],
},
'accentué': {
'title': 'accentué',
'name': 'accentué',
'thumbnail': 'accentu%C3%A9/thumbnails/h%C3%A9lico%C3%AFde.tn.jpg',
'subdirs': [],
'medias': ['hélicoïde.jpg', '11.jpg'],
"accentué": {
"title": "accentué",
"name": "accentué",
"thumbnail": "accentu%C3%A9/thumbnails/h%C3%A9lico%C3%AFde.tn.jpg",
"subdirs": [],
"medias": ["hélicoïde.jpg", "11.jpg"],
},
'video': {
'title': 'video',
'name': 'video',
'thumbnail': 'video/thumbnails/example%20video.tn.jpg',
'subdirs': [],
'medias': ['example video.ogv'],
"video": {
"title": "video",
"name": "video",
"thumbnail": "video/thumbnails/example%20video.tn.jpg",
"subdirs": [],
"medias": ["example video.ogv"],
},
'webp': {
'title': 'webp',
'name': 'webp',
'thumbnail': 'webp/thumbnails/_MG_7805_lossy80.tn.webp',
'subdirs': [],
'medias': ['_MG_7805_lossy80.webp', '_MG_7808_lossy80.webp'],
"webp": {
"title": "webp",
"name": "webp",
"thumbnail": "webp/thumbnails/_MG_7805_lossy80.tn.webp",
"subdirs": [],
"medias": ["_MG_7805_lossy80.webp", "_MG_7808_lossy80.webp"],
},
}
def test_media(settings):
m = Media('11.jpg', 'dir1/test1', settings)
path = join('dir1', 'test1')
file_path = join(path, '11.jpg')
thumb = join('thumbnails', '11.tn.jpg')
assert m.dst_filename == '11.jpg'
assert m.src_path == join(settings['source'], file_path)
assert m.dst_path == join(settings['destination'], file_path)
m = Media("11.jpg", "dir1/test1", settings)
path = join("dir1", "test1")
file_path = join(path, "11.jpg")
thumb = join("thumbnails", "11.tn.jpg")
assert m.dst_filename == "11.jpg"
assert m.src_path == join(settings["source"], file_path)
assert m.dst_path == join(settings["destination"], file_path)
assert m.thumb_name == thumb
assert m.thumb_path == join(settings['destination'], path, thumb)
assert m.thumb_path == join(settings["destination"], path, thumb)
assert m.title == "Foo Bar"
assert m.description == "<p>This is a funny description of this image</p>"
@ -101,223 +101,223 @@ def test_media(settings):
def test_media_orig(settings, tmpdir):
settings['keep_orig'] = False
m = Media('11.jpg', 'dir1/test1', settings)
settings["keep_orig"] = False
m = Media("11.jpg", "dir1/test1", settings)
assert m.big is None
settings['keep_orig'] = True
settings['destination'] = str(tmpdir)
settings["keep_orig"] = True
settings["destination"] = str(tmpdir)
m = Image('11.jpg', 'dir1/test1', settings)
assert m.big == 'original/11.jpg'
m = Image("11.jpg", "dir1/test1", settings)
assert m.big == "original/11.jpg"
m = Video('example video.ogv', 'video', settings)
assert m.dst_filename == 'example video.webm'
assert m.big_url == 'original/example%20video.ogv'
assert os.path.isfile(join(settings['destination'], m.path, m.big))
m = Video("example video.ogv", "video", settings)
assert m.dst_filename == "example video.webm"
assert m.big_url == "original/example%20video.ogv"
assert os.path.isfile(join(settings["destination"], m.path, m.big))
settings['use_orig'] = True
settings["use_orig"] = True
m = Image('21.jpg', 'dir1/test2', settings)
assert m.big == '21.jpg'
m = Image("21.jpg", "dir1/test2", settings)
assert m.big == "21.jpg"
def test_media_iptc_override(settings):
img_with_md = Image('2.jpg', 'iptcTest', settings)
img_with_md = Image("2.jpg", "iptcTest", settings)
assert img_with_md.title == "Markdown title beats iptc"
# Markdown parsing adds formatting. Let's just focus on content
assert "Markdown description beats iptc" in img_with_md.description
img_no_md = Image('1.jpg', 'iptcTest', settings)
img_no_md = Image("1.jpg", "iptcTest", settings)
assert (
img_no_md.title
== 'Haemostratulus clouds over Canberra - 2005-12-28 at 03-25-07'
== "Haemostratulus clouds over Canberra - 2005-12-28 at 03-25-07"
)
assert (
img_no_md.description == '"Haemo" because they look like haemoglobin '
'cells and "stratulus" because I can\'t work out whether '
'they\'re Stratus or Cumulus clouds.\nWe\'re driving down '
'the main drag in Canberra so it\'s Parliament House that '
'you can see at the end of the road.'
"they're Stratus or Cumulus clouds.\nWe're driving down "
"the main drag in Canberra so it's Parliament House that "
"you can see at the end of the road."
)
def test_media_img_format(settings):
settings['img_format'] = 'jpeg'
m = Image('11.tiff', 'dir1/test1', settings)
path = join('dir1', 'test1')
thumb = join('thumbnails', '11.tn.jpeg')
assert m.dst_filename == '11.jpeg'
assert m.src_path == join(settings['source'], path, '11.tiff')
assert m.dst_path == join(settings['destination'], path, '11.jpeg')
settings["img_format"] = "jpeg"
m = Image("11.tiff", "dir1/test1", settings)
path = join("dir1", "test1")
thumb = join("thumbnails", "11.tn.jpeg")
assert m.dst_filename == "11.jpeg"
assert m.src_path == join(settings["source"], path, "11.tiff")
assert m.dst_path == join(settings["destination"], path, "11.jpeg")
assert m.thumb_name == thumb
assert m.thumb_path == join(settings['destination'], path, thumb)
assert m.thumb_path == join(settings["destination"], path, thumb)
assert m.title == "Foo Bar"
assert m.description == "<p>This is a funny description of this image</p>"
file_path = join(path, '11.tiff')
file_path = join(path, "11.tiff")
assert repr(m) == f"<Image>('{file_path}')"
assert str(m) == file_path
def test_image(settings, tmpdir):
settings['destination'] = str(tmpdir)
settings['datetime_format'] = '%d/%m/%Y'
m = Image('11.jpg', 'dir1/test1', settings)
settings["destination"] = str(tmpdir)
settings["datetime_format"] = "%d/%m/%Y"
m = Image("11.jpg", "dir1/test1", settings)
assert m.date == datetime.datetime(2006, 1, 22, 10, 32, 42)
assert m.exif['datetime'] == '22/01/2006'
assert m.exif["datetime"] == "22/01/2006"
os.makedirs(join(settings['destination'], 'dir1', 'test1', 'thumbnails'))
assert m.thumbnail == join('thumbnails', '11.tn.jpg')
os.makedirs(join(settings["destination"], "dir1", "test1", "thumbnails"))
assert m.thumbnail == join("thumbnails", "11.tn.jpg")
assert os.path.isfile(m.thumb_path)
def test_video(settings, tmpdir):
settings['destination'] = str(tmpdir)
m = Video('example video.ogv', 'video', settings)
settings["destination"] = str(tmpdir)
m = Video("example video.ogv", "video", settings)
src_path = join('video', 'example video.ogv')
src_path = join("video", "example video.ogv")
assert str(m) == src_path
file_path = join('video', 'example video.webm')
assert m.dst_path == join(settings['destination'], file_path)
file_path = join("video", "example video.webm")
assert m.dst_path == join(settings["destination"], file_path)
os.makedirs(join(settings['destination'], 'video', 'thumbnails'))
assert m.thumbnail == join('thumbnails', 'example%20video.tn.jpg')
os.makedirs(join(settings["destination"], "video", "thumbnails"))
assert m.thumbnail == join("thumbnails", "example%20video.tn.jpg")
assert os.path.isfile(m.thumb_path)
@pytest.mark.parametrize("path,album", REF.items())
def test_album(path, album, settings, tmpdir):
gal = Gallery(settings, ncpu=1)
a = Album(path, settings, album['subdirs'], album['medias'], gal)
a = Album(path, settings, album["subdirs"], album["medias"], gal)
assert a.title == album['title']
assert a.name == album['name']
assert a.subdirs == album['subdirs']
assert a.thumbnail == album['thumbnail']
if path == 'video':
assert a.title == album["title"]
assert a.name == album["name"]
assert a.subdirs == album["subdirs"]
assert a.thumbnail == album["thumbnail"]
if path == "video":
assert list(a.images) == []
assert [m.dst_filename for m in a.medias] == [
album['medias'][0].replace('.ogv', '.webm')
album["medias"][0].replace(".ogv", ".webm")
]
else:
assert list(a.videos) == []
assert [m.dst_filename for m in a.medias] == album['medias']
assert len(a) == len(album['medias'])
assert [m.dst_filename for m in a.medias] == album["medias"]
assert len(a) == len(album["medias"])
def test_albums_sort(settings):
gal = Gallery(settings, ncpu=1)
album = REF['dir1']
subdirs = list(album['subdirs'])
album = REF["dir1"]
subdirs = list(album["subdirs"])
settings['albums_sort_reverse'] = False
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('')
settings["albums_sort_reverse"] = False
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("")
assert [alb.name for alb in a.albums] == subdirs
settings['albums_sort_reverse'] = True
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('')
settings["albums_sort_reverse"] = True
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("")
assert [alb.name for alb in a.albums] == list(reversed(subdirs))
titles = [im.title for im in a.albums]
titles.sort()
settings['albums_sort_reverse'] = False
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('title')
settings["albums_sort_reverse"] = False
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("title")
assert [im.title for im in a.albums] == titles
settings['albums_sort_reverse'] = True
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('title')
settings["albums_sort_reverse"] = True
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("title")
assert [im.title for im in a.albums] == list(reversed(titles))
orders = ['01', '02', '03']
orders = ["01", "02", "03"]
orders.sort()
settings['albums_sort_reverse'] = False
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('meta.order')
assert [d.meta['order'][0] for d in a.albums] == orders
settings["albums_sort_reverse"] = False
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("meta.order")
assert [d.meta["order"][0] for d in a.albums] == orders
settings['albums_sort_reverse'] = True
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs('meta.order')
assert [d.meta['order'][0] for d in a.albums] == list(reversed(orders))
settings["albums_sort_reverse"] = True
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs("meta.order")
assert [d.meta["order"][0] for d in a.albums] == list(reversed(orders))
settings['albums_sort_reverse'] = False
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs(['meta.partialorder', 'meta.order'])
assert [d.name for d in a.albums] == list(['test1', 'test2', 'test3'])
settings["albums_sort_reverse"] = False
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs(["meta.partialorder", "meta.order"])
assert [d.name for d in a.albums] == list(["test1", "test2", "test3"])
settings['albums_sort_reverse'] = False
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs(['meta.partialorderb', 'name'])
assert [d.name for d in a.albums] == list(['test2', 'test3', 'test1'])
settings["albums_sort_reverse"] = False
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs(["meta.partialorderb", "name"])
assert [d.name for d in a.albums] == list(["test2", "test3", "test1"])
settings['albums_sort_reverse'] = True
a = Album('dir1', settings, album['subdirs'], album['medias'], gal)
a.sort_subdirs(['meta.partialorderb', 'name'])
assert [d.name for d in a.albums] == list(['test1', 'test3', 'test2'])
settings["albums_sort_reverse"] = True
a = Album("dir1", settings, album["subdirs"], album["medias"], gal)
a.sort_subdirs(["meta.partialorderb", "name"])
assert [d.name for d in a.albums] == list(["test1", "test3", "test2"])
def test_medias_sort(settings):
gal = Gallery(settings, ncpu=1)
album = REF['dir1/test2']
settings['medias_sort_reverse'] = True
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
assert [im.dst_filename for im in a.images] == list(reversed(album['medias']))
settings['medias_sort_attr'] = 'date'
settings['medias_sort_reverse'] = False
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
assert a.medias[0].src_filename == '22.jpg'
settings['medias_sort_attr'] = 'meta.order'
settings['medias_sort_reverse'] = False
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
album = REF["dir1/test2"]
settings["medias_sort_reverse"] = True
a = Album("dir1/test2", settings, album["subdirs"], album["medias"], gal)
a.sort_medias(settings["medias_sort_attr"])
assert [im.dst_filename for im in a.images] == list(reversed(album["medias"]))
settings["medias_sort_attr"] = "date"
settings["medias_sort_reverse"] = False
a = Album("dir1/test2", settings, album["subdirs"], album["medias"], gal)
a.sort_medias(settings["medias_sort_attr"])
assert a.medias[0].src_filename == "22.jpg"
settings["medias_sort_attr"] = "meta.order"
settings["medias_sort_reverse"] = False
a = Album("dir1/test2", settings, album["subdirs"], album["medias"], gal)
a.sort_medias(settings["medias_sort_attr"])
assert [im.dst_filename for im in a.images] == [
'21.tiff',
'22.jpg',
'CMB_Timeline300_no_WMAP.jpg',
"21.tiff",
"22.jpg",
"CMB_Timeline300_no_WMAP.jpg",
]
def test_gallery(settings, tmp_path, caplog):
"Test the Gallery class."
caplog.set_level('ERROR')
settings['destination'] = str(tmp_path)
settings['user_css'] = str(tmp_path / 'my.css')
settings['webm_options'] = ['-missing-option', 'foobar']
caplog.set_level("ERROR")
settings["destination"] = str(tmp_path)
settings["user_css"] = str(tmp_path / "my.css")
settings["webm_options"] = ["-missing-option", "foobar"]
gal = Gallery(settings, ncpu=1)
gal.build()
assert re.match(r'CSS file .* could not be found', caplog.records[3].message)
assert re.match(r"CSS file .* could not be found", caplog.records[3].message)
with open(tmp_path / 'my.css', mode='w') as f:
f.write('color: red')
with open(tmp_path / "my.css", mode="w") as f:
f.write("color: red")
gal.build()
mycss = os.path.join(settings['destination'], 'static', 'my.css')
mycss = os.path.join(settings["destination"], "static", "my.css")
assert os.path.isfile(mycss)
out_html = os.path.join(settings['destination'], 'index.html')
out_html = os.path.join(settings["destination"], "index.html")
assert os.path.isfile(out_html)
with open(out_html) as f:
html = f.read()
assert '<title>Sigal test gallery - Sigal test gallery ☺</title>' in html
assert "<title>Sigal test gallery - Sigal test gallery ☺</title>" in html
assert '<link rel="stylesheet" href="static/my.css">' in html
logger = logging.getLogger('sigal')
logger = logging.getLogger("sigal")
logger.setLevel(logging.DEBUG)
try:
gal = Gallery(settings, ncpu=1)
@ -328,33 +328,33 @@ def test_gallery(settings, tmp_path, caplog):
def test_custom_theme(settings, tmp_path, caplog):
theme_path = tmp_path / 'mytheme'
tpl_path = theme_path / 'templates'
theme_path = tmp_path / "mytheme"
tpl_path = theme_path / "templates"
settings['destination'] = str(tmp_path / 'build')
settings['source'] = os.path.join(settings['source'], 'encryptTest')
settings['theme'] = str(theme_path)
settings['title'] = 'My gallery'
settings["destination"] = str(tmp_path / "build")
settings["source"] = os.path.join(settings["source"], "encryptTest")
settings["theme"] = str(theme_path)
settings["title"] = "My gallery"
gal = Gallery(settings, ncpu=1)
with pytest.raises(Exception, match='Impossible to find the theme'):
with pytest.raises(Exception, match="Impossible to find the theme"):
gal.build()
tpl_path.mkdir(parents=True)
(theme_path / 'static').mkdir(parents=True)
(theme_path / "static").mkdir(parents=True)
with pytest.raises(SystemExit):
gal.build()
assert caplog.records[0].message.startswith(
'The template album.html was not found in template folder'
"The template album.html was not found in template folder"
)
with open(tpl_path / 'album.html', mode='w') as f:
with open(tpl_path / "album.html", mode="w") as f:
f.write(""" {{ settings.title|myfilter }} """)
with open(tpl_path / 'album_list.html', mode='w') as f:
with open(tpl_path / "album_list.html", mode="w") as f:
f.write(""" {{ settings.title|myfilter }} """)
with open(theme_path / 'filters.py', mode='w') as f:
with open(theme_path / "filters.py", mode="w") as f:
f.write(
"""
def myfilter(value):
@ -365,13 +365,13 @@ def myfilter(value):
gal = Gallery(settings, ncpu=1)
gal.build()
out_html = os.path.join(settings['destination'], 'index.html')
out_html = os.path.join(settings["destination"], "index.html")
assert os.path.isfile(out_html)
with open(out_html) as f:
html = f.read()
assert 'My gallery is very nice' in html
assert "My gallery is very nice" in html
def test_gallery_max_img_pixels(settings, tmpdir, monkeypatch):
@ -379,23 +379,23 @@ def test_gallery_max_img_pixels(settings, tmpdir, monkeypatch):
# monkeypatch is used here to reset the value to the PIL default.
# This value does not matter, other than it is "large"
# to show that settings['max_img_pixels'] works.
monkeypatch.setattr('PIL.Image.MAX_IMAGE_PIXELS', 100_000_000)
monkeypatch.setattr("PIL.Image.MAX_IMAGE_PIXELS", 100_000_000)
with open(str(tmpdir.join('my.css')), mode='w') as f:
f.write('color: red')
with open(str(tmpdir.join("my.css")), mode="w") as f:
f.write("color: red")
settings['destination'] = str(tmpdir)
settings['user_css'] = str(tmpdir.join('my.css'))
settings['max_img_pixels'] = 5000
settings["destination"] = str(tmpdir)
settings["user_css"] = str(tmpdir.join("my.css"))
settings["max_img_pixels"] = 5000
logger = logging.getLogger('sigal')
logger = logging.getLogger("sigal")
logger.setLevel(logging.DEBUG)
try:
with pytest.raises(PILImage.DecompressionBombError):
gal = Gallery(settings, ncpu=1)
gal.build()
settings['max_img_pixels'] = 100_000_000
settings["max_img_pixels"] = 100_000_000
gal = Gallery(settings, ncpu=1)
gal.build()
finally:
@ -404,19 +404,19 @@ def test_gallery_max_img_pixels(settings, tmpdir, monkeypatch):
def test_empty_dirs(settings):
gal = Gallery(settings, ncpu=1)
assert 'empty' not in gal.albums
assert 'dir1/empty' not in gal.albums
assert "empty" not in gal.albums
assert "dir1/empty" not in gal.albums
def test_ignores(settings, tmpdir):
tmp = str(tmpdir)
settings['destination'] = tmp
settings['ignore_directories'] = ['*test2', 'accentué']
settings['ignore_files'] = ['dir2/Hubble*', '*.png', '*CMB_*']
settings["destination"] = tmp
settings["ignore_directories"] = ["*test2", "accentué"]
settings["ignore_files"] = ["dir2/Hubble*", "*.png", "*CMB_*"]
gal = Gallery(settings, ncpu=1)
gal.build()
assert 'test2' not in os.listdir(join(tmp, 'dir1'))
assert 'accentué' not in os.listdir(tmp)
assert 'CMB_Timeline300_no_WMAP.jpg' not in os.listdir(join(tmp, 'dir1', 'test1'))
assert 'Hubble Interacting Galaxy NGC 5257.jpg' not in os.listdir(join(tmp, 'dir2'))
assert "test2" not in os.listdir(join(tmp, "dir1"))
assert "accentué" not in os.listdir(tmp)
assert "CMB_Timeline300_no_WMAP.jpg" not in os.listdir(join(tmp, "dir1", "test1"))
assert "Hubble Interacting Galaxy NGC 5257.jpg" not in os.listdir(join(tmp, "dir2"))

162
tests/test_image.py

@ -19,31 +19,31 @@ from sigal.image import (
from sigal.settings import Status, create_settings
CURRENT_DIR = os.path.dirname(__file__)
SRCDIR = os.path.join(CURRENT_DIR, 'sample', 'pictures')
TEST_IMAGE = 'KeckObservatory20071020.jpg'
SRCFILE = os.path.join(SRCDIR, 'dir2', TEST_IMAGE)
SRCDIR = os.path.join(CURRENT_DIR, "sample", "pictures")
TEST_IMAGE = "KeckObservatory20071020.jpg"
SRCFILE = os.path.join(SRCDIR, "dir2", TEST_IMAGE)
TEST_GIF_IMAGE = 'example.gif'
SRC_GIF_FILE = os.path.join(SRCDIR, 'dir1', 'test1', TEST_GIF_IMAGE)
TEST_GIF_IMAGE = "example.gif"
SRC_GIF_FILE = os.path.join(SRCDIR, "dir1", "test1", TEST_GIF_IMAGE)
def test_process_image(tmpdir):
"Test the process_image function."
status = process_image(Image('foo.txt', 'bar', create_settings()))
status = process_image(Image("foo.txt", "bar", create_settings()))
assert status == Status.FAILURE
settings = create_settings(
img_processor='ResizeToFill',
img_processor="ResizeToFill",
make_thumbs=False,
source=os.path.join(SRCDIR, 'dir2'),
source=os.path.join(SRCDIR, "dir2"),
destination=str(tmpdir),
)
image = Image(TEST_IMAGE, '.', settings)
image = Image(TEST_IMAGE, ".", settings)
status = process_image(image)
assert status == Status.SUCCESS
im = PILImage.open(os.path.join(str(tmpdir), TEST_IMAGE))
assert im.size == settings['img_size']
assert im.size == settings["img_size"]
def test_generate_image(tmpdir):
@ -52,9 +52,9 @@ def test_generate_image(tmpdir):
dstfile = str(tmpdir.join(TEST_IMAGE))
for i, size in enumerate([(600, 600), (300, 200)]):
settings = create_settings(
img_size=size, img_processor='ResizeToFill', copy_exif_data=True
img_size=size, img_processor="ResizeToFill", copy_exif_data=True
)
options = None if i == 0 else {'quality': 85}
options = None if i == 0 else {"quality": 85}
generate_image(SRCFILE, dstfile, settings, options=options)
im = PILImage.open(dstfile)
assert im.size == size
@ -67,11 +67,11 @@ def test_generate_image_imgformat(tmpdir):
for i, outfmt in enumerate(["JPEG", "PNG", "TIFF"]):
settings = create_settings(
img_size=(300, 300),
img_processor='ResizeToFill',
img_processor="ResizeToFill",
copy_exif_data=True,
img_format=outfmt,
)
options = {'quality': 85}
options = {"quality": 85}
generate_image(SRCFILE, dstfile, settings, options=options)
im = PILImage.open(dstfile)
assert im.format == outfmt
@ -82,9 +82,9 @@ def test_resize_image_portrait(tmpdir):
size = (300, 200)
settings = create_settings(img_size=size)
portrait_image = 'm57_the_ring_nebula-587px.jpg'
portrait_image = "m57_the_ring_nebula-587px.jpg"
portrait_src = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir2', portrait_image
CURRENT_DIR, "sample", "pictures", "dir2", portrait_image
)
portrait_dst = str(tmpdir.join(portrait_image))
@ -96,9 +96,9 @@ def test_resize_image_portrait(tmpdir):
# Hence we test that the shorter side has the smallest length.
assert im.size[0] == 200
landscape_image = 'KeckObservatory20071020.jpg'
landscape_image = "KeckObservatory20071020.jpg"
landscape_src = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir2', landscape_image
CURRENT_DIR, "sample", "pictures", "dir2", landscape_image
)
landscape_dst = str(tmpdir.join(landscape_image))
@ -137,9 +137,9 @@ def test_generate_image_passthrough_symlink(tmpdir):
def test_generate_image_processor(tmpdir):
"Test generate_image with a wrong processor name."
init_logging('sigal')
init_logging("sigal")
dstfile = str(tmpdir.join(TEST_IMAGE))
settings = create_settings(img_size=(200, 200), img_processor='WrongMethod')
settings = create_settings(img_size=(200, 200), img_processor="WrongMethod")
with pytest.raises(SystemExit):
generate_image(SRCFILE, dstfile, settings)
@ -168,123 +168,123 @@ def test_generate_thumbnail(tmpdir, image, path, wide_size, high_size):
def test_get_exif_tags():
test_image = '11.jpg'
test_image = "11.jpg"
src_file = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir1', 'test1', test_image
CURRENT_DIR, "sample", "pictures", "dir1", "test1", test_image
)
data = get_exif_data(src_file)
simple = get_exif_tags(data, datetime_format='%d/%m/%Y')
assert simple['fstop'] == 3.9
assert simple['focal'] == 12.0
assert simple['iso'] == 50
assert simple['Make'] == 'NIKON'
assert simple['datetime'] == '22/01/2006'
simple = get_exif_tags(data, datetime_format="%d/%m/%Y")
assert simple["fstop"] == 3.9
assert simple["focal"] == 12.0
assert simple["iso"] == 50
assert simple["Make"] == "NIKON"
assert simple["datetime"] == "22/01/2006"
try:
# Pillow 7.2+
assert simple['exposure'] == '0.00100603'
assert simple["exposure"] == "0.00100603"
except Exception:
assert simple['exposure'] == '100603/100000000'
assert simple["exposure"] == "100603/100000000"
data = {'FNumber': [1, 0], 'FocalLength': [1, 0], 'ExposureTime': 10}
data = {"FNumber": [1, 0], "FocalLength": [1, 0], "ExposureTime": 10}
simple = get_exif_tags(data)
assert 'fstop' not in simple
assert 'focal' not in simple
assert simple['exposure'] == '10'
assert "fstop" not in simple
assert "focal" not in simple
assert simple["exposure"] == "10"
data = {
'ExposureTime': '--',
'DateTimeOriginal': '---',
'GPSInfo': {
'GPSLatitude': ((34, 0), (1, 0), (4500, 100)),
'GPSLatitudeRef': 'N',
'GPSLongitude': ((116, 0), (8, 0), (3900, 100)),
'GPSLongitudeRef': 'W',
"ExposureTime": "--",
"DateTimeOriginal": "---",
"GPSInfo": {
"GPSLatitude": ((34, 0), (1, 0), (4500, 100)),
"GPSLatitudeRef": "N",
"GPSLongitude": ((116, 0), (8, 0), (3900, 100)),
"GPSLongitudeRef": "W",
},
}
simple = get_exif_tags(data)
assert 'exposure' not in simple
assert 'datetime' not in simple
assert 'gps' not in simple
assert "exposure" not in simple
assert "datetime" not in simple
assert "gps" not in simple
def test_get_iptc_data(caplog):
test_image = '1.jpg'
src_file = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'iptcTest', test_image)
test_image = "1.jpg"
src_file = os.path.join(CURRENT_DIR, "sample", "pictures", "iptcTest", test_image)
data = get_iptc_data(src_file)
# Title
assert (
data["title"]
== 'Haemostratulus clouds over Canberra - ' + '2005-12-28 at 03-25-07'
== "Haemostratulus clouds over Canberra - " + "2005-12-28 at 03-25-07"
)
# Description
assert (
data["description"]
== '"Haemo" because they look like haemoglobin '
+ 'cells and "stratulus" because I can\'t work out whether '
+ 'they\'re Stratus or Cumulus clouds.\nWe\'re driving down '
+ 'the main drag in Canberra so it\'s Parliament House that '
+ 'you can see at the end of the road.'
+ "they're Stratus or Cumulus clouds.\nWe're driving down "
+ "the main drag in Canberra so it's Parliament House that "
+ "you can see at the end of the road."
)
# This file has no IPTC data
test_image = '21.jpg'
src_file = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'exifTest', test_image)
test_image = "21.jpg"
src_file = os.path.join(CURRENT_DIR, "sample", "pictures", "exifTest", test_image)
assert get_iptc_data(src_file) == {}
# Headline
test_image = '3.jpg'
src_file = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'iptcTest', test_image)
test_image = "3.jpg"
src_file = os.path.join(CURRENT_DIR, "sample", "pictures", "iptcTest", test_image)
data = get_iptc_data(src_file)
assert data["headline"] == 'Ring Nebula, M57'
assert data["headline"] == "Ring Nebula, M57"
# Test catching the SyntaxError -- assert output
with patch('sigal.image.IptcImagePlugin.getiptcinfo', side_effect=SyntaxError):
with patch("sigal.image.IptcImagePlugin.getiptcinfo", side_effect=SyntaxError):
get_iptc_data(src_file)
assert ['IPTC Error in'] == [log.message[:13] for log in caplog.records]
assert ["IPTC Error in"] == [log.message[:13] for log in caplog.records]
def test_get_image_metadata_exceptions():
# image does not exist
test_image = 'bad_image.jpg'
src_file = os.path.join(CURRENT_DIR, 'sample', test_image)
test_image = "bad_image.jpg"
src_file = os.path.join(CURRENT_DIR, "sample", test_image)
data = get_image_metadata(src_file)
assert data == {'exif': {}, 'iptc': {}, 'size': {}}
assert data == {"exif": {}, "iptc": {}, "size": {}}
def test_iso_speed_ratings():
data = {'ISOSpeedRatings': ()}
data = {"ISOSpeedRatings": ()}
simple = get_exif_tags(data)
assert 'iso' not in simple
assert "iso" not in simple
data = {'ISOSpeedRatings': None}
data = {"ISOSpeedRatings": None}
simple = get_exif_tags(data)
assert 'iso' not in simple
assert "iso" not in simple
data = {'ISOSpeedRatings': 125}
data = {"ISOSpeedRatings": 125}
simple = get_exif_tags(data)
assert 'iso' in simple
assert "iso" in simple
def test_null_exposure_time():
data = {'ExposureTime': (0, 0)}
data = {"ExposureTime": (0, 0)}
simple = get_exif_tags(data)
assert 'exposure' not in simple
assert "exposure" not in simple
def test_exif_copy(tmpdir):
"Test if EXIF data can transferred copied to the resized image."
test_image = '11.jpg'
test_image = "11.jpg"
src_file = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir1', 'test1', test_image
CURRENT_DIR, "sample", "pictures", "dir1", "test1", test_image
)
dst_file = str(tmpdir.join(test_image))
settings = create_settings(img_size=(300, 400), copy_exif_data=True)
generate_image(src_file, dst_file, settings)
simple = get_exif_tags(get_exif_data(dst_file))
assert simple['iso'] == 50
assert simple["iso"] == 50
settings['copy_exif_data'] = False
settings["copy_exif_data"] = False
generate_image(src_file, dst_file, settings)
simple = get_exif_tags(get_exif_data(dst_file))
assert not simple
@ -293,40 +293,40 @@ def test_exif_copy(tmpdir):
def test_exif_gps(tmpdir):
"""Test reading out correct geo tags"""
test_image = 'flickr_jerquiaga_2394751088_cc-by-nc.jpg'
test_image = "flickr_jerquiaga_2394751088_cc-by-nc.jpg"
src_file = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir1', 'test1', test_image
CURRENT_DIR, "sample", "pictures", "dir1", "test1", test_image
)
dst_file = str(tmpdir.join(test_image))
settings = create_settings(img_size=(400, 300), copy_exif_data=True)
generate_image(src_file, dst_file, settings)
simple = get_exif_tags(get_exif_data(dst_file))
assert 'gps' in simple
assert "gps" in simple
lat = 34.029167
lon = -116.144167
assert abs(simple['gps']['lat'] - lat) < 0.0001
assert abs(simple['gps']['lon'] - lon) < 0.0001
assert abs(simple["gps"]["lat"] - lat) < 0.0001
assert abs(simple["gps"]["lon"] - lon) < 0.0001
def test_get_size(tmpdir):
"""Test reading out image size"""
test_image = 'flickr_jerquiaga_2394751088_cc-by-nc.jpg'
test_image = "flickr_jerquiaga_2394751088_cc-by-nc.jpg"
src_file = os.path.join(
CURRENT_DIR, 'sample', 'pictures', 'dir1', 'test1', test_image
CURRENT_DIR, "sample", "pictures", "dir1", "test1", test_image
)
result = get_size(src_file)
assert result == {'height': 800, 'width': 600}
assert result == {"height": 800, "width": 600}
def test_get_size_with_invalid_path(tmpdir):
"""Test reading out image size with a missing file"""
test_image = 'missing-file.jpg'
test_image = "missing-file.jpg"
src_file = os.path.join(CURRENT_DIR, test_image)
result = get_size(src_file)

22
tests/test_plugins.py

@ -7,18 +7,18 @@ CURRENT_DIR = os.path.dirname(__file__)
def test_plugins(settings, tmpdir, disconnect_signals):
settings['destination'] = str(tmpdir)
settings["destination"] = str(tmpdir)
if "sigal.plugins.nomedia" not in settings["plugins"]:
settings['plugins'] += ["sigal.plugins.nomedia"]
settings["plugins"] += ["sigal.plugins.nomedia"]
if "sigal.plugins.media_page" not in settings["plugins"]:
settings['plugins'] += ["sigal.plugins.media_page"]
settings["plugins"] += ["sigal.plugins.media_page"]
init_plugins(settings)
gal = Gallery(settings)
gal.build()
out_html = os.path.join(
settings['destination'], 'dir2', 'KeckObservatory20071020.jpg.html'
settings["destination"], "dir2", "KeckObservatory20071020.jpg.html"
)
assert os.path.isfile(out_html)
@ -30,30 +30,30 @@ def test_plugins(settings, tmpdir, disconnect_signals):
def test_nonmedia_files(settings, tmpdir, disconnect_signals):
settings['destination'] = str(tmpdir)
settings['plugins'] += ['sigal.plugins.nonmedia_files']
settings['nonmedia_files_options'] = {'thumb_bg_color': 'red'}
settings["destination"] = str(tmpdir)
settings["plugins"] += ["sigal.plugins.nonmedia_files"]
settings["nonmedia_files_options"] = {"thumb_bg_color": "red"}
init_plugins(settings)
gal = Gallery(settings)
gal.build()
outfile = os.path.join(settings['destination'], 'nonmedia_files', 'dummy.pdf')
outfile = os.path.join(settings["destination"], "nonmedia_files", "dummy.pdf")
assert os.path.isfile(outfile)
outthumb = os.path.join(
settings['destination'], 'nonmedia_files', 'thumbnails', 'dummy.tn.jpg'
settings["destination"], "nonmedia_files", "thumbnails", "dummy.tn.jpg"
)
assert os.path.isfile(outthumb)
def test_titleregexp(settings, tmpdir, disconnect_signals):
if "sigal.plugins.titleregexp" not in settings["plugins"]:
settings['plugins'] += ["sigal.plugins.titleregexp"]
settings["plugins"] += ["sigal.plugins.titleregexp"]
init_plugins(settings)
gal = Gallery(settings)
gal.build()
assert gal.albums.get('dir1').albums[1].title == "titleregexp 02"
assert gal.albums.get("dir1").albums[1].title == "titleregexp 02"

32
tests/test_settings.py

@ -7,26 +7,26 @@ CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
def test_read_settings(settings):
"""Test that the settings are correctly read."""
assert settings['img_size'] == (640, 480)
assert settings['thumb_size'] == (200, 150)
assert settings['thumb_suffix'] == '.tn'
assert settings['source'] == os.path.join(CURRENT_DIR, 'sample', 'pictures')
assert settings["img_size"] == (640, 480)
assert settings["thumb_size"] == (200, 150)
assert settings["thumb_suffix"] == ".tn"
assert settings["source"] == os.path.join(CURRENT_DIR, "sample", "pictures")
def test_get_thumb(settings):
"""Test the get_thumb function."""
tests = [
('example.jpg', 'thumbnails/example.tn.jpg'),
('test/example.jpg', 'test/thumbnails/example.tn.jpg'),
('test/t/example.jpg', 'test/t/thumbnails/example.tn.jpg'),
("example.jpg", "thumbnails/example.tn.jpg"),
("test/example.jpg", "test/thumbnails/example.tn.jpg"),
("test/t/example.jpg", "test/t/thumbnails/example.tn.jpg"),
]
for src, ref in tests:
assert get_thumb(settings, src) == ref
tests = [
('example.webm', 'thumbnails/example.tn.jpg'),
('test/example.mp4', 'test/thumbnails/example.tn.jpg'),
('test/t/example.avi', 'test/t/thumbnails/example.tn.jpg'),
("example.webm", "thumbnails/example.tn.jpg"),
("test/example.mp4", "test/thumbnails/example.tn.jpg"),
("test/t/example.avi", "test/t/thumbnails/example.tn.jpg"),
]
for src, ref in tests:
assert get_thumb(settings, src) == ref
@ -35,20 +35,20 @@ def test_get_thumb(settings):
def test_img_sizes(tmpdir):
"""Test that image size is swaped if needed."""
conf = tmpdir.join('sigal.conf.py')
conf = tmpdir.join("sigal.conf.py")
conf.write("thumb_size = (150, 200)")
settings = read_settings(str(conf))
assert settings['thumb_size'] == (200, 150)
assert settings["thumb_size"] == (200, 150)
def test_theme_path(tmpdir):
"""Test that image size is swaped if needed."""
tmpdir.join('theme').mkdir()
tmpdir.join('theme').join('templates').mkdir()
conf = tmpdir.join('sigal.conf.py')
tmpdir.join("theme").mkdir()
tmpdir.join("theme").join("templates").mkdir()
conf = tmpdir.join("sigal.conf.py")
conf.write("theme = 'theme'")
settings = read_settings(str(conf))
assert settings['theme'] == tmpdir.join('theme')
assert settings["theme"] == tmpdir.join("theme")

58
tests/test_utils.py

@ -4,39 +4,39 @@ from pathlib import Path
from sigal import utils
CURRENT_DIR = os.path.dirname(__file__)
SAMPLE_DIR = os.path.join(CURRENT_DIR, 'sample')
SAMPLE_DIR = os.path.join(CURRENT_DIR, "sample")
def test_copy(tmpdir):
filename = 'KeckObservatory20071020.jpg'
src = os.path.join(SAMPLE_DIR, 'pictures', 'dir2', filename)
filename = "KeckObservatory20071020.jpg"
src = os.path.join(SAMPLE_DIR, "pictures", "dir2", filename)
dst = str(tmpdir.join(filename))
utils.copy(src, dst)
assert os.path.isfile(dst)
filename = 'm57_the_ring_nebula-587px.jpg'
src = os.path.join(SAMPLE_DIR, 'pictures', 'dir2', filename)
filename = "m57_the_ring_nebula-587px.jpg"
src = os.path.join(SAMPLE_DIR, "pictures", "dir2", filename)
dst = str(tmpdir.join(filename))
utils.copy(src, dst, symlink=True)
assert os.path.islink(dst)
assert os.readlink(dst) == src
filename = 'KeckObservatory20071020.jpg'
src = os.path.join(SAMPLE_DIR, 'pictures', 'dir2', filename)
filename = "KeckObservatory20071020.jpg"
src = os.path.join(SAMPLE_DIR, "pictures", "dir2", filename)
utils.copy(src, dst, symlink=True)
assert os.path.islink(dst)
assert os.readlink(dst) == src
filename = 'KeckObservatory20071020.jpg'
src = os.path.join(SAMPLE_DIR, 'pictures', 'dir2', filename)
filename = "KeckObservatory20071020.jpg"
src = os.path.join(SAMPLE_DIR, "pictures", "dir2", filename)
dst = str(tmpdir.join(filename))
utils.copy(src, dst, symlink=True, rellink=True)
assert os.path.islink(dst)
assert os.path.join(os.path.dirname(CURRENT_DIR)), os.readlink(dst) == src
# get absolute path of the current dir plus the relative dir
src = str(tmpdir.join('foo.txt'))
dst = str(tmpdir.join('bar.txt'))
src = str(tmpdir.join("foo.txt"))
dst = str(tmpdir.join("bar.txt"))
p = Path(src)
p.touch()
p.chmod(0o444)
@ -45,48 +45,48 @@ def test_copy(tmpdir):
def test_check_or_create_dir(tmpdir):
path = str(tmpdir.join('new_directory'))
path = str(tmpdir.join("new_directory"))
utils.check_or_create_dir(path)
assert os.path.isdir(path)
def test_url_from_path():
assert utils.url_from_path(os.sep.join(['foo', 'bar'])) == 'foo/bar'
assert utils.url_from_path(os.sep.join(["foo", "bar"])) == "foo/bar"
def test_url_from_windows_path(monkeypatch):
monkeypatch.setattr('os.sep', "\\")
path = os.sep.join(['foo', 'bar'])
assert path == r'foo\bar'
assert utils.url_from_path(path) == 'foo/bar'
monkeypatch.setattr("os.sep", "\\")
path = os.sep.join(["foo", "bar"])
assert path == r"foo\bar"
assert utils.url_from_path(path) == "foo/bar"
def test_read_markdown():
src = os.path.join(SAMPLE_DIR, 'pictures', 'dir1', 'test1', '11.md')
src = os.path.join(SAMPLE_DIR, "pictures", "dir1", "test1", "11.md")
m = utils.read_markdown(src)
assert m['title'] == "Foo Bar"
assert m['meta']['location'][0] == "Bavaria"
assert m['description'] == "<p>This is a funny description of this image</p>"
assert m["title"] == "Foo Bar"
assert m["meta"]["location"][0] == "Bavaria"
assert m["description"] == "<p>This is a funny description of this image</p>"
def test_read_markdown_empty_file(tmpdir):
src = tmpdir.join("file.txt")
src.write("content")
m = utils.read_markdown(str(src))
assert 'title' not in m
assert m['meta'] == {}
assert m['description'] == '<p>content</p>'
assert "title" not in m
assert m["meta"] == {}
assert m["description"] == "<p>content</p>"
src = tmpdir.join("empty.txt")
src.write("")
m = utils.read_markdown(str(src))
assert 'title' not in m
assert "title" not in m
# See https://github.com/Python-Markdown/markdown/pull/672
# Meta attributes should always be there
assert m['meta'] == {}
assert m['description'] == ''
assert m["meta"] == {}
assert m["description"] == ""
def test_is_valid_html5_video():
assert utils.is_valid_html5_video('.webm') is True
assert utils.is_valid_html5_video('.mpeg') is False
assert utils.is_valid_html5_video(".webm") is True
assert utils.is_valid_html5_video(".mpeg") is False

58
tests/test_video.py

@ -9,20 +9,20 @@ from sigal.settings import Status, create_settings
from sigal.video import generate_thumbnail, generate_video, process_video, video_size
CURRENT_DIR = os.path.dirname(__file__)
SRCDIR = os.path.join(CURRENT_DIR, 'sample', 'pictures')
TEST_VIDEO = 'example video.ogv'
SRCFILE = os.path.join(SRCDIR, 'video', TEST_VIDEO)
SRCDIR = os.path.join(CURRENT_DIR, "sample", "pictures")
TEST_VIDEO = "example video.ogv"
SRCFILE = os.path.join(SRCDIR, "video", TEST_VIDEO)
def test_video_size():
size_src = video_size(SRCFILE)
assert size_src == (320, 240)
size_src = video_size('missing/file.mp4')
size_src = video_size("missing/file.mp4")
assert size_src == (0, 0)
def test_generate_thumbnail(tmpdir):
outname = str(tmpdir.join('test.jpg'))
outname = str(tmpdir.join("test.jpg"))
generate_thumbnail(SRCFILE, outname, (50, 50), 5)
assert os.path.isfile(outname)
@ -31,18 +31,18 @@ def test_process_video(tmpdir):
base, ext = os.path.splitext(TEST_VIDEO)
settings = create_settings(
video_format='ogv',
video_format="ogv",
use_orig=True,
orig_link=True,
source=os.path.join(SRCDIR, 'video'),
source=os.path.join(SRCDIR, "video"),
destination=str(tmpdir),
)
video = Video(TEST_VIDEO, '.', settings)
video = Video(TEST_VIDEO, ".", settings)
process_video(video)
dstfile = str(tmpdir.join(base + '.ogv'))
dstfile = str(tmpdir.join(base + ".ogv"))
assert os.path.realpath(dstfile) == SRCFILE
settings = create_settings(video_format='mjpg')
settings = create_settings(video_format="mjpg")
assert process_video(video) == Status.FAILURE
settings = create_settings(thumb_video_delay=-1)
@ -53,23 +53,23 @@ def test_metadata(tmpdir):
base, ext = os.path.splitext(TEST_VIDEO)
settings = create_settings(
video_format='ogv',
video_format="ogv",
use_orig=True,
orig_link=True,
source=os.path.join(SRCDIR, 'video'),
source=os.path.join(SRCDIR, "video"),
destination=str(tmpdir),
)
video = Video(TEST_VIDEO, '.', settings)
assert video.meta == {'date': ['2020-01-01T09:00:00']}
video = Video(TEST_VIDEO, ".", settings)
assert video.meta == {"date": ["2020-01-01T09:00:00"]}
assert video.date == datetime(2020, 1, 1, 9, 0)
@pytest.mark.parametrize("fmt", ['webm', 'mp4'])
@pytest.mark.parametrize("fmt", ["webm", "mp4"])
def test_generate_video_fit_height(tmpdir, fmt):
"""largest fitting dimension is height"""
base, ext = os.path.splitext(TEST_VIDEO)
dstfile = str(tmpdir.join(base + '.' + fmt))
dstfile = str(tmpdir.join(base + "." + fmt))
settings = create_settings(video_size=(80, 100), video_format=fmt)
generate_video(SRCFILE, dstfile, settings)
@ -81,12 +81,12 @@ def test_generate_video_fit_height(tmpdir, fmt):
assert abs(size_dst[0] / size_dst[1] - size_src[0] / size_src[1]) < 2e-2
@pytest.mark.parametrize("fmt", ['webm', 'mp4'])
@pytest.mark.parametrize("fmt", ["webm", "mp4"])
def test_generate_video_fit_width(tmpdir, fmt):
"""largest fitting dimension is width"""
base, ext = os.path.splitext(TEST_VIDEO)
dstfile = str(tmpdir.join(base + '.' + fmt))
dstfile = str(tmpdir.join(base + "." + fmt))
settings = create_settings(video_size=(100, 50), video_format=fmt)
generate_video(SRCFILE, dstfile, settings)
@ -98,12 +98,12 @@ def test_generate_video_fit_width(tmpdir, fmt):
assert abs(size_dst[0] / size_dst[1] - size_src[0] / size_src[1]) < 2e-2
@pytest.mark.parametrize("fmt", ['webm', 'mp4', 'ogv'])
@pytest.mark.parametrize("fmt", ["webm", "mp4", "ogv"])
def test_generate_video_dont_enlarge(tmpdir, fmt):
"""Video dimensions should not be enlarged."""
base, ext = os.path.splitext(TEST_VIDEO)
dstfile = str(tmpdir.join(base + '.' + fmt))
dstfile = str(tmpdir.join(base + "." + fmt))
settings = create_settings(video_size=(1000, 1000), video_format=fmt)
generate_video(SRCFILE, dstfile, settings)
size_src = video_size(SRCFILE)
@ -112,19 +112,19 @@ def test_generate_video_dont_enlarge(tmpdir, fmt):
assert size_src == size_dst
@patch('sigal.video.generate_video_pass')
@pytest.mark.parametrize("fmt", ['webm', 'mp4'])
@patch("sigal.video.generate_video_pass")
@pytest.mark.parametrize("fmt", ["webm", "mp4"])
def test_second_pass_video(mock_generate_video_pass, fmt, tmpdir):
"""Video should be run through ffmpeg."""
base, ext = os.path.splitext(TEST_VIDEO)
dstfile = str(tmpdir.join(base + '.' + fmt))
settings_1 = '-c:v libvpx-vp9 -b:v 0 -crf 30 -pass 1 -an -f null dev/null'
settings_2 = f'-c:v libvpx-vp9 -b:v 0 -crf 30 -pass 2 -f {fmt}'
dstfile = str(tmpdir.join(base + "." + fmt))
settings_1 = "-c:v libvpx-vp9 -b:v 0 -crf 30 -pass 1 -an -f null dev/null"
settings_2 = f"-c:v libvpx-vp9 -b:v 0 -crf 30 -pass 2 -f {fmt}"
settings_opts = {
'video_size': (100, 50),
'video_format': fmt,
fmt + '_options': settings_1.split(" "),
fmt + '_options_second_pass': settings_2.split(" "),
"video_size": (100, 50),
"video_format": fmt,
fmt + "_options": settings_1.split(" "),
fmt + "_options_second_pass": settings_2.split(" "),
}
settings = create_settings(**settings_opts)

40
tests/test_zip.py

@ -6,14 +6,14 @@ from sigal.gallery import Gallery
from sigal.settings import read_settings
CURRENT_DIR = os.path.dirname(__file__)
SAMPLE_DIR = os.path.join(CURRENT_DIR, 'sample')
SAMPLE_SOURCE = os.path.join(SAMPLE_DIR, 'pictures')
SAMPLE_DIR = os.path.join(CURRENT_DIR, "sample")
SAMPLE_SOURCE = os.path.join(SAMPLE_DIR, "pictures")
def make_gallery(source_dir='dir1', **kwargs):
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
def make_gallery(source_dir="dir1", **kwargs):
default_conf = os.path.join(SAMPLE_DIR, "sigal.conf.py")
settings = read_settings(default_conf)
settings['source'] = os.path.join(SAMPLE_SOURCE, source_dir)
settings["source"] = os.path.join(SAMPLE_SOURCE, source_dir)
settings.update(kwargs)
init_plugins(settings)
return Gallery(settings, ncpu=1)
@ -21,18 +21,18 @@ def make_gallery(source_dir='dir1', **kwargs):
def test_zipped_correctly(tmpdir):
outpath = str(tmpdir)
gallery = make_gallery(destination=outpath, zip_gallery='archive.zip')
gallery = make_gallery(destination=outpath, zip_gallery="archive.zip")
gallery.build()
zipf = os.path.join(outpath, 'test1', 'archive.zip')
zipf = os.path.join(outpath, "test1", "archive.zip")
assert os.path.isfile(zipf)
zip_file = zipfile.ZipFile(zipf, 'r')
zip_file = zipfile.ZipFile(zipf, "r")
expected = (
'11.jpg',
'CMB_Timeline300_no_WMAP.jpg',
'flickr_jerquiaga_2394751088_cc-by-nc.jpg',
'example.gif',
"11.jpg",
"CMB_Timeline300_no_WMAP.jpg",
"flickr_jerquiaga_2394751088_cc-by-nc.jpg",
"example.gif",
)
for filename in zip_file.namelist():
@ -40,7 +40,7 @@ def test_zipped_correctly(tmpdir):
zip_file.close()
assert os.path.isfile(os.path.join(outpath, 'test2', 'archive.zip'))
assert os.path.isfile(os.path.join(outpath, "test2", "archive.zip"))
def test_not_zipped(tmpdir):
@ -48,10 +48,10 @@ def test_not_zipped(tmpdir):
# is present
outpath = str(tmpdir)
gallery = make_gallery(
destination=outpath, zip_gallery='archive.zip', source_dir='dir2'
destination=outpath, zip_gallery="archive.zip", source_dir="dir2"
)
gallery.build()
assert not os.path.isfile(os.path.join(outpath, 'archive.zip'))
assert not os.path.isfile(os.path.join(outpath, "archive.zip"))
def test_no_archive(tmpdir):
@ -59,16 +59,16 @@ def test_no_archive(tmpdir):
gallery = make_gallery(destination=outpath, zip_gallery=False)
gallery.build()
assert not os.path.isfile(os.path.join(outpath, 'test1', 'archive.zip'))
assert not os.path.isfile(os.path.join(outpath, 'test2', 'archive.zip'))
assert not os.path.isfile(os.path.join(outpath, "test1", "archive.zip"))
assert not os.path.isfile(os.path.join(outpath, "test2", "archive.zip"))
def test_correct_filename(tmpdir, caplog):
caplog.set_level('ERROR')
caplog.set_level("ERROR")
outpath = str(tmpdir)
gallery = make_gallery(destination=outpath, zip_gallery=True)
gallery.build()
assert caplog.records[0].message == "'zip_gallery' should be set to a filename"
assert not os.path.isfile(os.path.join(outpath, 'test1', 'archive.zip'))
assert not os.path.isfile(os.path.join(outpath, 'test2', 'archive.zip'))
assert not os.path.isfile(os.path.join(outpath, "test1", "archive.zip"))
assert not os.path.isfile(os.path.join(outpath, "test2", "archive.zip"))

Loading…
Cancel
Save