# HG changeset patch # User Tero Marttila # Date 1246561141 -10800 # Node ID 97505a78900343d59a09aca6835679d646599911 # Parent a6e53a20fccb9a2cb88dbbb4838e8f40f056fd2a reorganize/rename the commands and their options stuff, options like --force-html are now split out from main.py into commands/update.py diff -r a6e53a20fccb -r 97505a789003 degal/command.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)) - diff -r a6e53a20fccb -r 97505a789003 degal/commands/__init__.py --- 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, ) diff -r a6e53a20fccb -r 97505a789003 degal/commands/update.py --- /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) + diff -r a6e53a20fccb -r 97505a789003 degal/config.py --- 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 diff -r a6e53a20fccb -r 97505a789003 degal/html.py --- 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) diff -r a6e53a20fccb -r 97505a789003 degal/main.py --- 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 :