reorganize/rename the commands and their options stuff, options like --force-html are now split out from main.py into commands/update.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 :