configuration magic - can now load configuration data from ./degal.cfg, --config, folder/degal.cfg
authorTero Marttila <terom@fixme.fi>
Sun, 14 Jun 2009 22:52:07 +0300
changeset 118 60b126ff0b74
parent 117 a2e4562deaab
child 119 e7855eefb4c7
configuration magic - can now load configuration data from ./degal.cfg, --config, folder/degal.cfg
degal/commands/main.py
degal/config.py
degal/filesystem.py
degal/folder.py
degal/gallery.py
degal/image.py
degal/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
--- 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