More pylint-related changes.
authorJoel Rosdahl <joel@rosdahl.net>
Tue, 27 Sep 2005 06:28:57 +0000 (06:28 +0000)
committerJoel Rosdahl <joel@rosdahl.net>
Tue, 27 Sep 2005 06:28:57 +0000 (06:28 +0000)
15 files changed:
check.py
src/packages/kofoto/EXIF.py
src/packages/kofoto/alternative.py
src/packages/kofoto/common.py
src/packages/kofoto/config.py
src/packages/kofoto/imagecache.py
src/packages/kofoto/output/__init__.py
src/packages/kofoto/output/woolly.py
src/packages/kofoto/outputengine.py
src/packages/kofoto/search.py
src/packages/kofoto/shelf.py
src/packages/kofoto/shelfupgrade.py
src/packages/kofoto/structclass.py
src/packages/kofoto/timer.py
src/packages/kofoto/version.py

index 38ff48d..c703500 100755 (executable)
--- a/check.py
+++ b/check.py
@@ -1,21 +1,64 @@
 #! /usr/bin/env python
 
+import os
 import sys
+from optparse import OptionParser
 from pylint import lint
 
-sys.path.insert(0, "src/packages")
-if len(sys.argv) > 1:
-    modules = sys.argv[1:]
+def disable_message(arguments, message_id):
+    arguments.append("--disable-msg=%s" % message_id)
+
+######################################################################
+
+option_parser = OptionParser()
+option_parser.add_option(
+    "--all",
+    action="store_true",
+    help="perform all checks",
+    default=False)
+option_parser.add_option(
+    "--all-complexity",
+    action="store_true",
+    help="perform all complexity checks",
+    default=False)
+options, args = option_parser.parse_args(sys.argv[1:])
+
+topdir = os.path.dirname(sys.argv[0])
+
+sys.path.insert(0, os.path.join(topdir, "src/packages"))
+if len(args) > 0:
+    modules = args
 else:
-    modules = ["kofoto", "kofoto.commandline", "kofoto.output"]
+    modules = ["kofoto"]
 
-tests_to_disable = [
+normally_disabled_tests = [
     "C0101", # "Too short variable name."
+    "I0011", # "Locally disabling ..."
     "W0142", # "Used * or ** magic."
     "W0704", # "Except doesn't do anything."
 ]
 
-lint.Run(
-    ["--rcfile", "misc/pylintrc"] +
-    ["--disable-msg=" + x for x in tests_to_disable] +
-    modules)
+normally_disabled_complexity_tests = [
+    "R0901", # "Too many parent classes."
+    "R0902", # "Too many instance attributes."
+    "R0903", # "Not enough public methods."
+    "R0904", # "Too many public methods."
+    "R0911", # "Too many return statement."
+    "R0912", # "Too many branches."
+    "R0913", # "Too many arguments."
+    "R0914", # "Too many local variables."
+    "R0915", # "Too many statements."
+    "W0302", # "Too many lines in module."
+    "C0301", # "Line too long."
+]
+
+rc_file_location = os.path.join(topdir, "misc/pylintrc")
+flags = ["--rcfile", rc_file_location]
+if not options.all:
+    for x in normally_disabled_tests:
+        disable_message(flags, x)
+    if not options.all_complexity:
+        for x in normally_disabled_complexity_tests:
+            disable_message(flags, x)
+
+lint.Run(flags + modules)
index f9d26c8..01d846e 100644 (file)
@@ -1,3 +1,7 @@
+# pylint: disable-msg=W0311, W0302, W0622, C0103, W0131, W0402, C0322, W0141
+# pylint: disable-msg=R0913, W0621, W0612, R0914, W0102, W0702, R0912, R0915
+# pylint: disable-msg=W0702
+
 # Library to extract EXIF information in digital camera image files
 #
 # Contains code from "exifdump.py" originally written by Thierry Bousch
index 67d89df..fbb4aa7 100644 (file)
@@ -62,6 +62,8 @@ class Alternative:
         return x in self.__identifiers
 
 class AlternativeInstance:
+    """An alternativ instance."""
+
     def __init__(self, identifier):
         self.__identifier = identifier
 
index de34f8f..fd5ca00 100644 (file)
@@ -5,6 +5,7 @@
 
 __all__ = [
     "KofotoError",
+    "UnimplementedError",
     "calculateDownscaledDimensions",
     "symlinkOrCopyFile",
     ]
@@ -19,7 +20,9 @@ import os
 
 class KofotoError(Exception):
     """Base class for Kofoto exceptions."""
-    pass
+
+class UnimplementedError(KofotoError):
+    """Unimplemented method."""
 
 ######################################################################
 ### Functions.
index 6663c7d..d9d8fc5 100644 (file)
@@ -1,3 +1,5 @@
+# pylint: disable-msg=C0301, W0221
+
 """Configuration module for Kofoto."""
 
 __all__ = [
@@ -12,8 +14,9 @@ __all__ = [
     "createConfigTemplate",
 ]
 
-from ConfigParser import *
-ConfigParserMissingSectionHeaderError = MissingSectionHeaderError
+from ConfigParser import ConfigParser
+from ConfigParser import MissingSectionHeaderError as \
+    ConfigParserMissingSectionHeaderError
 import os
 import re
 import sys
@@ -35,32 +38,59 @@ else:
         "~", ".kofoto", "imagecache")
 
 class ConfigError(KofotoError):
+    """Configuration error."""
     pass
 
 class MissingSectionHeaderError(KofotoError):
+    """A section header is missing in the configuration file."""
     pass
 
 class MissingConfigurationKeyError(KofotoError):
+    """A key is missing in the configuration file."""
     pass
 
 class BadConfigurationValueError(KofotoError):
+    """A value is badly formatted in the configuration file."""
     pass
 
 class Config(ConfigParser):
+    """A customized configuration parser."""
+
     def __init__(self, encoding):
+        """Constructor.
+
+        Arguments:
+
+        encoding -- The encoding to use when translating between Unicode and
+                    byte strings.
+        """
         ConfigParser.__init__(self)
         self.encoding = encoding
 
     def read(self, filenames):
+        """Read configuration files."""
         try:
             ConfigParser.read(self, filenames)
         except ConfigParserMissingSectionHeaderError:
             raise MissingSectionHeaderError
 
     def get(self, *args, **kwargs):
+        """Get a configuration item.
+
+        This method wraps ConfigReader.read and decodes the value into
+        Unicode according to the chosen encoding.
+        """
         return unicode(ConfigParser.get(self, *args, **kwargs), self.encoding)
 
     def getcoordlist(self, section, option):
+        """Get a coordinate list.
+
+        Coordinate lists look like this:
+
+        100x200, 1024x768 3000x2000
+
+        Returns a list of two-tuples of integers.
+        """
         val = self.get(section, option)
         coords = re.split("[,\s]+", val)
         ret = []
@@ -79,7 +109,10 @@ class Config(ConfigParser):
         return ret
 
     def verify(self):
+        """Verify the Kofoto configuration."""
+
         def checkConfigurationItem(section, key, function):
+            """Internal helper."""
             if not self.has_option(section, key):
                 raise MissingConfigurationKeyError, (section, key)
             value = self.get(section, key)
@@ -99,6 +132,8 @@ class Config(ConfigParser):
 
 
 def createConfigTemplate(filename):
+    """Write a Kofoto configuration template to a file."""
+
     file(filename, "w").write(
         """### Configuration file for Kofoto.
 
index d9f93e6..a283982 100644 (file)
@@ -1,10 +1,14 @@
+"""Implementation of the ImageCache class."""
+
 __all__ = ["ImageCache"]
 
 import os
 import Image as PILImage
-from kofoto.common import calculateDownscaledDimensions, symlinkOrCopyFile
+from kofoto.common import calculateDownscaledDimensions
 
 class ImageCache:
+    """A class representing the Kofoto image cache."""
+
     def __init__(self, cacheLocation, useOrientation=False):
         """Constructor.
 
@@ -89,6 +93,7 @@ class ImageCache:
 
     def _get(self, location, mtime, width, height, widthlimit,
              heightlimit, orientation):
+        """Internal helper method."""
         # Scale image to fit within limits.
         w, h = calculateDownscaledDimensions(
             width, height, widthlimit, heightlimit)
@@ -102,7 +107,7 @@ class ImageCache:
 
         # No version of the wanted size existed in the cache. Create
         # one.
-        directory, filename = os.path.split(path)
+        directory, _ = os.path.split(path)
         if not os.path.isdir(directory):
             os.makedirs(directory)
         pilimg = PILImage.open(location)
@@ -127,6 +132,7 @@ class ImageCache:
 
     def _getCachedImagePath(self, location, mtime, width, height,
                             orientation):
+        """Internal helper method."""
         drive, drivelessPath = os.path.splitdrive(location)
         directory, filename = os.path.split(drivelessPath[1:])
         if drive:
index ec721f1..a5d1a22 100644 (file)
@@ -1,3 +1,7 @@
+# pylint: disable-msg=C0301
+
+'''Implementation of the output module "woolly".'''
+
 import os
 import re
 from kofoto.outputengine import OutputEngine
@@ -304,8 +308,11 @@ image_frame_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
 
 
 class OutputGenerator(OutputEngine):
+    '''A class implementing the "woolly" output module.'''
+
     def __init__(self, env, character_encoding):
         OutputEngine.__init__(self, env)
+        self.iconsdir = None
         self.env = env
         self.charEnc = character_encoding
         if env.config.has_option("woolly", "display_categories"):
@@ -329,12 +336,14 @@ class OutputGenerator(OutputEngine):
             pass
 
 
-    def preGeneration(self, root):
+    def preGeneration(self, _):
+        """Method called before generation of the output."""
         self.iconsdir = "@icons"
         self.makeDirectory(self.iconsdir)
 
 
     def postGeneration(self, root):
+        """Method called after generation of the output."""
         if self.env.verbose:
             self.env.out("Generating index page, style sheet and icons...\n")
         self.symlinkFile(
@@ -367,6 +376,14 @@ class OutputGenerator(OutputEngine):
 
 
     def generateAlbum(self, album, subalbums, images, paths):
+        """Method called to generate output of an album.
+
+        Arguments:
+
+        album     -- The Album instance.
+        subalbums -- Album children of the album.
+        images    -- Image children of the album.
+        """
         # ------------------------------------------------------------
         # Create album overview pages, one per size limit.
         # ------------------------------------------------------------
@@ -523,71 +540,104 @@ class OutputGenerator(OutputEngine):
 
 
     def generateImage(self, album, image, images, number, paths):
+        """Method called to generate output of an album.
+
+        Arguments:
+
+        album     -- The parent album of the image.
+        image     -- The Image instance.
+        images    -- A list of images in the album.
+        number    -- The current image's index in the image list.
+        paths     -- A list of lists of Album instances.
+        """
         # ------------------------------------------------------------
         # Create image frameset and image fram, one per size limit.
         # ------------------------------------------------------------
 
         for limnumber, (wlim, hlim) in enumerate(self.env.imagesizelimits):
             pathtext = self._generatePathText(wlim, hlim, paths, "../")
-            uplink = '<link rel="up" href="../%s-%dx%d.html" target="_top" />' % (
-                paths[0][-1].getTag().encode(self.charEnc),
-                wlim,
-                hlim)
-
-            if number > 0:
-                previouslink = '<link rel="previous" href="%s-%dx%d-frame.html" />' % (
-                    number - 1,
+            uplink = \
+                '<link rel="up" href="../%s-%dx%d.html" target="_top" />' % (
+                    paths[0][-1].getTag().encode(self.charEnc),
                     wlim,
                     hlim)
-                previoustext = '<a href="%s"><img class="icon" src="../%s/previous.png" alt="Previous image" /></a>' % (
-                    "%s-%dx%d.html" % (number - 1, wlim, hlim),
-                    self.iconsdir)
-                cpi_text = '<img src="%s" width="1" height="1" style="display: none" alt="" >' % (
-                    "../" + self.getImageReference(
-                                images[number - 1], wlim, hlim)[0])
+
+            if number > 0:
+                previouslink = \
+                    '<link rel="previous" href="%s-%dx%d-frame.html" />' % (
+                        number - 1,
+                        wlim,
+                        hlim)
+                previoustext = (
+                    '<a href="%s"><img class="icon" src="../%s/previous.png"'
+                    ' alt="Previous image" /></a>' % (
+                        "%s-%dx%d.html" % (number - 1, wlim, hlim),
+                        self.iconsdir))
+                cpi_text = (
+                    '<img src="%s" width="1" height="1" style="display: none"'
+                    ' alt="" >' % (
+                        "../" + self.getImageReference(
+                            images[number - 1], wlim, hlim)[0]))
             else:
                 previouslink = ""
-                previoustext = '<img class="icon" src="../%s/noprevious.png" alt="No previous image" />' % self.iconsdir
+                previoustext = (
+                    '<img class="icon" src="../%s/noprevious.png"'
+                    ' alt="No previous image" />' % self.iconsdir)
                 cpi_text = ""
 
             if number < len(images) - 1:
                 nextlink = '<link rel="next" href="%s-%dx%d-frame.html" />' % (
                     number + 1, wlim, hlim)
-                nexttext = '<a href="%s"><img class="icon" src="../%s/next.png"  alt="Next image" /></a>' % (
-                    "%s-%dx%d.html" % (number + 1, wlim, hlim),
-                    self.iconsdir)
-                cni_text = '<img src="%s" width="1" height="1" style="display: none" alt="" >' % (
-                    "../" + self.getImageReference(
-                                images[number + 1], wlim, hlim)[0])
+                nexttext = (
+                    '<a href="%s"><img class="icon" src="../%s/next.png"'
+                    ' alt="Next image" /></a>' % (
+                        "%s-%dx%d.html" % (number + 1, wlim, hlim),
+                        self.iconsdir))
+                cni_text = (
+                    '<img src="%s" width="1" height="1" style="display: none"'
+                    ' alt="" >' % (
+                        "../" + self.getImageReference(
+                            images[number + 1], wlim, hlim)[0]))
             else:
                 nextlink = ""
-                nexttext = '<img class="icon" src="../%s/nonext.png" alt="No next image" />' % self.iconsdir
+                nexttext = (
+                    '<img class="icon" src="../%s/nonext.png"'
+                    ' alt="No next image" />' % self.iconsdir)
                 cni_text = ""
 
             if limnumber > 0:
-                smallertext = '<a href="%s" target="_top"><img class="icon" src="../%s/smaller.png" alt="Smaller image" /></a>' % (
-                    "%s-%dx%d-frame.html" % (
-                        number,
-                        self.env.imagesizelimits[limnumber - 1][0],
-                        self.env.imagesizelimits[limnumber - 1][1]),
-                    self.iconsdir)
+                smallertext = (
+                    '<a href="%s" target="_top"><img class="icon"'
+                    ' src="../%s/smaller.png" alt="Smaller image" /></a>' % (
+                        "%s-%dx%d-frame.html" % (
+                            number,
+                            self.env.imagesizelimits[limnumber - 1][0],
+                            self.env.imagesizelimits[limnumber - 1][1]),
+                        self.iconsdir))
 
             else:
-                smallertext = '<img class="icon" src="../%s/nosmaller.png" alt="No smaller image available" />' % self.iconsdir
+                smallertext = (
+                    '<img class="icon" src="../%s/nosmaller.png"'
+                    ' alt="No smaller image available" />' % self.iconsdir)
 
             if limnumber < len(self.env.imagesizelimits) - 1:
-                largertext = '<a href="%s" target="_top"><img class="icon" src="../%s/larger.png" alt="Larger image" /></a>' % (
-                    "%s-%dx%d-frame.html" % (
-                        number,
-                        self.env.imagesizelimits[limnumber + 1][0],
-                        self.env.imagesizelimits[limnumber + 1][1]),
-                    self.iconsdir)
+                largertext = (
+                    '<a href="%s" target="_top"><img class="icon"'
+                    ' src="../%s/larger.png" alt="Larger image" /></a>' % (
+                        "%s-%dx%d-frame.html" % (
+                            number,
+                            self.env.imagesizelimits[limnumber + 1][0],
+                            self.env.imagesizelimits[limnumber + 1][1]),
+                        self.iconsdir))
             else:
-                largertext = '<img class="icon" src="../%s/nolarger.png" alt="No larger image available" />' % self.iconsdir
-
-            desc = (image.getAttribute(u"description") or
-                    image.getAttribute(u"title") or
-                    u"")
+                largertext = (
+                    '<img class="icon" src="../%s/nolarger.png"'
+                    ' alt="No larger image available" />' % self.iconsdir)
+
+            desc = (
+                image.getAttribute(u"description") or
+                image.getAttribute(u"title") or
+                u"")
             desc = desc.encode(self.charEnc)
             title = image.getAttribute(u"title") or u""
             title = title.encode(self.charEnc)
@@ -612,7 +662,9 @@ class OutputGenerator(OutputEngine):
                 else:
                     descElement = ""
             infotextElements.append("<p>%s</p>\n" % descElement)
-            infotextElements.append('<table border="0" cellpadding="0" cellspacing="0" width="100%">\n<tr>')
+            infotextElements.append(
+                '<table border="0" cellpadding="0" cellspacing="0"'
+                ' width="100%">\n<tr>')
             firstrow = True
             for dispcat in self.displayCategories:
                 matching = [x.getDescription().encode(self.charEnc)
@@ -625,7 +677,8 @@ class OutputGenerator(OutputEngine):
                     else:
                         infotextElements.append("<td></td></tr>\n<tr>")
                     infotextElements.append(
-                        '<td align="left"><small><b>%s</b>: %s</small></td>' % (
+                        '<td align="left"><small><b>%s</b>:'
+                        ' %s</small></td>' % (
                             dispcat.getDescription().encode(self.charEnc),
                             ", ".join(matching)))
             infotextElements.append('</td><td align="right">')
@@ -638,8 +691,9 @@ class OutputGenerator(OutputEngine):
             infotext = "".join(infotextElements)
 
             self.writeFile(
-                os.path.join(str(album.getId()),
-                             "%s-%dx%d-frame.html" % (number, wlim, hlim)),
+                os.path.join(
+                    str(album.getId()),
+                    "%s-%dx%d-frame.html" % (number, wlim, hlim)),
                 image_frameset_template % {
                     "albumtitle": title,
                     "charenc": self.charEnc,
@@ -685,6 +739,8 @@ class OutputGenerator(OutputEngine):
 
 
     def _generatePathText(self, wlim, hlim, paths, pathprefix):
+        """Internal helper method."""
+
         # Create path text, used in top of the album overview.
         pathtextElements = []
         pathtextElements.append("<table width=\"100%\">\n")
@@ -693,18 +749,22 @@ class OutputGenerator(OutputEngine):
             els = []
             for node in path:
                 title = node.getAttribute(u"title") or node.getTag()
-                els.append('''<a href="%(pathprefix)s%(htmlref)s" target="_top">%(title)s</a>''' % {
-                    "htmlref": "%s-%dx%d.html" % (
-                        node.getTag().encode(self.charEnc),
-                        wlim,
-                        hlim),
-                    "pathprefix": pathprefix,
-                    "title": title.encode(self.charEnc),
-                    })
+                els.append(
+                    '''<a href="%(pathprefix)s%(htmlref)s" target="_top">'''
+                    '''%(title)s</a>''' % {
+                        "htmlref": "%s-%dx%d.html" % (
+                            node.getTag().encode(self.charEnc),
+                            wlim,
+                            hlim),
+                        "pathprefix": pathprefix,
+                        "title": title.encode(self.charEnc),
+                        })
             pathtextElements.append(
                 u" \xbb ".encode(self.charEnc).join(els))
             pathtextElements.append("</td>\n")
-            pathtextElements.append("<td width=\"40%\" align=\"right\" style=\"text-align: right; width: 40%\">\n")
+            pathtextElements.append(
+                "<td width=\"40%\" align=\"right\" style=\"text-align:"
+                " right; width: 40%\">\n")
             if len(path) == 1:
                 prevalbumtext = ""
             else:
@@ -719,15 +779,21 @@ class OutputGenerator(OutputEngine):
                     sibling = children[thispos - 1]
                     title = (sibling.getAttribute(u"title") or
                              sibling.getTag())
-                    prevalbumtext = '<a href="%(pathprefix)s%(htmlref)s" target="_top"><img class="icon" src="%(pathprefix)s%(iconsdir)s/previousalbum.png" alt="Previous album" /></a>&nbsp;<a href="%(pathprefix)s%(htmlref)s" target="_top">%(title)s</a>' % {
-                        "htmlref": "%s-%dx%d.html" % (
-                            sibling.getTag().encode(self.charEnc),
-                            wlim,
-                            hlim),
-                        "iconsdir": self.iconsdir,
-                        "pathprefix": pathprefix,
-                        "title": title.replace(" ", "&nbsp;").encode(self.charEnc)
-                        }
+                    prevalbumtext = (
+                        '<a href="%(pathprefix)s%(htmlref)s" target="_top">'
+                        '<img class="icon" src="%(pathprefix)s%(iconsdir)s/'
+                        'previousalbum.png" alt="Previous album" /></a>'
+                        '&nbsp;<a href="%(pathprefix)s%(htmlref)s"'
+                        ' target="_top">%(title)s</a>' % {
+                            "htmlref": "%s-%dx%d.html" % (
+                                sibling.getTag().encode(self.charEnc),
+                                wlim,
+                                hlim),
+                            "iconsdir": self.iconsdir,
+                            "pathprefix": pathprefix,
+                            "title": title.replace(" ", "&nbsp;").encode(
+                                self.charEnc)
+                            })
                 if thispos == len(children) - 1:
                     # No next sibling.
                     nextalbumtext = ""
@@ -735,15 +801,21 @@ class OutputGenerator(OutputEngine):
                     sibling = children[thispos + 1]
                     title = (sibling.getAttribute(u"title") or
                              sibling.getTag())
-                    nextalbumtext = '<a href="%(pathprefix)s%(htmlref)s" target="_top"><img class="icon" src="%(pathprefix)s%(iconsdir)s/nextalbum.png" alt="Next album" /></a>&nbsp;<a href="%(pathprefix)s%(htmlref)s" target="_top">%(title)s</a>' % {
-                        "htmlref": "%s-%dx%d.html" % (
-                            sibling.getTag().encode(self.charEnc),
-                            wlim,
-                            hlim),
-                        "iconsdir": self.iconsdir,
-                        "pathprefix": pathprefix,
-                        "title": title.replace(" ", "&nbsp;").encode(self.charEnc),
-                        }
+                    nextalbumtext = (
+                        '<a href="%(pathprefix)s%(htmlref)s" target="_top">'
+                        '<img class="icon" src="%(pathprefix)s%(iconsdir)s/'
+                        'nextalbum.png" alt="Next album" /></a>'
+                        '&nbsp;<a href="%(pathprefix)s%(htmlref)s"'
+                        ' target="_top">%(title)s</a>' % {
+                            "htmlref": "%s-%dx%d.html" % (
+                                sibling.getTag().encode(self.charEnc),
+                                wlim,
+                                hlim),
+                            "iconsdir": self.iconsdir,
+                            "pathprefix": pathprefix,
+                            "title": title.replace(" ", "&nbsp;").encode(
+                                self.charEnc),
+                            })
                 pathtextElements.append(prevalbumtext)
                 pathtextElements.append("\n")
                 pathtextElements.append(nextalbumtext)
@@ -752,31 +824,33 @@ class OutputGenerator(OutputEngine):
         return "".join(pathtextElements)
 
 
-    def _getFrontImage(self, object, visited=None):
-        if visited and object.getId() in visited:
+    def _getFrontImage(self, obj, visited=None):
+        """Internal helper method."""
+        if visited and obj.getId() in visited:
             return None
 
-        if object.isAlbum():
+        if obj.isAlbum():
             if not visited:
                 visited = []
-            visited.append(object.getId())
-            thumbid = object.getAttribute(u"frontimage")
+            visited.append(obj.getId())
+            thumbid = obj.getAttribute(u"frontimage")
             if thumbid:
                 from kofoto.shelf import ImageDoesNotExistError
                 try:
                     return self.env.shelf.getImage(int(thumbid))
                 except ImageDoesNotExistError:
                     pass
-            children = iter(object.getChildren())
+            children = iter(obj.getChildren())
             try:
                 return self._getFrontImage(children.next(), visited)
             except StopIteration:
                 return None
         else:
-            return object
+            return obj
 
 
     def _maybeMakeUTF8Symlink(self, filename):
+        """Internal helper method."""
         try:
             # Check whether the filename contains ASCII characters
             # only. If so, do nothing.
index b6d6c98..bb18725 100644 (file)
@@ -1,20 +1,68 @@
+"""Implementation of the OutputEngine class."""
+
 __all__ = ["OutputEngine"]
 
 import os
 import re
 import time
 from sets import Set
-from kofoto.common import symlinkOrCopyFile
+from kofoto.common import symlinkOrCopyFile, UnimplementedError
 
 class OutputEngine:
+    """An abstract base class for output generators of an album tree."""
+
     def __init__(self, env):
         self.env = env
-        self.blurb = 'Generated by <a href="http://kofoto.rosdahl.net" target="_top">Kofoto</a> %s.' % time.strftime("%Y-%m-%d %H:%M:%S")
+        self.blurb = (
+            'Generated by <a href="http://kofoto.rosdahl.net"'
+            ' target="_top">Kofoto</a> %s.' %
+            time.strftime("%Y-%m-%d %H:%M:%S"))
         self.generatedFiles = Set()
+        self.__dest = None
+        self.__imgrefMap = None
+
+
+    def preGeneration(self, root):
+        """Method called before generation of the output."""
+        raise UnimplementedError
+
+
+    def postGeneration(self, root):
+        """Method called after generation of the output."""
+        raise UnimplementedError
+
+
+    def generateAlbum(self, album, subalbums, images, paths):
+        """Method called to generate output of an album.
+
+        Arguments:
+
+        album     -- The Album instance.
+        subalbums -- Album children of the album.
+        images    -- Image children of the album.
+        """
+        raise UnimplementedError
+
+
+    def generateImage(self, album, image, images, number, paths):
+        """Method called to generate output of an album.
+
+        Arguments:
+
+        album     -- The parent album of the image.
+        image     -- The Image instance.
+        images    -- A list of images in the album.
+        number    -- The current image's index in the image list.
+        paths     -- A list of lists of Album instances.
+        """
+        raise UnimplementedError
 
 
     def getImageReference(self, image, widthlimit, heightlimit):
+        """Get a href to an image of given limits."""
+
         def helper(ext):
+            """Internal helper function."""
             # Given the image, this function computes and returns a
             # suitable image name and a reference be appended to
             # "@images/<size>/".
@@ -31,9 +79,9 @@ class OutputEngine:
                                   .replace(":", "") \
                                   .replace("-", "")
                         # Also handle time stamps like "2004-11-11 +/- 3 days"
-                        filename = "%s%s" % (re.match("^(\w*)",
-                                                       timestr).group(1),
-                                              ext)
+                        filename = "%s%s" % (
+                            re.match("^(\w*)", timestr).group(1),
+                            ext)
                         return "/".join([year, month, filename])
             filename = "%s%s" % (image.getId(), ext)
             return "/".join([year, filename])
@@ -44,7 +92,7 @@ class OutputEngine:
             raise Exception, "No image versions for image %d" % image.getid()
 
         key = (imageversion.getHash(), widthlimit, heightlimit)
-        if not self.imgref.has_key(key):
+        if not self.__imgrefMap.has_key(key):
             if self.env.verbose:
                 self.env.out("Generating image %d, size limit %dx%d..." % (
                     image.getId(), widthlimit, heightlimit))
@@ -64,51 +112,78 @@ class OutputEngine:
                 base, ext = os.path.splitext(htmlimgloc)
                 htmlimgloc = re.sub(r"(-\d*)?$", "-%d" % i, base) + ext
                 i += 1
-            imgloc = os.path.join(self.dest, htmlimgloc)
+            imgloc = os.path.join(self.__dest, htmlimgloc)
             try:
                 os.makedirs(os.path.dirname(imgloc))
             except OSError:
                 pass
             symlinkOrCopyFile(imgabsloc, imgloc)
-            self.imgref[key] = ("/".join(htmlimgloc.split(os.sep)),
-                                width,
-                                height)
+            self.__imgrefMap[key] = (
+                "/".join(htmlimgloc.split(os.sep)),
+                width,
+                height)
             if self.env.verbose:
                 self.env.out("\n")
-        return self.imgref[key]
+        return self.__imgrefMap[key]
 
 
     def writeFile(self, filename, text, binary=False):
+        """Write a text to a file in the generated directory.
+
+        Arguments:
+
+        filename -- A location in the generated directory.
+        text     -- The text to write.
+        binary   -- Whether the text is to be treated as binary.
+        """
         if binary:
             mode = "wb"
         else:
             mode = "w"
-        file(os.path.join(self.dest, filename), mode).write(text)
+        file(os.path.join(self.__dest, filename), mode).write(text)
 
 
     def symlinkFile(self, source, destination):
-        symlinkOrCopyFile(source, os.path.join(self.dest, destination))
+        """Create a symlink in the generated directory to a file.
 
+        Arguments:
 
-    def makeDirectory(self, dir):
-        absdir = os.path.join(self.dest, dir)
+        source      -- A location in the filesystem.
+        destination -- A location in the generated directory.
+        """
+        symlinkOrCopyFile(source, os.path.join(self.__dest, destination))
+
+
+    def makeDirectory(self, directory):
+        """Make a directory in the generated directory."""
+        absdir = os.path.join(self.__dest, directory)
         if not os.path.isdir(absdir):
             os.mkdir(absdir)
 
 
     def generate(self, root, subalbums, dest):
+        """Start the engine.
+
+        Arguments:
+
+        root      -- Album to generate.
+        subalbums -- If false, generate all descendants of the root. 
+                     Otherwise a list of Album instances to generate.
+        """
+
         def addDescendants(albumset, album):
+            """Internal helper function."""
             if not album in albumset:
                 albumset.add(album)
                 for child in album.getAlbumChildren():
                     addDescendants(albumset, child)
 
-        self.dest = dest.encode(self.env.codeset)
+        self.__dest = dest.encode(self.env.codeset)
         try:
-            os.mkdir(self.dest)
+            os.mkdir(self.__dest)
         except OSError:
             pass
-        self.imgref = {}
+        self.__imgrefMap = {}
 
         self.env.out("Calculating album paths...\n")
         albummap = _findAlbumPaths(root)
@@ -144,6 +219,7 @@ class OutputEngine:
 
 
     def _generateAlbumHelper(self, album, paths):
+        """Internal helper function."""
         if self.env.verbose:
             self.env.out("Generating album page for %s...\n" %
                          album.getTag().encode(self.env.codeset))
@@ -176,8 +252,10 @@ def _findAlbumPaths(startalbum):
     The traversal is started at startalbum. The return value is a
     mapping where each key is an Album instance and the associated
     value is a list of paths, where a path is a list of Album
-    instances."""
+    instances.
+    """
     def helper(album, path):
+        """Internal helper function."""
         if album in path:
             # Already visited album, so break recursion here.
             return
index c35a361..973d462 100644 (file)
@@ -1,32 +1,38 @@
-# LL(1) grammar for search expressions:
-#
-# <searchexpr> ::= <expr> <eof>
-#
-# <expr> ::= <andexpr> ("or" <andexpr>)*
-#
-# <andexpr> ::= <notexpr> ("and" <notexpr>)*
-#
-# <notexpr> ::= "not" <term> | <term>
-#
-# <term> ::=   <bareword>                           (a category)
-#            | "exactly" <bareword>                 (a category)
-#            | <album>
-#            | <attribute> <attroper> <attrvalue>
-#            | "(" <expr> ")"
-#
-# <bareword> ::= \w [\w-]*
-#
-# <album> ::= "/" \w [\w-]*
-#
-# <attribute> ::= "@" \w [\w-]*
-#
-# <attroper> ::= "=" | "!=" | "<" | ">" | "<=" | ">="
-#
-# <attrvalue> ::= <quoted string> | <bareword>
-#
-# <quoted string> ::= "\"" .* "\""   (where each backslash and quotation mark in
-#                                     the .* part is preceeded by a backslash)
-# where \w is alpha-numeric characters and underscore.
+'''Implementation of Kofoto searching and search expression parsing.
+
+The module implements a hand-coded recursive descent parser for search
+expressions.
+
+The LL(1) grammar for search expressions is defined as follows:
+
+<searchexpr> ::= <expr> <eof>
+
+<expr> ::= <andexpr> ("or" <andexpr>)*
+
+<andexpr> ::= <notexpr> ("and" <notexpr>)*
+
+<notexpr> ::= "not" <term> | <term>
+
+<term> ::=   <bareword>                           (a category)
+           | "exactly" <bareword>                 (a category)
+           | <album>
+           | <attribute> <attroper> <attrvalue>
+           | "(" <expr> ")"
+
+<bareword> ::= \w [\w-]*
+
+<album> ::= "/" \w [\w-]*
+
+<attribute> ::= "@" \w [\w-]*
+
+<attroper> ::= "=" | "!=" | "<" | ">" | "<=" | ">="
+
+<attrvalue> ::= <quoted string> | <bareword>
+
+<quoted string> ::= "\"" .* "\""   (where each backslash and quotation mark in
+                                    the .* part is preceeded by a backslash)
+where \w is alpha-numeric characters and underscore.
+'''
 
 __all__ = [
     "BadTokenError",
@@ -37,23 +43,40 @@ __all__ = [
 ]
 
 import re
-from kofoto.common import KofotoError
+from kofoto.common import KofotoError, UnimplementedError
 import kofoto.shelf
 
 class ParseError(KofotoError):
+    """Base class for parse error exceptions related to search expressions."""
     pass
 
 class BadTokenError(ParseError):
+    """Bad token in search expression."""
     pass
 
 class UnterminatedStringError(ParseError):
+    """Search expression has an unterminated string."""
     pass
 
 class SearchNodeFactory:
+    """A class with factory methods constructing search tree nodes."""
+
     def __init__(self, shelf):
+        """Constructor.
+
+        Arguments:
+
+        shelf -- The shelf instance.
+        """
         self._shelf = shelf
 
     def albumNode(self, tag_or_album):
+        """Construct an AlbumSearchNode instance.
+
+        Arguments:
+
+        tag_or_album -- A tag string or Album instance.
+        """
         if isinstance(tag_or_album, kofoto.shelf.Album):
             album = tag_or_album
         else:
@@ -61,13 +84,33 @@ class SearchNodeFactory:
         return AlbumSearchNode(self._shelf, album)
 
     def andNode(self, subnodes):
+        """Construct an AndSearchNode instance.
+
+        Arguments:
+
+        subnodes -- A list of subnodes.
+        """
         return AndSearchNode(subnodes)
 
     def attrcondNode(self, name, operator, value):
+        """Construct an AttributeConditionSearchNode instance.
+
+        Arguments:
+
+        name     -- Name of the attribute.
+        operator -- A string representing the operator (=, !=, <, >, <= or >=).
+        value    -- The value of the attribute.
+        """
         assert operator in ["=", "!=", "<", ">", "<=", ">="]
         return AttributeConditionSearchNode(name, operator, value)
 
     def categoryNode(self, tag_or_category, recursive=False):
+        """Construct an CategorySearchNode instance.
+
+        Arguments:
+
+        tag_or_category -- A tag string or Category instance.
+        """
         if isinstance(tag_or_category, kofoto.shelf.Category):
             category = tag_or_category
         else:
@@ -80,72 +123,109 @@ class SearchNodeFactory:
         return CategorySearchNode(catids)
 
     def notNode(self, subnode):
+        """Construct an NotSearchNode instance.
+
+        Arguments:
+
+        subnode -- The subnode.
+        """
         return NotSearchNode(subnode)
 
     def orNode(self, subnodes):
+        """Construct an OrSearchNode instance.
+
+        Arguments:
+
+        subnodes -- A list of subnodes.
+        """
         return OrSearchNode(subnodes)
 
 
 class Parser:
+    """A recursive descent parser of Kofoto search expressions."""
+
     def __init__(self, shelf):
+        """Constructor.
+
+        Arguments:
+
+        shelf -- The shelf instance.
+        """
         self._snfactory = SearchNodeFactory(shelf)
+        self._scanner = None
 
     def parse(self, string):
+        """Parse a search expression.
+
+        Arguments:
+
+        string -- The search expression.
+
+        Returns a SearchNode.
+        """
+
         assert isinstance(string, unicode), "non-Unicode search string"
         self._scanner = Scanner(string)
-        return self.searchexpr()
+        return self.__searchexpr()
 
-    def searchexpr(self):
-        expr = self.expr()
+    def __searchexpr(self):
+        """Parse a <searchexpr> term."""
+        expr = self.__expr()
         kind, token = self._scanner.next()
         if kind != "eof":
-            raise ParseError, \
-                "expected end of expression or conjunction, got: \"%s\"" % token
+            raise ParseError(
+                "expected end of expression or conjunction, got: \"%s\"" %
+                token)
         return expr
 
-    def expr(self):
-        andexprs = [self.andexpr()]
+    def __expr(self):
+        """Parse an <expr> term."""
+        andexprs = [self.__andexpr()]
         while True:
-            kind, token = self._scanner.next()
+            kind, _ = self._scanner.next()
             if kind != "or":
                 self._scanner.rewind()
                 break
-            andexprs.append(self.andexpr())
+            andexprs.append(self.__andexpr())
         if len(andexprs) == 1:
             return andexprs[0]
         else:
             return self._snfactory.orNode(andexprs)
 
-    def andexpr(self):
-        notexprs = [self.notexpr()]
+    def __andexpr(self):
+        """Parse an <andexpr> term."""
+        notexprs = [self.__notexpr()]
         while True:
-            kind, token = self._scanner.next()
+            kind, _ = self._scanner.next()
             if kind != "and":
                 self._scanner.rewind()
                 break
-            notexprs.append(self.notexpr())
+            notexprs.append(self.__notexpr())
         if len(notexprs) == 1:
             return notexprs[0]
         else:
             return self._snfactory.andNode(notexprs)
 
-    def notexpr(self):
-        kind, token = self._scanner.next()
+    def __notexpr(self):
+        """Parse a <notexpr> term."""
+        kind, _ = self._scanner.next()
         if kind == "not":
-            return self._snfactory.notNode(self.term())
+            return self._snfactory.notNode(self.__term())
         else:
             self._scanner.rewind()
-            return self.term()
+            return self.__term()
 
-    def term(self):
+    def __term(self):
+        """Parse a <term> term."""
         kind, token = self._scanner.next()
         if kind == "bareword":
             return self._snfactory.categoryNode(token, recursive=True)
         elif kind == "exactly":
             kind, token = self._scanner.next()
             if kind != "bareword":
-                raise ParseError, \
-                      "expected category tag after \"exactly\", got: \"%s\"" % token
+                raise ParseError(
+                    "expected category tag after \"exactly\", got: \"%s\"" %
+                    token)
             return self._snfactory.categoryNode(token, recursive=False)
         elif kind == "album":
             return self._snfactory.albumNode(token[1:])
@@ -155,28 +235,42 @@ class Parser:
             if kind in ["ne", "eq", "le", "ge", "lt", "gt"]:
                 attroper = token
             else:
-                raise ParseError, \
-                      "expected comparison operator, got: \"%s\"" % token
+                raise ParseError(
+                    "expected comparison operator, got: \"%s\"" % token)
             kind, token = self._scanner.next()
             if kind in ("bareword", "string"):
                 value = token
             else:
-                raise ParseError, \
-                      "expected bareword or quoted string, got: \"%s\"" % token
+                raise ParseError(
+                    "expected bareword or quoted string, got: \"%s\"" % token)
             return self._snfactory.attrcondNode(attribute[1:], attroper, value)
         elif kind == "lparen":
-            expr = self.expr()
+            expr = self.__expr()
             kind, token = self._scanner.next()
             if kind != "rparen":
-                raise ParseError, \
-                      "expected right parenthesis or conjunction, got: \"%s\"" % token
+                raise ParseError(
+                    "expected right parenthesis or conjunction, got: \"%s\"" %
+                    token)
             return expr
-        raise ParseError, "expected expression, got: \"%s\"" % token
+        raise ParseError("expected expression, got: \"%s\"" % token)
 
 ######################################################################
 
-class AlbumSearchNode:
+class SearchNode:
+    """Abstract base class of search nodes."""
+
+    def __init__(self):
+        pass
+
+    def getQuery(self):
+        """Return the SQL expression for the node."""
+        raise UnimplementedError
+
+class AlbumSearchNode(SearchNode):
+    """A node representing the search for an album."""
+
     def __init__(self, shelf, album):
+        SearchNode.__init__(self)
         self._shelf = shelf
         self._album = album
 
@@ -186,6 +280,7 @@ class AlbumSearchNode:
     __str__ = __repr__
 
     def getQuery(self):
+        """Return the SQL expression for the node."""
         t = self._album.getType()
         if t == kofoto.shelf.AlbumType.Orphans:
             return (" select i.id"
@@ -207,8 +302,11 @@ class AlbumSearchNode:
         else:
             assert False, ("Unknown album type", t)
 
-class AndSearchNode:
+class AndSearchNode(SearchNode):
+    """A node representing the search for expr AND expr."""
+
     def __init__(self, subnodes):
+        SearchNode.__init__(self)
         self._subnodes = subnodes
 
     def __repr__(self):
@@ -218,6 +316,7 @@ class AndSearchNode:
     __str__ = __repr__
 
     def getQuery(self):
+        """Return the SQL expression for the node."""
         categories = []
         attrconds = []
         others = []
@@ -282,8 +381,10 @@ class AndSearchNode:
                     " and ".join(andclauses)))
 
 
-class AttributeConditionSearchNode:
+class AttributeConditionSearchNode(SearchNode):
+    """A node representing the search for an attribute of a certain value."""
     def __init__(self, attrname, operator, value):
+        SearchNode.__init__(self)
         self._name = attrname
         if operator == "=":
             self._operator = "glob"
@@ -305,15 +406,19 @@ class AttributeConditionSearchNode:
     __str__ = __repr__
 
     def getAttributeName(self):
+        """Get the name of the attribute."""
         return self._name
 
     def getAttributeValue(self):
+        """Get the value of the attribute."""
         return self._value
 
     def getOperator(self):
+        """Get the operator (=, !=, <, >, <= or >=)."""
         return self._operator
 
     def getQuery(self):
+        """Return the SQL expression for the node."""
         return (" select distinct object"
                 " from   attribute"
                 " where  name = '%s' and lcvalue %s '%s'" % (
@@ -321,8 +426,16 @@ class AttributeConditionSearchNode:
                     self._operator,
                     self._value.replace("'", "''")))
 
-class CategorySearchNode:
+class CategorySearchNode(SearchNode):
+    """A node representing the search for a category."""
     def __init__(self, ids):
+        SearchNode.__init__(self)
+        """Constructor.
+
+        Arguments:
+
+        ids -- A list of category IDs this node represents.
+        """
         self._ids = ids
 
     def __repr__(self):
@@ -331,16 +444,37 @@ class CategorySearchNode:
     __str__ = __repr__
 
     def getQuery(self):
+        """Return the SQL expression for the node."""
         return (" select distinct object"
                 " from   object_category"
                 " where  category in (%s)" % (
                     ",".join([str(x) for x in self._ids])))
 
     def getIds(self):
+        """Get the list of category IDs this node represents."""
         return self._ids
 
-class OrSearchNode:
+class NotSearchNode(SearchNode):
+    """A node representing the search for NOT expr."""
+    def __init__(self, subnode):
+        SearchNode.__init__(self)
+        self._subnode = subnode
+
+    def __repr__(self):
+        return "NotSearchNode(%r)" % self._subnode
+
+    __str__ = __repr__
+
+    def getQuery(self):
+        """Return the SQL expression for the node."""
+        return (" select id"
+                " from   object"
+                " where  id not in (%s)" % self._subnode.getQuery())
+
+class OrSearchNode(SearchNode):
+    """A node representing the search for expr OR expr."""
     def __init__(self, subnodes):
+        SearchNode.__init__(self)
         self._subnodes = subnodes
 
     def __repr__(self):
@@ -350,6 +484,7 @@ class OrSearchNode:
     __str__ = __repr__
 
     def getQuery(self):
+        """Return the SQL expression for the node."""
         catids = []
         attrconds = []
         others = []
@@ -385,21 +520,9 @@ class OrSearchNode:
             selects += [x.getQuery() for x in others]
         return " union ".join(selects)
 
-class NotSearchNode:
-    def __init__(self, subnode):
-        self._subnode = subnode
-
-    def __repr__(self):
-        return "NotSearchNode(%r)" % self._subnode
-
-    __str__ = __repr__
-
-    def getQuery(self):
-        return (" select id"
-                " from   object"
-                " where  id not in (%s)" % self._subnode.getQuery())
-
 class Scanner:
+    """A tokenizer of Kofoto search expressions."""
+
     _whiteRegexp = re.compile(r"\s*", re.UNICODE | re.MULTILINE)
     _tokenRegexps = [
         (re.compile(x, re.IGNORECASE | re.UNICODE | re.MULTILINE), y)
@@ -425,40 +548,57 @@ class Scanner:
             ]]
 
     def __init__(self, string):
+        """Constructor.
+
+        Arguments:
+
+        string -- The search expression to tokenize.
+        """
         self._string = string
         self._nexttoken = None
         self._pos = 0
-        self.dorewind = False
+        self._dorewind = False
+        self._currenttoken = None
 
     def __iter__(self):
         return self
 
     def next(self):
-        if self.dorewind:
-            self.dorewind = False
+        """Get the next token."""
+        if self._dorewind:
+            self._dorewind = False
         else:
-            self.currenttoken = self._next()
-        return self.currenttoken
+            self._currenttoken = self._next()
+        return self._currenttoken
 
     def rewind(self):
-        self.dorewind = True
+        """Rewind the parser one step.
+
+        Rewinding can only be performed after the next method has been called,
+        and only once.
+        """
+        assert self._currenttoken
+        assert not self._dorewind
+        self._dorewind = True
 
     def _eatWhite(self):
+        """Internal helper method."""
         nwhite = self._whiteRegexp.match(self._string).end()
         self._pos += nwhite
         self._string = self._string[nwhite:]
 
     def _next(self):
+        """Internal helper method."""
         self._eatWhite()
         for tokenregexp, kind in self._tokenRegexps:
             m = tokenregexp.match(self._string)
             if m:
                 token = m.group(0)
                 if kind == "untermstring":
-                    raise UnterminatedStringError, self._pos
+                    raise UnterminatedStringError(self._pos)
                 if kind == "string":
                     token = re.sub(r"\\(.)", r"\1", token[1:-1])
                 self._pos += m.end()
                 self._string = self._string[m.end():]
                 return kind, token
-        raise BadTokenError, self._pos
+        raise BadTokenError(self._pos)
index 1b76101..1b3c8ab 100644 (file)
@@ -27,7 +27,6 @@ __all__ = [
     "ShelfLockedError",
     "ShelfNotFoundError",
     "UndeletableAlbumError",
-    "UnimplementedError",
     "UnknownAlbumTypeError",
     "UnknownImageVersionTypeError",
     "UnsettableChildrenError",
@@ -45,10 +44,9 @@ __all__ = [
 import os
 import re
 import threading
-import time
 import sqlite as sql
 from sets import Set
-from kofoto.common import KofotoError
+from kofoto.common import KofotoError, UnimplementedError
 from kofoto.dag import DAG, LoopError
 from kofoto.cachedobject import CachedObject
 from kofoto.alternative import Alternative
@@ -367,11 +365,6 @@ class UndeletableAlbumError(KofotoError):
     pass
 
 
-class UnimplementedError(KofotoError):
-    """Unimplemented action."""
-    pass
-
-
 class UnknownAlbumTypeError(KofotoError):
     """The album type is unknown."""
     pass
@@ -408,6 +401,7 @@ def computeImageHash(filename):
 
 
 def verifyValidAlbumTag(tag):
+    """Verify that an album tag is valid."""
     if not isinstance(tag, (str, unicode)):
         raise BadAlbumTagError, tag
     try:
@@ -420,6 +414,7 @@ def verifyValidAlbumTag(tag):
 
 
 def verifyValidCategoryTag(tag):
+    """Verify that a category tag is valid."""
     if not isinstance(tag, (str, unicode)):
         raise BadCategoryTagError, tag
     try:
@@ -433,6 +428,10 @@ def verifyValidCategoryTag(tag):
 
 
 def makeValidTag(tag):
+    """Make a string a valid tag.
+
+    Returns the valid tag string.
+    """
     tag = tag.lstrip("@")
     tag = re.sub(r"\s", "", tag)
     if re.match("^\d+$", tag):
@@ -480,6 +479,8 @@ class Shelf:
             self.logfile = file("sql.log", "a")
         else:
             self.logfile = None
+        self.connection = None
+        self.categorydag = None
 
 
     def create(self):
@@ -517,7 +518,8 @@ class Shelf:
         Returns True if upgrade was successful, otherwise False.
         """
         assert not self.inTransaction
-        return kofoto.shelfupgrade.tryUpgrade(self.location, _SHELF_FORMAT_VERSION)
+        return kofoto.shelfupgrade.tryUpgrade(
+            self.location, _SHELF_FORMAT_VERSION)
 
 
     def begin(self):
@@ -621,6 +623,14 @@ class Shelf:
 
 
     def getStatistics(self):
+        """Get statistics about the metadata database.
+
+        The returned value is a mapping with the following keys:
+
+        nalbums        -- Number of albums.
+        nimages        -- Number of images.
+        nimageversions -- Number of image versions.
+        """
         assert self.inTransaction
         cursor = self.connection.cursor()
         cursor.execute(
@@ -1153,7 +1163,6 @@ class Shelf:
         row = cursor.fetchone()
         if not row:
             raise CategoryDoesNotExistError, catid
-        (tag,) = row
         cursor.execute(
             " delete from category_child"
             " where  parent = %s",
@@ -1265,6 +1274,7 @@ class Shelf:
     # Internal methods.
 
     def _createShelf(self):
+        """Helper method for Shelf.create."""
         cursor = self.connection.cursor()
         cursor.execute(schema)
         cursor.execute(
@@ -1299,6 +1309,7 @@ class Shelf:
 
 
     def _openShelf(self):
+        """Helper method for Shelf.open."""
         cursor = self.connection.cursor()
         try:
             cursor.execute(
@@ -1314,6 +1325,14 @@ class Shelf:
 
 
     def _albumFactory(self, albumid, tag, albumtype):
+        """Factory method for creating Album instances.
+
+        Arguments:
+
+        albumid   -- ID of the album.
+        tag       -- Tag of the album.
+        albumtype -- An instance of the AlbumType alternative.
+        """
         albumtypemap = {
             AlbumType.Orphans: OrphansAlbum,
             AlbumType.Plain: PlainAlbum,
@@ -1325,6 +1344,13 @@ class Shelf:
 
 
     def _imageFactory(self, imageid, primary_version_id):
+        """Factory method for creating Image instances.
+
+        Arguments:
+
+        imageid            -- ID of the image.
+        primary_version_id -- ID of the primary image version.
+        """
         image = Image(self, imageid, primary_version_id)
         self.objectcache[imageid] = image
         return image
@@ -1332,6 +1358,20 @@ class Shelf:
 
     def _imageVersionFactory(self, ivid, imageid, ivtype, ivhash,
                              location, mtime, width, height, comment):
+        """Factory method for creating ImageVersion instances.
+
+        Arguments:
+
+        ivid     -- ID of the image version.
+        imageid  -- ID of the image the image version belongs to.
+        ivtype   -- An instance of the ImageVersionType alternative.
+        ivhash   -- Hash of the image version file.
+        location -- Location of the image version file.
+        mtime    -- mtime of the image version file.
+        width    -- Width of the image version.
+        height   -- Height of the image version.
+        comment  -- Comment of the image version.
+        """
         imageversion = ImageVersion(
             self, ivid, imageid, ivtype, ivhash, location, mtime, width,
             height, comment)
@@ -1340,6 +1380,7 @@ class Shelf:
 
 
     def _deleteObjectFromParents(self, objid):
+        """Helper method that deletes an object from its parents."""
         cursor = self.connection.cursor()
         cursor.execute(
             " select distinct album.id, album.tag"
@@ -1347,7 +1388,7 @@ class Shelf:
             " where  member.object = %s and member.album = album.id",
             objid)
         parentinfolist = cursor.fetchall()
-        for parentid, parenttag in parentinfolist:
+        for parentid, _ in parentinfolist:
             cursor.execute(
                 " select position"
                 " from   member"
@@ -1373,38 +1414,45 @@ class Shelf:
 
 
     def _setModified(self):
+        """Set the modified flag."""
         self.modified = True
         for fn in self.modificationCallbacks:
             fn(True)
 
 
     def _unsetModified(self):
+        """Unset the modified flag."""
         self.modified = False
         for fn in self.modificationCallbacks:
             fn(False)
 
 
     def _getConnection(self):
+        """Get the database connection instance."""
         assert self.inTransaction
         return self.connection
 
 
     def _getOrphanAlbumsCache(self):
+        """Get the cache of the orphaned albums."""
         assert self.inTransaction
         return self.orphanAlbumsCache
 
 
     def _setOrphanAlbumsCache(self, albums):
+        """Set the cache of the orphaned albums."""
         assert self.inTransaction
         self.orphanAlbumsCache = albums
 
 
     def _getOrphanImagesCache(self):
+        """Get the cache of the orphaned images."""
         assert self.inTransaction
         return self.orphanImagesCache
 
 
     def _setOrphanImagesCache(self, images):
+        """Set the cache of the orphaned images."""
         assert self.inTransaction
         self.orphanImagesCache = images
 
@@ -1568,10 +1616,17 @@ class Category:
 
 
 class _Object:
+    """Abstract base class of Kofoto objects (albums and images)."""
+
     ##############################
     # Public methods.
 
+    def isAlbum(self):
+        """Return True if this an album, False if this is an image."""
+        raise UnimplementedError
+
     def getId(self):
+        """Get the ID of an object."""
         return self.objid
 
 
@@ -1626,12 +1681,12 @@ class _Object:
             " from   attribute"
             " where  object = %s",
             self.getId())
-        map = {}
+        amap = {}
         for key, value in cursor:
-            map[key] = value
-        self.attributes = map
+            amap[key] = value
+        self.attributes = amap
         self.allAttributesFetched = True
-        return map
+        return amap
 
 
     def getAttributeNames(self):
@@ -1746,6 +1801,7 @@ class _Object:
         self.allCategoriesFetched = False
 
     def _categoriesDirty(self):
+        """Set the categories dirty flag."""
         self.allCategoriesFetched = False
 
 
@@ -1762,12 +1818,16 @@ class _Object:
 
 
 class Album(_Object):
-    """Base class of Kofoto albums."""
+    """Abstract base class of Kofoto albums."""
 
     ##############################
     # Public methods.
 
     def getType(self):
+        """Get the album type.
+
+        The returned value is an instance of the AlbumType alternative.
+        """
         return self.albumtype
 
 
@@ -1782,6 +1842,7 @@ class Album(_Object):
 
 
     def setTag(self, newtag):
+        """Set the tag of the album."""
         verifyValidAlbumTag(newtag)
         cursor = self.shelf._getConnection().cursor()
         cursor.execute(
@@ -1795,10 +1856,18 @@ class Album(_Object):
 
 
     def getChildren(self):
+        """Get the album's children.
+
+        Returns an iterable returning Album/Image instances.
+        """
         raise UnimplementedError
 
 
     def getAlbumChildren(self):
+        """Get the album's album children.
+
+        Returns an iterable returning Album instances.
+        """
         raise UnimplementedError
 
 
@@ -1819,10 +1888,17 @@ class Album(_Object):
 
 
     def setChildren(self, children):
+        """Set the children of the album.
+
+        Arguments:
+
+        children -- A list of Album/Image instances.
+        """
         raise UnimplementedError
 
 
     def isAlbum(self):
+        """Return True if this an album, False if this is an image."""
         return True
 
 
@@ -1844,6 +1920,7 @@ class PlainAlbum(Album):
     # Public methods.
 
     def isMutable(self):
+        """Whether the album can be modified with setChildren."""
         return True
 
 
@@ -1894,7 +1971,12 @@ class PlainAlbum(Album):
 
 
     def setChildren(self, children):
-        """Set an album's children."""
+        """Set the album's children.
+
+        Arguments:
+
+        children -- A list of Album/Image instances.
+        """
         albumid = self.getId()
         cursor = self.shelf._getConnection().cursor()
         cursor.execute(
@@ -1949,6 +2031,7 @@ class Image(_Object):
     # Public methods.
 
     def isAlbum(self):
+        """Return True if this an album, False if this is an image."""
         return False
 
 
@@ -1990,6 +2073,7 @@ class Image(_Object):
 
 
     def _makeNewPrimaryVersion(self):
+        """Helper method to make a new primary image version of needed."""
         ivs = list(self.getImageVersions())
         if len(ivs) > 0:
             # The last version is probably the best.
@@ -2006,6 +2090,7 @@ class Image(_Object):
 
 
     def _setPrimaryVersion(self, imageversion):
+        """Helper method to set the primary image version."""
         self.primary_version_id = imageversion.getId()
 
 
@@ -2059,6 +2144,7 @@ class ImageVersion:
 
 
     def setImage(self, image):
+        """Associate the image version with an image."""
         oldimage = self.getImage()
         if image == oldimage:
             return
@@ -2082,6 +2168,12 @@ class ImageVersion:
 
 
     def setType(self, ivtype):
+        """Set the type of the image version.
+
+        Arguments:
+
+        ivtype -- An instance of the ImageVersionType alternative.
+        """
         self.type = ivtype
         cursor = self.shelf._getConnection().cursor()
         cursor.execute(
@@ -2094,6 +2186,7 @@ class ImageVersion:
 
 
     def setComment(self, comment):
+        """Set the comment of the image version."""
         self.comment = comment
         cursor = self.shelf._getConnection().cursor()
         cursor.execute(
@@ -2106,6 +2199,7 @@ class ImageVersion:
 
 
     def makePrimary(self):
+        """Make this image version the primary image version."""
         cursor = self.shelf._getConnection().cursor()
         cursor.execute(
             " update image"
@@ -2127,7 +2221,8 @@ class ImageVersion:
 
         Checksum, width, height and mtime are updated.
 
-        It is assumed that the image version location is still correct."""
+        It is assumed that the image version location is still correct.
+        """
         self.hash = computeImageHash(self.location)
         import Image as PILImage
         try:
@@ -2277,10 +2372,17 @@ class MagicAlbum(Album):
     # Public methods.
 
     def isMutable(self):
+        """Whether the album can be modified with setChildren."""
         return False
 
 
     def setChildren(self, children):
+        """Set the album's children.
+
+        Arguments:
+
+        children -- A list of Album/Image instances.
+        """
         raise UnsettableChildrenError, self.getTag()
 
 
@@ -2310,6 +2412,12 @@ class OrphansAlbum(MagicAlbum):
     # Internal methods.
 
     def _getChildren(self, includeimages):
+        """Helper method to get the albums children.
+
+        Arguments:
+
+        includeimages -- Whether images should be included.
+        """
         albums = self.shelf._getOrphanAlbumsCache()
         if albums != None:
             for album in albums:
@@ -2377,16 +2485,22 @@ class SearchAlbum(MagicAlbum):
     # Internal methods.
 
     def _getChildren(self, includeimages):
+        """Helper method to get the albums children.
+
+        Arguments:
+
+        includeimages -- Whether images should be included.
+        """
         query = self.getAttribute(u"query")
         if not query:
             return []
-        import kofoto.search
-        parser = kofoto.search.Parser(self.shelf)
+        from kofoto import search
+        parser = search.Parser(self.shelf)
         try:
             tree = parser.parse(query)
         except (AlbumDoesNotExistError,
                 CategoryDoesNotExistError,
-                kofoto.search.ParseError):
+                search.ParseError):
             return []
         objects = self.shelf.search(tree)
         if includeimages:
@@ -2395,6 +2509,7 @@ class SearchAlbum(MagicAlbum):
             objectlist = [x for x in objects if x.isAlbum()]
 
         def sortfn(x, y):
+            """Helper function."""
             a = cmp(x.getAttribute(u"captured"), y.getAttribute(u"captured"))
             if a == 0:
                 return cmp(x.getId(), y.getId())
@@ -2409,6 +2524,7 @@ class SearchAlbum(MagicAlbum):
 ### Internal helper functions and classes.
 
 def _albumTypeIdentifierToType(atid):
+    """Map an album type identifer string to an AlbumType alternative."""
     try:
         return {
             u"orphans": AlbumType.Orphans,
@@ -2420,6 +2536,7 @@ def _albumTypeIdentifierToType(atid):
 
 
 def _albumTypeToIdentifier(atype):
+    """Map an AlbumType alternative to an album type identifer string."""
     try:
         return {
             AlbumType.Orphans: u"orphans",
@@ -2431,6 +2548,7 @@ def _albumTypeToIdentifier(atype):
 
 
 def _createCategoryDAG(connection):
+    """Create the category DAG."""
     cursor = connection.cursor()
     cursor.execute(
         " select id"
@@ -2445,6 +2563,9 @@ def _createCategoryDAG(connection):
 
 
 def _imageVersionTypeIdentifierToType(ivtype):
+    """Map an image version type identifer string to an ImageVersionType
+    alternative.
+    """
     try:
         return {
             u"important": ImageVersionType.Important,
@@ -2456,6 +2577,9 @@ def _imageVersionTypeIdentifierToType(ivtype):
 
 
 def _imageVersionTypeToIdentifier(ivtype):
+    """Map an ImageVersionType alternative to an image version type identifer
+    string.
+    """
     try:
         return {
             ImageVersionType.Important: u"important",
@@ -2467,7 +2591,16 @@ def _imageVersionTypeToIdentifier(ivtype):
 
 
 class _UnicodeConnectionDecorator:
+    """A class that makes a database connection Unicode-aware."""
+
     def __init__(self, connection, encoding):
+        """Constructor.
+
+        Arguments:
+
+        connection -- The database connection to wrap.
+        encoding   -- The encoding (e.g. utf-8) to use.
+        """
         self.connection = connection
         self.encoding = encoding
 
@@ -2475,12 +2608,22 @@ class _UnicodeConnectionDecorator:
         return getattr(self.connection, attrname)
 
     def cursor(self):
+        """Return a _UnicodeConnectionDecorator."""
         return _UnicodeCursorDecorator(
             self.connection.cursor(), self.encoding)
 
 
 class _UnicodeCursorDecorator:
+    """A class that makes database cursor Unicode-aware."""
+
     def __init__(self, cursor, encoding):
+        """Constructor.
+
+        Arguments:
+
+        cursor   -- The database cursor to wrap.
+        encoding -- The encoding (e.g. utf-8) to use.
+        """
         self.cursor = cursor
         self.encoding = encoding
 
@@ -2499,6 +2642,7 @@ class _UnicodeCursorDecorator:
                 yield self._unicodifyRow(row)
 
     def _unicodifyRow(self, row):
+        """Helper method that decodes fields of a row into Unicode."""
         result = []
         for col in row:
             if isinstance(col, str):
@@ -2508,6 +2652,10 @@ class _UnicodeCursorDecorator:
         return result
 
     def _assertUnicode(self, obj):
+        """Check that all strings in an object are in Unicode.
+
+        Lists, tuples and maps are recursively checked.
+        """
         if isinstance(obj, str):
             raise AssertionError, ("non-Unicode string", obj)
         elif isinstance(obj, (list, tuple)):
@@ -2517,11 +2665,19 @@ class _UnicodeCursorDecorator:
             for val in obj.itervalues():
                 self._assertUnicode(val)
 
-    def execute(self, sql, *parameters):
+    def execute(self, statement, *parameters):
+        """Execute an SQL statement.
+
+        The SQL string must be Unicode.
+        """
         self._assertUnicode(parameters)
-        return self.cursor.execute(sql, *parameters)
+        return self.cursor.execute(statement, *parameters)
 
     def fetchone(self):
+        """Fetch a row from the result set.
+
+        The returned row contains Unicode strings.
+        """
         row = self.cursor.fetchone()
         if row:
             return self._unicodifyRow(row)
@@ -2529,4 +2685,8 @@ class _UnicodeCursorDecorator:
             return None
 
     def fetchall(self):
+        """Fetch all row of the result set.
+
+        The returned rows contain Unicode strings.
+        """
         return list(self)
index 8498837..269a234 100644 (file)
@@ -1,4 +1,6 @@
-__all__ = ["upgradeShelf"]
+"""Implementation of code that upgrades the shelf to a newer format."""
+
+__all__ = ["isUpgradable", "upgradeShelf"]
 
 import os
 import kofoto.shelf
@@ -6,6 +8,8 @@ import sqlite as sql
 import time
 
 def isUpgradable(location):
+    """Check whether a shelf is upgradable, i.e. not the latest version."""
+
     if not os.path.exists(location):
         raise kofoto.shelf.ShelfNotFoundError, location
     try:
@@ -25,7 +29,8 @@ def isUpgradable(location):
 def tryUpgrade(location, toVersion):
     """Upgrade the database format.
 
-    Returns True if upgrade was successful, otherwise False."""
+    Returns True if upgrade was successful, otherwise False.
+    """
 
     connection = sql.connect(location)
     cursor = connection.cursor()
@@ -66,7 +71,7 @@ def tryUpgrade(location, toVersion):
             " where  type in ('allalbums', 'allimages')")
         aids = [x[0] for x in cursor]
         if aids:
-            aids_str = ",".join(map(str, aids))
+            aids_str = ",".join([str(x) for x in aids])
             cursor.execute(
                 " delete from album"
                 " where  id in (%s)" % aids_str)
index 27e15db..91c43c1 100644 (file)
@@ -1,3 +1,5 @@
+# pylint: disable-msg=W0232
+
 """A simple struct-like class."""
 
 __all__ = ["StructClass"]
@@ -17,5 +19,6 @@ def makeStructClass(*attributes):
     """
 
     class Struct(object):
+        """A struct."""
         __slots__ = attributes
     return Struct
index 8b0dc15..c4bc865 100644 (file)
@@ -1,16 +1,26 @@
+"""Implementation of the Timer class."""
+
 import time
 
 class Timer:
+    """A class for measuring a time interval."""
+
     def __init__(self):
+        self.__time = None
         self.reset()
 
     def reset(self):
-        self.time = time.time()
+        """Reset the timer."""
+        self.__time = time.time()
 
     def get(self):
-        return time.time() - self.time
+        """Get the number of seconds since the timer was last reset (or
+        created)."""
+        return time.time() - self.__time
 
     def getAndReset(self):
-        t = time.time() - self.time
+        """Get the number of seconds since the timer was last reset (or
+        created) and then reset the timer."""
+        t = time.time() - self.__time
         self.reset()
         return t
index 8cc07c5..7da56e7 100644 (file)
@@ -1 +1,3 @@
+"""This module contains the Kofoto version."""
+
 version = "pre-0.4"