configuration magic - can now load configuration data from ./degal.cfg, --config, folder/degal.cfg
--- a/degal/commands/main.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/commands/main.py Sun Jun 14 22:52:07 2009 +0300
@@ -33,9 +33,9 @@
"""
Render the given series of images
"""
+
+ # XXX: handle this thumb-update/html-update stuff better
- # XXX: this breaks force_html
-
# render the thumbnails concurrently
for image in ctx.concurrent.execute(
task(update_image_thumbs, image)
@@ -49,7 +49,8 @@
# log image path
ctx.log_info("%s", image)
- # render HTML
+ # then render all HTML
+ for image in images :
render_image_html(ctx, image)
# release large objects that are not needed anymore
--- a/degal/config.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/config.py Sun Jun 14 22:52:07 2009 +0300
@@ -2,53 +2,117 @@
Configuration
"""
-import logging
+import copy, logging
-class Configuration (object) :
+class InstanceContext (object) :
+ """
+ An object that behaves like a dict, performing all item lookups as attribute lookups on the given object.
+
+ Useful for binding an exec statement's locals.
+ """
+
+ def __init__ (self, obj) : self.obj = obj
+ def __getitem__ (self, key) : return getattr(self.obj, key)
+ def __setitem__ (self, key, value) : setattr(self.obj, key, value)
+
+class ConfigurationMachinery (object) :
+ """
+ The low-level configuration machinery.
+ """
+
+ def import_py (self, path) :
+ """
+ Import python-style configuration from the given File object into this one.
+
+ This runs the file's code using exec, using a context mapped to this object.
+ """
+
+ # the code
+ file = open(path)
+
+ # build suitable locals/globals to proxy this object
+ locals = InstanceContext(self)
+
+ # run
+ exec file in {}, locals
+
+ def import_file (self, path) :
+ """
+ Import configuration from the given File into this one.
+ """
+
+ # as python code
+ self.import_py(path)
+
+ def load (self, path) :
+ """
+ Loads the configuration from the given File, creating a new Configuration with defaults from this one.
+ """
+
+ # copy ourself
+ new = copy.copy(self)
+
+ # import into it
+ new.import_file(path)
+
+ # return
+ return new
+
+class Configuration (ConfigurationMachinery) :
"""
Various configuration settings
"""
- ## runtime settings
- # the path to the gallery
+ # the path to the gallery root
+ # only valid in top-level config
gallery_path = "."
# force-update items
force_thumb = False
force_html = False
+ def get_force_update (self) :
+ return self.force_thumb or self.force_html
+
+ def set_force_update (self, value) :
+ self.force_thumb = self.force_html = value
+
+ force_update = property(get_force_update, set_force_update)
+
# minimum logging level
log_level = logging.INFO
+ def quiet (self) : self.log_level = logging.WARN
+ def debug (self) : self.log_level = logging.DEBUG
+
# number of threads to use for concurrency
thread_count = 2
- ## detailed configuration
- # the name of the gallery
- gallery_title = "Image Gallery"
+ # the name of this folder, only applies on one level
+ # default applies to the root gallery
+ title = "Image Gallery"
- # recognized image extensions
+ # recognized image extensions, case-insensitive
image_exts = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
- # subdirectory used for generated thumbnails/previews
+ # subdirectory names used for generated thumbnails/previews
thumb_dir = 'thumbs'
preview_dir = 'previews'
- # size of generated thumbnails/previews
+ # size of generated thumbnails/previews as (width, height) tuples
thumb_size = (160, 120)
preview_size = (640, 480)
# number of images displayed per folder page
images_per_page = 50
- # load Exif data for images
- # this may be slow
- exif_enabled = False
+ # load Exif data for images, this may be slow
+ with_exif = False
# exif tags used in output
# Copyright (C) 2008, Santtu Pajukanta <santtu@pajukanta.fi>
# XXX: import from dexif?
- exif_tags = [
+ exif_tags = (
# TODO Create date is in a useless format, needs some strptime love
("CreateDate", "Create date" ),
("Model", "Camera model" ),
@@ -60,15 +124,15 @@
("ISO", "ISO" ),
("ShootingMode", "Shooting mode" ),
("LensType", "Lens type" ),
- ("FocalLength", "Focal length" )
- ]
-
+ ("FocalLength", "Focal length" ),
+ )
- ### functions
+ # XXX: move elsewhere?
def is_image (self, file) :
"""
Tests if the given File is an image, based on its file extension
+
"""
return file.matchext(self.image_exts)
-
+
--- a/degal/filesystem.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/filesystem.py Sun Jun 14 22:52:07 2009 +0300
@@ -225,7 +225,7 @@
except OSError, e :
# trap ENOENT for soft
- if soft and e.errno == errno.ENOENT :
+ if e.errno == errno.ENOENT :
return None
else :
@@ -562,11 +562,14 @@
else :
return open(self.path, mode, *(arg for arg in (bufsize, ) if arg is not None))
+
+ def open_read (self, *args, **kwargs) :
+ """ Open for read using open('r') """
+
+ return self.open('r', *args, **kwargs)
def open_write (self, *args, **kwargs) :
- """
- Open for write using open('w').
- """
+ """ Open for write using open('w') """
return self.open('w', *args, **kwargs)
@@ -699,10 +702,7 @@
"""
# abuse Node's concept of a "name" a bit
- super(Root, self).__init__(None, fspath)
-
- # store our config
- self.config = config
+ super(Root, self).__init__(None, fspath, config=config)
def nodepath (self) :
"""
--- a/degal/folder.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/folder.py Sun Jun 14 22:52:07 2009 +0300
@@ -13,13 +13,51 @@
A Folder is a filesystem Directory that contains any number of other Folders and Images.
"""
+ def iter_config_files (self) :
+ """
+ Iterate over the possible config files for this dir
+ """
+
+ yield self.subfile('degal.cfg')
+ yield self.subdir('.degal').subfile('cfg')
+
def __init__ (self, *args, **kwargs) :
super(Folder, self).__init__(*args, **kwargs)
- # info
- self.title = self.name.title()
- self.description = None
-
+ # find config
+ for file in self.iter_config_files() :
+ if file.exists() :
+ # yay! More configuration!
+ self.config = self.config.load(file.path)
+
+ break
+
+ # load some info from title?
+ if self.config and self.config.title :
+ self.title = self.config.title
+
+ # disable it so it won't be used by children
+ # XXX: figure out a better way of doing this
+ self.config.title = None
+
+ @lazy_load
+ def title (self) :
+ """
+ Find the title for this dir
+ """
+
+ # default
+ return self.name.title()
+
+ @lazy_load
+ def description (self) :
+ """
+ Find the descriptive text for this dir
+ """
+
+ # default
+ return None
+
@lazy_load
def preview_dir (self) :
"""
--- a/degal/gallery.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/gallery.py Sun Jun 14 22:52:07 2009 +0300
@@ -20,6 +20,14 @@
super(Gallery, self).__init__(path, config)
+ @property
+ def _degal_dir (self) :
+ """
+ The dir containing the degal configuration.
+ """
+
+ return self.subdir('.degal')
+
@lazy_load
def degal_dir (self) :
"""
--- a/degal/image.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/image.py Sun Jun 14 22:52:07 2009 +0300
@@ -95,7 +95,7 @@
})
# optionally load Exif metadata
- if self.config.exif_enabled :
+ if self.config.with_exif :
exif = self.exif
# Get the wanted tags
--- a/degal/main.py Sun Jun 14 20:05:11 2009 +0300
+++ b/degal/main.py Sun Jun 14 22:52:07 2009 +0300
@@ -2,82 +2,86 @@
Main entry point for the command-line interface
"""
-import gallery, commands, config as config_module
+import gallery, commands, config
from optparse import OptionParser
+import os.path
-def option_parser (command_name) :
+def build_config () :
+ """
+ Build the default configuration to use
+ """
+
+ return config.Configuration()
+
+def option_parser (exec_name) :
"""
Build the OptionParser that we use
"""
- # create parser using the given command
- parser = OptionParser(prog=command_name)
+ parser = OptionParser(prog=exec_name, description="Degal - A photo gallery", version="???")
- # define options
- parser.add_option('-G', "--gallery-path", metavar='DIR', dest='gallery_path', default=None,
- help="Use DIR as the Gallery path [default: CWD]")
+ parser.add_option('-C', "--config", metavar='PATH', dest="_load_config_path",
+ help="Load configuration from PATH")
- parser.add_option('-F', "--force-update", dest='force_update', action="store_true", default=False,
+ parser.add_option('-H', "--gallery-path", metavar='DIR',
+ help="Use DIR as the Gallery path instead of the CWD")
+
+ parser.add_option('-F', "--force-update", action="store_true",
help="--force-thumb + --force-html")
- parser.add_option("--force-thumb", dest='force_thumb', action="store_true", default=False,
- help="Force-update all thumbnails")
+ parser.add_option("--force-thumb", action="store_true",
+ help="Force-update thumbnails")
- parser.add_option("--force-html", dest='force_html', action="store_true", default=False,
- help="Force-update all .html files")
+ parser.add_option("--force-html", action="store_true",
+ help="Force-update .html files")
- parser.add_option("--with-exif", dest='exif_enabled', action="store_true", default=None,
+ parser.add_option("--with-exif", action="store_true",
help="Include Exif metadata in updated .html files")
- parser.add_option('-c', "--thread-count", dest='thread_count', type="int", default=None,
- help="Size of thread pool")
+ parser.add_option('-c', "--thread-count", metavar='COUNT', type="int",
+ help="Use COUNT threads for concurrent tasks")
- parser.add_option('-d', "--debug", dest='debug', action="store_true", default=False,
+ parser.add_option('-d', "--debug", action="store_const", dest="log_level", const=config.logging.DEBUG,
help="Show debug output")
- parser.add_option('-q', "--quiet", dest='quiet', action="store_true", default=False,
- help="Reduced output")
+ parser.add_option('-q', "--quiet", action="store_const", dest="log_level", const=config.logging.WARN,
+ help="Reduced output (only warnings)")
return parser
-def build_config (options) :
+def parse_args (config, parser, args) :
"""
- Build a configuration object with the given options
+ Parse command-line options/arguments.
+
+ Returns the remaining positional arguments.
"""
- # build default config
- config = config_module.Configuration()
-
- # apply options
- if options.gallery_path :
- config.gallery_path = options.gallery_path
-
- if options.force_update :
- config.force_html = True
- config.force_thumb = True
-
- if options.force_thumb :
- config.force_thumb = True
+ # parse the given arguments, storing output directly in the config
+ _, args = parser.parse_args(args=args, values=config)
- if options.force_html :
- config.force_html = True
-
- if options.exif_enabled is not None :
- config.exif_enabled = options.exif_enabled
-
- if options.thread_count is not None :
- config.thread_count = options.thread_count
+ # return the posargs
+ return args
- if options.debug :
- config.log_level = config_module.logging.DEBUG
+def postprocess_config (config) :
+ """
+ Post-process our Configuration after our command-line arguments have been parsed.
- if options.quiet :
- config.log_level = config_module.logging.WARN
+ This will attempt to load any additional configuration.
+ """
+
+ # figure out what, if any, path to import
+ if hasattr(config, '_load_config_path') :
+ path = config._load_config_path
- # XXX: load config file(s)
+ elif os.path.exists('degal.cfg') :
+ path = 'degal.cfg'
+
+ else :
+ return
- return config
+ # import it
+ config.import_file(path)
def load_gallery (config) :
"""
@@ -114,14 +118,17 @@
## load commands
#commands = load_commands()
+ # build our default config
+ config = build_config()
+
# build optparser
parser = option_parser(argv[0])
-
- # parse the given argv
- options, args = parser.parse_args(argv[1:])
- # build our config
- config = build_config(options)
+ # parse the args into our config
+ args = parse_args(config, parser, argv[1:])
+
+ # postprocess
+ postprocess_config(config)
# open gallery
gallery = load_gallery(config)
@@ -129,9 +136,12 @@
# figure out what command to run
command, args, kwargs = load_command(config, args)
+
+
# run the selected command
ret = run_command(config, gallery, command, args, kwargs)
-
+
+
if ret is None :
# success
return 0