# HG changeset patch # User Tero Marttila # Date 1245009127 -10800 # Node ID 60b126ff0b7451047a96aa0f184364d028d167bb # Parent a2e4562deaabdfa2b9863095ef0044512852c6c3 configuration magic - can now load configuration data from ./degal.cfg, --config, folder/degal.cfg diff -r a2e4562deaab -r 60b126ff0b74 degal/commands/main.py --- 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 diff -r a2e4562deaab -r 60b126ff0b74 degal/config.py --- 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 # 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) - + diff -r a2e4562deaab -r 60b126ff0b74 degal/filesystem.py --- 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) : """ diff -r a2e4562deaab -r 60b126ff0b74 degal/folder.py --- 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) : """ diff -r a2e4562deaab -r 60b126ff0b74 degal/gallery.py --- 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) : """ diff -r a2e4562deaab -r 60b126ff0b74 degal/image.py --- 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 diff -r a2e4562deaab -r 60b126ff0b74 degal/main.py --- 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