reorganize/rename the commands and their options stuff, options like --force-html are now split out from main.py into commands/update.py
authorTero Marttila <terom@fixme.fi>
Thu, 02 Jul 2009 21:59:01 +0300
changeset 144 97505a789003
parent 143 a6e53a20fccb
reorganize/rename the commands and their options stuff, options like --force-html are now split out from main.py into commands/update.py
degal/command.py
degal/commands/__init__.py
degal/commands/update.py
degal/config.py
degal/html.py
degal/main.py
--- a/degal/command.py	Wed Jul 01 20:57:03 2009 +0300
+++ b/degal/command.py	Thu Jul 02 21:59:01 2009 +0300
@@ -1,14 +1,94 @@
 """
-    Command implementations
+    Command-line based command implementation, handling option/argument parsing.
 """
 
-import inspect, logging, traceback, concurrent
+import concurrent
+
+import inspect, itertools, logging, traceback, optparse
+
+def optify (symbol) :
+    """
+        Turns the given python symbol into the suitable version for use as a command-line argument.
+
+        In other words, this replaces '_' with '-'.
+    """
+
+    return symbol.replace('_', '-')
+
+# wrapper around optparse.make_option
+Option = optparse.make_option
+
+class Command (object) :
+    """
+        A Command is simply a function that can be executed from the command line with some options/arguments
+    """
+
+    def __init__ (self, name, func, doc=None, options=None) :
+        """
+            Create a new Command
+
+             name       - the name of the command
+             func       - the callable python function
+             doc        - descriptive help text
+             options    - named options as Option objects
+        """
+        
+        self.name = name
+        self.func = func
+        self.doc = doc
+        self.options = options
+    
+    def parse_args (self, args) :
+        """
+            Pre-parse the given arguments list.
+        """
+
+        return args
+
+    def apply (self, config, gallery, options, *args, **kwargs) :
+        """
+            Construct a CommandContext for execution of this command with the given environment.
+            the command with the given context.
+        """
+        
+        # apply extra options
+        for k, v in kwargs.iteritems() :
+            setattr(options, k, v)
+
+        return Environment(self, config, gallery, options, args)
+    
+    def option_group (self, parser) :
+        """
+            Returns an optparse.OptionGroup for this command's options.
+        """
+
+        group = optparse.OptionGroup(parser, "Command-specific options")
+
+        for opt in self.options :
+            group.add_option(opt)
+    
+        return group
+
+    def cmdopt_callback (self, option, opt_str, value, parser) :
+        """
+            Used as a optparse.Option callback-action, adds this command's options to the parser, and stores the
+            selected command.
+        """
+
+        # check
+        if hasattr(parser.values, option.dest) :
+            raise ArgumentError("More than one command option given: %s + %s" % (getattr(parser.values, option.dest), self.name))
+
+        if self.options :
+            # setup command-specific options
+            parser.add_option_group(self.build_options(parser))
+        
+        # store selected command
+        setattr(parser.values, option.dest, self)
 
 class CommandList (object) :
     """
-        A list of available Commands
-
-        XXX: not yet used
+        A set of available Commands
     """
 
     def __init__ (self, commands) :
@@ -26,39 +106,30 @@
 
         return self.dict[name]
 
-class Command (object) :
+    def option_group (self, parser, default) :
+        """
+            Returns an optparse.OptionGroup for these commands, using the given parser.
+        """
+        
+        group = optparse.OptionGroup(parser, "Command Options", "Select what command to execute, may introduce other options")
+
+        for command in self.list :
+            group.add_option('--%s' % optify(command.name), action='callback', callback=command.cmdopt_callback, dest='command', help=command.doc)
+        
+        # store default
+        parser.set_defaults(command=default)
+
+        return group
+
+class Environment (object) :
     """
-        A Command is simply a function that can be executed from the command line with some options/arguments
+        The environment that a Command will execute in.
+        
+        This is bound to a Configuration, a Gallery, options values, argument values and other miscellaneous things. An
+        environment also provides other services, such as status output, concurrent execution and error handling.
     """
 
-    def __init__ (self, name, func, doc=None) :
-        """
-            Create a new Command
-
-             name       - the name of the command
-             func       - the callable python function
-             doc        - descriptive help text
-        """
-
-        self.name = name
-        self.func = func
-        self.doc = doc
-
-    def setup (self, config, gallery) :
-        """
-            Run the command with the given context
-        """
-        
-        return CommandContext(self, config, gallery)
-
-class CommandContext (object) :
-    """
-        A CommandContext is the context that a Command executes in
-
-        It is bound to a Configuration and a Gallery.
-    """
-
-    def __init__ (self, command, config, gallery) :
+    def __init__ (self, command, config, gallery, options, args) :
         """
             Create the execution environment
         """
@@ -66,25 +137,27 @@
         self.command = command
         self.config = config
         self.gallery = gallery
+        self.options = options
+        self.args = args
 
         # conccurency
         self.concurrent = concurrent.Manager(thread_count=config.thread_count)
 
-    def execute (self, *args, **kwargs) :
+    def execute (self) :
         """
             Run the command in this context
         """
 
-        return self.command.func(self, *args, **kwargs)
+        return self.command.func(self, *self.args)
     
-    def run (self, *args, **kwargs) :
+    def run (self) :
         """
             Run the command with error handling
         """
 
         try :
             # run it
-            return self.execute(*args, **kwargs)
+            return self.execute()
         
         except KeyboardInterrupt :
             self.log_error("Interrupted")
@@ -93,7 +166,8 @@
             # dump traceback
             # XXX: skip all crap up to the actual function
             self.handle_error()
-
+    
+    # XXX: split off to a .log object
     def log_msg (self, level, msg, *args, **kwargs) :
         """
             Output a log message with the given level
@@ -143,10 +217,16 @@
         else :
             traceback.print_exc()
 
-def command (func) :
-    """
-        A function decorator used to define Commands automatically
-    """
+def command (options=None) :
+    def _decorator (func) :
+        """
+            A function decorator used to define Commands automatically
+        """
+        
+        # find help string
+        doc = inspect.getdoc(func)
+        
+        return Command(func.__name__, func, doc, options)
+    
+    return _decorator
 
-    return Command(func.__name__, func, inspect.getdoc(func))
-
--- a/degal/commands/__init__.py	Wed Jul 01 20:57:03 2009 +0300
+++ b/degal/commands/__init__.py	Thu Jul 02 21:59:01 2009 +0300
@@ -2,5 +2,6 @@
     Core commands
 """
 
-from main import main
+from update import update
 
+COMMANDS = (update, )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/commands/update.py	Thu Jul 02 21:59:01 2009 +0300
@@ -0,0 +1,143 @@
+from degal.command import command, Option
+from degal.concurrent import task
+from degal import templates
+
+def render_image_html (ctx, image) :
+    """
+        Render and write out the static .html file for the give image.
+    """
+
+    ctx.log_debug("%s", image.html)
+
+    # render full-xhtml-document
+    tpl = templates.master(ctx.gallery, image, 
+        # with content
+        templates.image_page(image)
+    )
+    
+    # write output
+    tpl.render_file(image.html)
+
+def update_image_thumbs (image) :
+    """
+        Update/render and write out the thumbnails (thumb+preview) for the given image, returning the image object
+        itself.
+
+        This /should/ be threadsafe. XXX: but it probably isn't entirely.
+    """
+    
+    # this will unconditionally update the image
+    image.update()
+
+    return image
+
+def render_folder_images (ctx, images) :
+    """
+        Render the given series of images (html+thumbnails) as required based on the settings.
+
+        This is capable of rendering the given set of images in parallel.
+    """
+
+    # first, update HTML
+    for image in images :
+        if ctx.options.force_html or image.stale() :
+            render_image_html(ctx, image)
+    
+    # define the render-tasks
+    tasks = (task(update_image_thumbs, image) for image in images if ctx.options.force_thumb or image.stale())
+    
+    # render the thumbnails themselves concurrently, returning the rendered Image objects
+    for image in ctx.concurrent.execute(tasks) :
+        # log image path
+        ctx.log_info("%s", image)
+
+        # release large objects that are not needed anymore
+        image.cleanup()
+
+def render_folder_html (ctx, folder) :
+    """
+        Render and write out the required static .html files for the given folder.
+
+        This will paginate large numbers of images, handle Folders with only subfolders within as a single page, and as
+        a bonus, will not render anything for (non-recursively) empty folders.
+    """
+
+    # render each page separately
+    for page in xrange(folder.page_count) :
+        # output .html path
+        html = folder.html_page(page)
+    
+        ctx.log_debug("%s", html)
+        
+        # render full-html template
+        tpl = templates.master(ctx.gallery, folder,
+            # content
+            templates.folder_page(folder, page)
+        )
+
+        # write output
+        tpl.render_file(html)
+
+def render_folder (ctx, folder) :
+    """
+        Recursively render a folder, with render_folder_images and render_folder.
+
+        This does a depth-first search of subfolders.
+        
+        Updates the Images as needed (based on config.force_thumbs/config.force_html).
+
+        Currently, this will always update the .html for non-empty Folders.
+    """
+
+    # do depth-first recursion
+    for subfolder in folder.subfolders :
+        render_folder(ctx, subfolder)
+
+    if folder.empty :
+        # warn
+        ctx.log_debug("%s - empty, skipping", folder)
+        
+        return
+ 
+    # force-update HTML, every time
+    render_folder_html(ctx, folder)
+    
+    # get the list of images that we are going to update, only those that are stale unless any force_update
+    update_images = list(folder.index_images(for_update=not ctx.options.force_index))
+    
+    if update_images :
+        # status
+        ctx.log_info("%s - rendering %d/%d images", folder, len(update_images), len(folder.images))
+
+        # update images as needed
+        render_folder_images(ctx, folder.images)
+    
+    else :
+        # nothing to do
+        ctx.log_info("%s - up-to-date", folder)
+
+@command(
+    options = (
+        Option('--force-html',    help="Force-update HTML documents", action="store_true", default="False"), 
+        Option('--force-thumb',   help="Force-update thumbnails", action="store_true", default="False"),
+        Option('-F', '--force-update', help="Force-update both", action="store_true", default="False"),
+    )
+)
+def update (ctx, *filter) :
+    """
+        Scan the gallery for new folders/images, and render updated ones.
+    """
+    
+    # do the force_update/force_index semantics
+    if ctx.options.force_update :
+        ctx.options.force_index = ctx.options.force_html = ctx.options.force_thumb = True
+
+    elif ctx.options.force_html or ctx.options.force_thumb :
+        ctx.options.force_index = True
+    
+    else :
+        ctx.options.force_index = False
+
+    # render the gallery root as a folder
+    render_folder(ctx, ctx.gallery)
+    
--- a/degal/config.py	Wed Jul 01 20:57:03 2009 +0300
+++ b/degal/config.py	Thu Jul 02 21:59:01 2009 +0300
@@ -81,18 +81,6 @@
     # 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
 
--- a/degal/html.py	Wed Jul 01 20:57:03 2009 +0300
+++ b/degal/html.py	Thu Jul 02 21:59:01 2009 +0300
@@ -66,7 +66,7 @@
     
     def render_file (self, file, encoding=None, **render_opts) :
         """
-            Render output to given file, overwriteing anything already there
+            Render output to given File, overwriteing anything already there
         """
 
         self.render_out(file.open_write(encoding), **render_opts)
--- a/degal/main.py	Wed Jul 01 20:57:03 2009 +0300
+++ b/degal/main.py	Thu Jul 02 21:59:01 2009 +0300
@@ -2,11 +2,18 @@
     Main entry point for the command-line interface
 """
 
-import gallery, commands, config, version
+import gallery, command, commands, config, version
 
 from optparse import OptionParser
 import os.path
 
+def load_commands () :
+    """
+        Build the CommandList for us to use
+    """
+
+    return command.CommandList(commands.COMMANDS)
+
 def build_config () :
     """
         Build the default configuration to use
@@ -14,28 +21,20 @@
     
     return config.Configuration()
 
-def option_parser (exec_name) :
+def option_parser (exec_name, command) :
     """
-        Build the OptionParser that we use
+        Build the OptionParser that we use, with the given command
     """
     
     parser = OptionParser(prog=exec_name, description="Degal - A photo gallery", version="Degal %s" % version.VERSION_STRING)
     
+    # core options
     parser.add_option('-C', "--config",         metavar='PATH', dest="_load_config_path",
             help="Load configuration from PATH")
 
     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",          action="store_true",
-            help="Force-update thumbnails")
-
-    parser.add_option("--force-html",           action="store_true",
-            help="Force-update .html files")
-    
     parser.add_option("--with-exif",            action="store_true",
             help="Include Exif metadata in updated .html files")
     
@@ -51,6 +50,9 @@
     parser.add_option('-q', "--quiet",          action="store_const", dest="log_level", const=config.logging.WARN,
             help="Reduced output (only warnings)")
     
+    # command's options
+    parser.add_option_group(command.option_group(parser))
+
     return parser
 
 def parse_args (config, parser, args) :
@@ -94,38 +96,23 @@
     # read path from config
     return gallery.Gallery(config.gallery_path, config)
 
-def load_command (config, args) :
-    """
-        Figure out what command to run and with what args
-    """
-    
-    # XXX: hardcoded
-    return commands.main, args, {}
-
-def run_command (config, gallery, command, args, kwargs) :
-    """
-        Run the given command
-    """
-    
-    # setup the command execution context
-    command_ctx = command.setup(config, gallery)
-  
-    # run with error handling
-    return command_ctx.run()
-
 def main (argv) :
     """
         Main entry point
     """
 
     ## load commands
-    #commands = load_commands()
+    commands = load_commands()
 
     # build our default config
     config = build_config()
+    
+    
+    # XXX: hardcoded
+    command = commands.lookup('update')
 
     # build optparser
-    parser = option_parser(argv[0])
+    parser = option_parser(argv[0], command)
 
     # parse the args into our config
     args = parse_args(config, parser, argv[1:])
@@ -136,13 +123,9 @@
     # open gallery
     gallery = load_gallery(config)
 
-    # 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)
+    # XXX: mix up configs with options
+    ret = command.apply(config, gallery, config, *args).run()
 
 
     if ret is None :