Major review of Unicode usage in all code. The new code should, with
authorJoel Rosdahl <joel@rosdahl.net>
Sun, 2 Oct 2005 19:27:07 +0000 (19:27 +0000)
committerJoel Rosdahl <joel@rosdahl.net>
Sun, 2 Oct 2005 19:27:07 +0000 (19:27 +0000)
one exception, hopefully be functionally equivalent to the old code,
but cleaner and more consistent. The exception is file path handling
in Windows, which now should work better for non-ASCII paths (ticket
#108).

21 files changed:
src/packages/kofoto/clientenvironment.py
src/packages/kofoto/clientutils.py
src/packages/kofoto/commandline/main.py
src/packages/kofoto/config.py
src/packages/kofoto/generate.py
src/packages/kofoto/gkofoto/albums.py
src/packages/kofoto/gkofoto/duplicateandopenimagedialog.py
src/packages/kofoto/gkofoto/generatehtmldialog.py
src/packages/kofoto/gkofoto/handleimagesdialog.py
src/packages/kofoto/gkofoto/imagepreloader.py
src/packages/kofoto/gkofoto/imageversionsdialog.py
src/packages/kofoto/gkofoto/imageversionslist.py
src/packages/kofoto/gkofoto/main.py
src/packages/kofoto/gkofoto/mainwindow.py
src/packages/kofoto/gkofoto/objectcollection.py
src/packages/kofoto/gkofoto/persistentstate.py
src/packages/kofoto/gkofoto/registerimageversionsdialog.py
src/packages/kofoto/output/woolly.py
src/packages/kofoto/outputengine.py
src/packages/kofoto/shelf.py
src/test/shelftests.py

index fa3553e..6be10f6 100644 (file)
@@ -9,12 +9,16 @@ __all__ = [
     "MissingShelfError",
 ]
 
+import codecs
 import locale
 import os
 import sys
+from kofoto.clientutils import expanduser
+from kofoto.common import UnimplementedError
 from kofoto.config import \
     BadConfigurationValueError, \
     Config, \
+    DEFAULT_CONFIGFILE_LOCATION, \
     MissingConfigurationKeyError, \
     MissingSectionHeaderError, \
     createConfigTemplate
@@ -71,32 +75,27 @@ class ClientEnvironment(object):
     A properly initialized ClientEnvironment instance has the
     following available attributes:
 
-    codeset            -- The codeset to use for encoding to and decoding from
-                          the current locale.
     config             -- A kofoto.config.Config instance.
     configFileLocation -- Location of the configuration file.
+    filesystemEncoding -- The codeset to use for encoding to and decoding from
+                          paths in the file system.
+    imageCache         -- A kofoto.imagecache.ImageCache instance.
+    localeEncoding     -- The codeset to use for encoding to and decoding from
+                          the current locale.
     shelf              -- A kofoto.shelf.Shelf instance.
     shelfLocation      -- Location of the shelf.
-    imageCache         -- A kofoto.imagecache.ImageCache instance.
     version            -- Kofoto version (a string).
     """
 
-    def __init__(self, localCodeset=None):
+    def __init__(self):
         """Initialize the client environment instance.
 
-        If localCodeset is None, the preferred character set encoding
-        is read from the environment and used as the local codeset.
-        Otherwise, localCodeset is used.
-
         Note that the setup method must be called to further
         initialize the instance.
         """
 
-        if localCodeset == None:
-            locale.setlocale(locale.LC_ALL, "")
-            self.__codeset = locale.getpreferredencoding()
-        else:
-            self.__codeset = localCodeset
+        self.__localeEncoding = locale.getpreferredencoding()
+        self.__filesystemEncoding = sys.getfilesystemencoding()
 
         # These are initiazlied in the setup method.
         self.__config = None
@@ -121,12 +120,7 @@ class ClientEnvironment(object):
         """
 
         if configFileLocation == None:
-            if sys.platform.startswith("win"):
-                self.__configFileLocation = os.path.expanduser(
-                    os.path.join("~", "KofotoData", "config.ini"))
-            else:
-                self.__configFileLocation = os.path.expanduser(
-                    os.path.join("~", ".kofoto", "config"))
+            self.__configFileLocation = expanduser(DEFAULT_CONFIGFILE_LOCATION)
         else:
             self.__configFileLocation = configFileLocation
 
@@ -134,17 +128,19 @@ class ClientEnvironment(object):
             confdir = os.path.dirname(self.configFileLocation)
             if confdir and not os.path.exists(confdir):
                 os.mkdir(confdir)
-                self._writeInfo("Created directory \"%s\".\n" % confdir)
+                self._writeInfo(u"Created directory \"%s\".\n" % confdir)
             if createMissingConfigFile:
-                createConfigTemplate(self.configFileLocation)
-                self._writeInfo("Created configuration file \"%s\".\n" %
+                f = codecs.open(
+                    self.configFileLocation, "w", self.localeEncoding)
+                createConfigTemplate(f)
+                self._writeInfo(u"Created configuration file \"%s\".\n" %
                                 self.configFileLocation)
             else:
                 raise MissingConfigFileError, \
-                    ("Missing configuration file: \"%s\"\n" %
+                    (u"Missing configuration file: \"%s\"\n" %
                          self.configFileLocation,
                      self.configFileLocation)
-        self.__config = Config(self.codeset)
+        self.__config = Config(self.localeEncoding)
 
         try:
             self.config.read(self.configFileLocation)
@@ -165,14 +161,12 @@ class ClientEnvironment(object):
                    self.configFileLocation)
 
         if shelfLocation == None:
-            self.__shelfLocation = \
-                os.path.expanduser(
-                    self.unicodeToLocalizedString(
-                        self.config.get("database", "location")))
+            location = self.config.get("database", "location")
+            self.__shelfLocation = expanduser(location)
         else:
             self.__shelfLocation = shelfLocation
 
-        self.__shelf = Shelf(self.shelfLocation, self.codeset)
+        self.__shelf = Shelf(self.shelfLocation)
 
         if not os.path.exists(self.shelfLocation):
             if createMissingShelf:
@@ -192,15 +186,18 @@ class ClientEnvironment(object):
                      self.shelfLocation)
 
         self.__imageCache = ImageCache(
-            os.path.expanduser(
-                self.unicodeToLocalizedString(
-                    self.config.get("image cache", "location"))),
+            expanduser(self.config.get("image cache", "location")),
             self.config.getboolean("image cache", "use_orientation_attribute"))
 
-    def getCodeset(self):
-        """Get codeset."""
-        return self.__codeset
-    codeset = property(getCodeset)
+    def getLocaleEncoding(self):
+        """Get encoding of the locale."""
+        return self.__localeEncoding
+    localeEncoding = property(getLocaleEncoding)
+
+    def getFilesystemEncoding(self):
+        """Get encoding of the filesystem."""
+        return self.__filesystemEncoding
+    filesystemEncoding = property(getFilesystemEncoding)
 
     def getConfig(self):
         """Get the Config instance."""
@@ -232,28 +229,9 @@ class ClientEnvironment(object):
         return kofotoVersion
     version = property(getVersion)
 
-    def unicodeToLocalizedString(self, unicodeString):
-        """Convert a Unicode string to a localized string.
-
-        If unicodeString is a Unicode string, convert it to a
-        localized string. Otherwise, unicodeString is returned without
-        any conversion.
-        """
-        if isinstance(unicodeString, unicode):
-            return unicodeString.encode(self.codeset)
-        else:
-            return unicodeString
-
-    def localizedStringToUnicode(self, localizedString):
-        """Convert a localized string to a Unicode string."""
-        return localizedString.decode(self.codeset)
-
     def _writeInfo(self, infoString):
         """Write an informational string to a suitable place.
 
-        This is the default implementation: write to standard output. 
-        Subclasses should override this method if they want to handle
-        the output themselves.
+        Should be overridden by subclasses.
         """
-        sys.stdout.write(self.unicodeToLocalizedString(infoString))
-        sys.stdout.flush()
+        raise UnimplementedError
index 49b443a..6a7d21e 100644 (file)
@@ -5,13 +5,17 @@
 
 __all__ = [
     "DIRECTORIES_TO_IGNORE",
+    "expanduser",
+    "get_file_encoding",
     "walk_files",
     ]
 
 ######################################################################
 ### Libraries.
 
+import locale
 import os
+import sys
 
 ######################################################################
 ### Implementation.
@@ -27,6 +31,23 @@ DIRECTORIES_TO_IGNORE = [
     "{arch}",
     ]
 
+def expanduser(path):
+    """Expand ~ and ~user constructions.
+
+    If user or $HOME is unknown, do nothing.
+
+    Unlike os.path.expanduser in Python 2.4.1, this function takes and
+    returns Unicode strings correctly.
+    """
+    fs_encoding = sys.getfilesystemencoding()
+    return os.path.expanduser(path.encode(fs_encoding)).decode(fs_encoding)
+
+def get_file_encoding(f):
+    if hasattr(f, "encoding") and f.encoding:
+        return f.encoding
+    else:
+        return locale.getpreferredencoding()
+
 def walk_files(paths, directories_to_ignore=None):
     """Traverse paths and return filename while ignoring some directories.
 
index 5431767..87ae9b6 100755 (executable)
@@ -5,6 +5,7 @@
 
 __all__ = ["main"]
 
+import codecs
 import getopt
 import os
 from sets import Set
@@ -12,7 +13,11 @@ import sys
 import time
 
 from kofoto.clientenvironment import ClientEnvironment, ClientEnvironmentError
-from kofoto.clientutils import DIRECTORIES_TO_IGNORE, walk_files
+from kofoto.clientutils import \
+    DIRECTORIES_TO_IGNORE, \
+    expanduser, \
+    get_file_encoding, \
+    walk_files
 from kofoto.config import DEFAULT_CONFIGFILE_LOCATION
 from kofoto.search import \
     BadTokenError, ParseError, Parser, UnterminatedStringError
@@ -492,8 +497,10 @@ class CommandlineClientEnvironment(ClientEnvironment):
         self.type = None
         self.verbose = False
 
-    to_l = ClientEnvironment.unicodeToLocalizedString
-    to_u = ClientEnvironment.localizedStringToUnicode
+
+    def _writeInfo(self, infoString):
+        sys.stdout.write(infoString)
+        sys.stdout.flush()
 
 ######################################################################
 ### Commands.
@@ -619,7 +626,7 @@ def cmdFindMissingImageVersions(env, args):
     badchecksums = []
     missingfiles = []
     for iv in env.shelf.getAllImageVersions():
-        location = env.to_l(iv.getLocation())
+        location = iv.getLocation()
         if env.verbose:
             env.out("Checking %s ...\n" % location)
         try:
@@ -657,7 +664,7 @@ def cmdGenerate(env, args):
         generator = kofoto.generate.Generator(otype, env)
         generator.generate(root, subalbums, dest, env.gencharenc)
     except kofoto.generate.OutputTypeError, x:
-        env.errexit("No such output module: %s\n" % env.to_l(x))
+        env.errexit("No such output module: %s\n" % x)
 
 
 def cmdGetAttribute(env, args):
@@ -667,7 +674,7 @@ def cmdGetAttribute(env, args):
     obj = sloppyGetObject(env, args[1])
     value = obj.getAttribute(args[0])
     if value:
-        env.out(env.to_l(value) + "\n")
+        env.out(value + "\n")
 
 
 def cmdGetAttributes(env, args):
@@ -676,9 +683,7 @@ def cmdGetAttributes(env, args):
         raise ArgumentError
     obj = sloppyGetObject(env, args[0])
     for name in obj.getAttributeNames():
-        env.out("%s: %s\n" % (
-            env.to_l(name),
-            env.to_l(obj.getAttribute(name))))
+        env.out("%s: %s\n" % (name, obj.getAttribute(name)))
 
 
 def cmdGetCategories(env, args):
@@ -688,8 +693,8 @@ def cmdGetCategories(env, args):
     obj = sloppyGetObject(env, args[0])
     for category in obj.getCategories():
         env.out("%s (%s) <%s>\n" % (
-            env.to_l(category.getDescription()),
-            env.to_l(category.getTag()),
+            category.getDescription(),
+            category.getTag(),
             category.getId()))
 
 
@@ -703,13 +708,13 @@ def cmdGetImageVersions(env, args):
             env.out("%s\n" % iv.getId())
         elif env.verbose:
             env.out("%s\n  %s%s\n" % (
-                env.to_l(iv.getLocation()),
+                iv.getLocation(),
                 iv.isPrimary() and "Primary, " or "",
                 iv.getType()))
             if iv.getComment():
-                env.out("  %s\n" % env.to_l(iv.getComment()))
+                env.out("  %s\n" % iv.getComment())
         else:
-            env.out("%s\n" % env.to_l(iv.getLocation()))
+            env.out("%s\n" % iv.getLocation())
 
 
 def cmdInspectPath(env, args):
@@ -722,22 +727,22 @@ def cmdInspectPath(env, args):
             imageversion = env.shelf.getImageVersionByHash(
                 computeImageHash(filepath))
             if imageversion.getLocation() == os.path.realpath(filepath):
-                env.out("[Registered]   %s\n" % env.to_l(filepath))
+                env.out("[Registered]   %s\n" % filepath)
             else:
-                env.out("[Moved]        %s\n" % env.to_l(filepath))
+                env.out("[Moved]        %s\n" % filepath)
         except ImageVersionDoesNotExistError:
             try:
                 imageversion = env.shelf.getImageVersionByLocation(filepath)
-                env.out("[Modified]     %s\n" % env.to_l(filepath))
+                env.out("[Modified]     %s\n" % filepath)
             except MultipleImageVersionsAtOneLocationError:
-                env.out("[Multiple]     %s\n" % env.to_l(filepath))
+                env.out("[Multiple]     %s\n" % filepath)
             except ImageVersionDoesNotExistError:
                 try:
-                    PILImage.open(env.to_l(filepath))
-                    env.out("[Unregistered] %s\n" % env.to_l(filepath))
+                    PILImage.open(filepath)
+                    env.out("[Unregistered] %s\n" % filepath)
 #                except IOError:
                 except: # Work-around for buggy PIL.
-                    env.out("[Non-image]    %s\n" % env.to_l(filepath))
+                    env.out("[Non-image]    %s\n" % filepath)
 
 
 def cmdMakePrimary(env, args):
@@ -770,10 +775,10 @@ def printAlbumsHelper(env, obj, position, level, visited):
         tag = obj.getTag()
         env.out(albtmpl % {
             "indent": level * indentspaces,
-            "name": env.to_l(tag),
+            "name": tag,
             "position": position,
             "id": obj.getId(),
-            "type": env.to_l(obj.getType()),
+            "type": obj.getType(),
             })
         if tag in visited:
             env.out("%s[...]\n" % ((level + 1) * indentspaces))
@@ -801,7 +806,7 @@ def printAlbumsHelper(env, obj, position, level, visited):
             env.out(imgvertmpl % {
                 "indent": (level + 1) * indentspaces,
                 "id": iv.getId(),
-                "location": env.to_l(iv.getLocation()),
+                "location": iv.getLocation(),
                 "primary": iv.isPrimary() and "Primary " or "",
                 "type": iv.getType(),
                 })
@@ -811,8 +816,8 @@ def printAlbumsHelper(env, obj, position, level, visited):
         for name in names:
             env.out(attrtmpl % {
                 "indent": (level + 1) * indentspaces,
-                "key": env.to_l(name),
-                "value": env.to_l(obj.getAttribute(name)),
+                "key": name,
+                "value": obj.getAttribute(name),
                 })
 
 
@@ -829,8 +834,8 @@ def printCategoriesHelper(env, category, level):
     indentspaces = PRINT_ALBUMS_INDENT * " "
     env.out("%s%s (%s) <%s>\n" % (
         level * indentspaces,
-        env.to_l(category.getDescription()),
-        env.to_l(category.getTag()),
+        category.getDescription(),
+        category.getTag(),
         category.getId()))
     for child in category.getChildren():
         printCategoriesHelper(env, child, level + 1)
@@ -854,7 +859,7 @@ def cmdRegister(env, args):
         env,
         destalbum,
         registrationTimeString,
-        [env.to_l(x) for x in args[1:]])
+        args[1:])
 
 
 def registerHelper(env, destalbum, registrationTimeString, paths):
@@ -873,7 +878,7 @@ def registerHelper(env, destalbum, registrationTimeString, paths):
             tag = makeValidTag(tag)
             while True:
                 try:
-                    album = env.shelf.createAlbum(env.to_u(tag))
+                    album = env.shelf.createAlbum(tag)
                     break
                 except AlbumExistsError:
                     tag += "_"
@@ -890,7 +895,7 @@ def registerHelper(env, destalbum, registrationTimeString, paths):
             try:
                 image = env.shelf.createImage()
                 env.shelf.createImageVersion(
-                    image, env.to_u(path), ImageVersionType.Original)
+                    image, path, ImageVersionType.Original)
                 image.setAttribute(u"registered", registrationTimeString)
                 newchildren.append(image)
                 if env.verbose:
@@ -914,7 +919,7 @@ def cmdRemove(env, args):
         try:
             positions.append(int(pos))
         except ValueError:
-            env.errexit("Bad position: %s.\n" % env.to_l(pos))
+            env.errexit("Bad position: %s.\n" % pos)
     positions.sort()
     positions.reverse()
     children = list(album.getChildren())
@@ -973,7 +978,7 @@ def cmdSearch(env, args):
                 (env.includeOriginal and t == ImageVersionType.Original) or
                 (env.includeOther and t == ImageVersionType.Other) or
                 (env.includePrimary and iv.isPrimary())):
-                output.append(env.to_l(iv.getLocation()))
+                output.append(iv.getLocation())
         output.sort()
     if output:
         env.out("%s\n" % "\n".join(output))
@@ -1048,18 +1053,18 @@ def cmdUpdateContents(env, args):
             oldhash = imageversion.getHash()
             imageversion.contentChanged()
             if imageversion.getHash() != oldhash:
-                env.out("New checksum: %s\n" % env.to_l(filepath))
+                env.out("New checksum: %s\n" % filepath)
             else:
                 if env.verbose:
                     env.out(
-                        "Same checksum as before: %s\n" % env.to_l(filepath))
+                        "Same checksum as before: %s\n" % filepath)
         except ImageVersionDoesNotExistError:
             if env.verbose:
-                env.out("Unregistered image/file: %s\n" % env.to_l(filepath))
+                env.out("Unregistered image/file: %s\n" % filepath)
         except MultipleImageVersionsAtOneLocationError:
             env.errexit(
                 "Multiple known image versions at this location: %s\n" % (
-                env.to_l(filepath)))
+                filepath))
 
 
 def cmdUpdateLocations(env, args):
@@ -1074,20 +1079,20 @@ def cmdUpdateLocations(env, args):
             if oldlocation != os.path.realpath(filepath):
                 imageversion.locationChanged(filepath)
                 env.out("New location: %s --> %s\n" % (
-                    env.to_l(oldlocation),
-                    env.to_l(imageversion.getLocation())))
+                    oldlocation,
+                    imageversion.getLocation()))
             else:
                 if env.verbose:
                     env.out(
-                        "Same location as before: %s\n" % env.to_l(filepath))
+                        "Same location as before: %s\n" % filepath)
         except IOError, x:
             if env.verbose:
                 env.out("Failed to read: %s (%s)\n" % (
-                    env.to_l(filepath),
+                    filepath,
                     x))
         except ImageVersionDoesNotExistError:
             if env.verbose:
-                env.out("Unregistered image/file: %s\n" % env.to_l(filepath))
+                env.out("Unregistered image/file: %s\n" % filepath)
 
 
 commandTable = {
@@ -1142,7 +1147,12 @@ def main(argv):
     """
     env = CommandlineClientEnvironment()
 
-    argv = [env.to_u(x) for x in argv]
+    argv = [x.decode(env.localeEncoding) for x in argv]
+
+    sys.stdin = codecs.getreader(get_file_encoding(sys.stdin))(sys.stdin)
+    sys.stdout = codecs.getwriter(get_file_encoding(sys.stdout))(sys.stdout)
+    sys.stderr = codecs.getwriter(get_file_encoding(sys.stderr))(sys.stderr)
+
     try:
         optlist, args = getopt.gnu_getopt(
             argv[1:],
@@ -1174,9 +1184,9 @@ def main(argv):
 
     for opt, optarg in optlist:
         if opt == "--configfile":
-            configFileLocation = os.path.expanduser(env.to_l(optarg))
+            configFileLocation = expanduser(optarg)
         elif opt == "--database":
-            shelfLocation = env.to_l(optarg)
+            shelfLocation = optarg
         elif opt == "--gencharenc":
             genCharEnc = str(optarg)
         elif opt in ("-h", "--help"):
@@ -1209,8 +1219,7 @@ def main(argv):
                 try:
                     env.position = int(optarg)
                 except ValueError:
-                    printErrorAndExit(
-                        "Invalid position: \"%s\"\n" % env.to_l(optarg))
+                    printErrorAndExit("Invalid position: \"%s\"\n" % optarg)
         elif opt in ("-t", "--type"):
             env.type = optarg
         elif opt in ("-v", "--verbose"):
@@ -1235,7 +1244,7 @@ def main(argv):
     if not commandTable.has_key(args[0]):
         printErrorAndExit(
             "Unknown command \"%s\". See \"kofoto --help\" for help.\n" % (
-            env.to_l(args[0])))
+            args[0]))
 
     try:
         if env.shelf.isUpgradable():
@@ -1288,54 +1297,52 @@ def main(argv):
         printErrorAndExit(
             "Bad arguments to command. See \"kofoto --help\" for help.\n")
     except UndeletableAlbumError, x:
-        printError("Undeletable album: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Undeletable album: \"%s\".\n" % x.args[0])
     except BadAlbumTagError, x:
-        printError("Bad album tag: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Bad album tag: \"%s\".\n" % x.args[0])
     except AlbumExistsError, x:
-        printError("Album already exists: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Album already exists: \"%s\".\n" % x.args[0])
     except ImageDoesNotExistError, x:
-        printError("Image does not exist: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Image does not exist: \"%s\".\n" % x.args[0])
     except AlbumDoesNotExistError, x:
-        printError("Album does not exist: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Album does not exist: \"%s\".\n" % x.args[0])
     except ObjectDoesNotExistError, x:
-        printError("Object does not exist: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Object does not exist: \"%s\".\n" % x.args[0])
     except UnknownAlbumTypeError, x:
-        printError("Unknown album type: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Unknown album type: \"%s\".\n" % x.args[0])
     except UnsettableChildrenError, x:
         printError(
             "Cannot modify children of \"%s\" (children are created"
-            " virtually).\n" % (
-            env.to_l(x.args[0])))
+            " virtually).\n" % x.args[0])
     except CategoryExistsError, x:
-        printError("Category already exists: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Category already exists: \"%s\".\n" % x.args[0])
     except CategoryDoesNotExistError, x:
-        printError("Category does not exist: \"%s\".\n" % env.to_l(x.args[0]))
+        printError("Category does not exist: \"%s\".\n" % x.args[0])
     except BadCategoryTagError, x:
-        printError("Bad category tag: %s.\n" % env.to_l(x.args[0]))
+        printError("Bad category tag: %s.\n" % x.args[0])
     except CategoryPresentError, x:
         printError("Object %s is already associated with category %s.\n" % (
-            env.to_l(x.args[0]), env.to_l(x.args[1])))
+            x.args[0], x.args[1]))
     except CategoriesAlreadyConnectedError, x:
         printError("Categories %s and %s are already connected.\n" % (
-            env.to_l(x.args[0]), env.to_l(x.args[1])))
+            x.args[0], x.args[1]))
     except CategoryLoopError, x:
         printError(
             "Connecting %s to %s would make a loop in the categories.\n" % (
-            env.to_l(x.args[0]), env.to_l(x.args[1])))
+            x.args[0], x.args[1]))
     except ParseError, x:
         printError(
-            "While parsing search expression: %s.\n" % env.to_l(x.args[0]))
+            "While parsing search expression: %s.\n" % x.args[0])
     except UnterminatedStringError, x:
         printError(
             "While scanning search expression: unterminated string starting at"
-            " character %d.\n" % env.to_l(x.args[0]))
+            " character %d.\n" % x.args[0])
     except BadTokenError, x:
         printError(
             "While scanning search expression: bad token starting at character"
-            " %d.\n" % env.to_l(x.args[0]))
+            " %d.\n" % x.args[0])
     except UnknownImageVersionTypeError, x:
-        printError("Unknown image version type: \"%s\".\n" % (
-            env.to_l(x.args[0])))
+        printError("Unknown image version type: \"%s\".\n" % x.args[0])
     except KeyboardInterrupt:
         printOutput("Interrupted.\n")
     except IOError, e:
index d9d8fc5..63b2a2b 100644 (file)
@@ -1,5 +1,3 @@
-# pylint: disable-msg=C0301, W0221
-
 """Configuration module for Kofoto."""
 
 __all__ = [
@@ -17,6 +15,7 @@ __all__ = [
 from ConfigParser import ConfigParser
 from ConfigParser import MissingSectionHeaderError as \
     ConfigParserMissingSectionHeaderError
+import codecs
 import os
 import re
 import sys
@@ -24,18 +23,18 @@ from kofoto.common import KofotoError
 
 if sys.platform.startswith("win"):
     DEFAULT_CONFIGFILE_LOCATION = os.path.join(
-        "~", "KofotoData", "config.ini")
+        u"~", u"KofotoData", u"config.ini")
     DEFAULT_SHELF_LOCATION = os.path.join(
-        "~", "KofotoData", "metadata.db")
+        u"~", u"KofotoData", u"metadata.db")
     DEFAULT_IMAGECACHE_LOCATION = os.path.join(
-        "~", "KofotoData", "ImageCache")
+        u"~", u"KofotoData", u"ImageCache")
 else:
     DEFAULT_CONFIGFILE_LOCATION = os.path.join(
-        "~", ".kofoto", "config")
+        u"~", u".kofoto", u"config")
     DEFAULT_SHELF_LOCATION = os.path.join(
-        "~", ".kofoto", "metadata.db")
+        u"~", u".kofoto", u"metadata.db")
     DEFAULT_IMAGECACHE_LOCATION = os.path.join(
-        "~", ".kofoto", "imagecache")
+        u"~", u".kofoto", u"imagecache")
 
 class ConfigError(KofotoError):
     """Configuration error."""
@@ -61,26 +60,26 @@ class Config(ConfigParser):
 
         Arguments:
 
-        encoding -- The encoding to use when translating between Unicode and
-                    byte strings.
+        encoding -- The encoding of the configuration files.
         """
         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)
+        if isinstance(filenames, basestring):
+            filenames = [filenames]
+        for filename in filenames:
+            try:
+                fp = codecs.open(filename, "r", self.encoding)
+                self.readfp(fp, filename)
+            except ConfigParserMissingSectionHeaderError:
+                raise MissingSectionHeaderError
+            except IOError:
+                # From ConfigParser.read documentation: "If a file
+                # named in filenames cannot be opened, that file will
+                # be ignored."
+                pass
 
     def getcoordlist(self, section, option):
         """Get a coordinate list.
@@ -131,11 +130,16 @@ class Config(ConfigParser):
             "album generation", "other_image_size_limits", None)
 
 
-def createConfigTemplate(filename):
-    """Write a Kofoto configuration template to a file."""
+def createConfigTemplate(fileobject):
+    """Write a Kofoto configuration template to a file.
+
+    Arguments:
+
+    fileobject -- File object to write the template to.
+    """
 
-    file(filename, "w").write(
-        """### Configuration file for Kofoto.
+    fileobject.write(
+        u'''### Configuration file for Kofoto.
 
 ######################################################################
 ## General configuration
@@ -207,4 +211,4 @@ enable_auto_descriptions = no
 # image has several matching subcategories, they are delimited with
 # commas.
 auto_descriptions_template = <depicted> (<location>)
-""" % (DEFAULT_SHELF_LOCATION, DEFAULT_IMAGECACHE_LOCATION))
+''' % (DEFAULT_SHELF_LOCATION, DEFAULT_IMAGECACHE_LOCATION))
index 9dae774..49caaac 100644 (file)
@@ -24,8 +24,7 @@ class Generator:
         self.env = env
         try:
             outputmodule = getattr(
-                __import__("kofoto.output.%s" %
-                           outputtype.encode(env.codeset)).output,
+                __import__("kofoto.output.%s" % outputtype).output,
                 outputtype)
         except ImportError:
             raise OutputTypeError, outputtype
index f2853ab..d1bbfd0 100644 (file)
@@ -76,9 +76,10 @@ class Albums:
         destroyMenuItem = self.__menuGroup[self.__destroyAlbumLabel]
         editMenuItem = self.__menuGroup[self.__editAlbumLabel]
         if iterator:
-            albumTag = albumModel.get_value(iterator, self.__COLUMN_TAG)
+            albumTag = albumModel.get_value(
+                iterator, self.__COLUMN_TAG).decode("utf-8")
             if load:
-                self.__mainWindow.loadQuery("/" + albumTag.decode("utf-8"))
+                self.__mainWindow.loadQuery(u"/" + albumTag)
             album = env.shelf.getAlbum(
                 albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID))
             createMenuItem.set_sensitive(album.isMutable())
index 88200e7..ab4ff66 100644 (file)
@@ -33,7 +33,7 @@ class DuplicateAndOpenImageDialog:
 
     def _onOk(self, *unused):
         duplicateLocation = self._fileEntry.get_text()
-        if os.path.exists(duplicateLocation.encode(env.codeset)):
+        if os.path.exists(duplicateLocation):
             dialog = gtk.MessageDialog(
                 self._dialog,
                 gtk.DIALOG_MODAL,
@@ -44,10 +44,10 @@ class DuplicateAndOpenImageDialog:
             dialog.destroy()
         else:
             shutil.copyfile(
-                self._imageversion.getLocation().encode(env.codeset),
-                duplicateLocation.encode(env.codeset))
+                self._imageversion.getLocation(),
+                duplicateLocation)
             command = env.openCommand % {"locations": duplicateLocation}
-            result = os.system(command.encode(env.codeset) + " &")
+            result = os.system(command.encode(env.localeEncoding) + " &")
             if result != 0:
                 dialog = gtk.MessageDialog(
                     self._dialog,
index 99ddf50..f1fad3f 100644 (file)
@@ -61,7 +61,7 @@ class GenerateHTMLDialog:
                 r"Creating album (\S+) \((\d+) of (\d+)\)",
                 string)
             if m:
-                progressBar.set_text(m.group(1).decode(env.codeset))
+                progressBar.set_text(m.group(1).decode(env.localeEncoding))
                 progressBar.set_fraction(
                     (int(m.group(2)) - 1) / float(m.group(3)))
                 while gtk.events_pending():
index efa56a1..de4c394 100644 (file)
@@ -50,11 +50,11 @@ class HandleImagesDialog(gtk.FileChooserDialog):
         investigatedFiles = 0
         modifiedImages = []
         movedImages = []
-        for filepath in walk_files([self.get_filename()]):
-            try:
-                filepath = filepath.decode(env.codeset)
-            except UnicodeDecodeError:
-                filepath = filepath.decode("latin1")
+        try:
+            directory = self.get_filename().decode("utf-8")
+        except UnicodeDecodeError:
+            directory = self.get_filename().decode("latin1")
+        for filepath in walk_files([directory]):
             try:
                 imageversion = env.shelf.getImageVersionByHash(
                     computeImageHash(filepath))
index b5fd72a..34b5f9f 100644 (file)
@@ -16,13 +16,13 @@ class _MyPixbufLoader(gtk.gdk.PixbufLoader):
             self.__closed = True
 
 class _PreloadState:
-    def __init__(self, filename, fileSystemCodeset):
+    def __init__(self, filename):
         self.fullsizePixbuf = None
         self.pixbufLoader = _MyPixbufLoader()
         self.loadFinished = False # Whether loading of fullsizePixbuf is ready.
         self.scaledPixbuf = None
         try:
-            self.fp = open(filename.encode(fileSystemCodeset), "rb")
+            self.fp = open(filename, "rb")
         except (IOError, OSError):
             self.loadFinished = True
             self.pixbufLoader = None
@@ -32,8 +32,7 @@ class _PreloadState:
             self.pixbufLoader.close()
 
 class ImagePreloader(object):
-    def __init__(self, fileSystemCodeset, debugPrintFunction=None):
-        self._fileSystemCodeset = fileSystemCodeset
+    def __init__(self, debugPrintFunction=None):
         if debugPrintFunction:
             self._debugPrint = debugPrintFunction
         else:
@@ -89,8 +88,7 @@ class ImagePreloader(object):
         The pixbuf may be None if the image was unloadable.
         """
         if not self.__preloadStates.has_key(filename):
-            self.__preloadStates[filename] = _PreloadState(
-                filename, self._fileSystemCodeset)
+            self.__preloadStates[filename] = _PreloadState(filename)
         ps = self.__preloadStates[filename]
         if not ps.loadFinished:
             try:
@@ -136,8 +134,7 @@ class ImagePreloader(object):
         # Preload the new images.
         for filename in filenames:
             if not self.__preloadStates.has_key(filename):
-                self.__preloadStates[filename] = _PreloadState(
-                    filename, self._fileSystemCodeset)
+                self.__preloadStates[filename] = _PreloadState(filename)
             ps = self.__preloadStates[filename]
             try:
                 self._debugPrint("Preloading %s" % filename)
index cfbaaa5..4476a53 100644 (file)
@@ -65,8 +65,9 @@ class ImageVersionsDialog:
     def _onOk(self, *unused):
         for data in self._versionDataList:
             tb = data.commentTextBuffer
-            comment = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
-            data.imageVersion.setComment(comment.decode("utf-8"))
+            comment = tb.get_text(
+                tb.get_start_iter(), tb.get_end_iter()).decode("utf-8")
+            data.imageVersion.setComment(comment)
             if data.primaryButton.get_active():
                 data.imageVersion.makePrimary()
             if data.importantButton.get_active():
@@ -159,8 +160,8 @@ class ImageVersionsDialog:
         image = gtk.Image()
         try:
             thumbnailLocation, _, _ = env.imageCache.get(
-                imageVersion.getLocation().encode(env.codeset), 128, 128)
-            image.set_from_file(thumbnailLocation.decode(env.codeset))
+                imageVersion.getLocation(), 128, 128)
+            image.set_from_file(thumbnailLocation)
         except OSError:
             image.set_from_pixbuf(env.unknownImageIconPixbuf)
         table.attach(
index 13fe9b0..fb3b88a 100644 (file)
@@ -74,7 +74,7 @@ class ImageVersionsList(gtk.ScrolledWindow):
             thumbnail = gtk.Image()
             try:
                 thumbnailLocation = env.imageCache.get(iv, 128, 128)[0]
-                thumbnail.set_from_file(thumbnailLocation.encode(env.codeset))
+                thumbnail.set_from_file(thumbnailLocation)
             except OSError:
                 thumbnail.set_from_pixbuf(env.unknownImageIconPixbuf)
             alignment = gtk.Alignment(0.5, 0.5, 0.5, 0.5)
@@ -265,7 +265,7 @@ class ImageVersionsList(gtk.ScrolledWindow):
             self.__imageWidgetToImageVersion[x].getLocation()
             for x in self.__selectedImageWidgets]
         command = env.openCommand % {"locations": " ".join(locations)}
-        result = os.system(command.encode(env.codeset) + " &")
+        result = os.system(command.encode(env.localeEncoding) + " &")
         if result != 0:
             dialog = gtk.MessageDialog(
                 type=gtk.MESSAGE_ERROR,
@@ -314,8 +314,7 @@ class ImageVersionsList(gtk.ScrolledWindow):
                 imageVersion = self.__imageWidgetToImageVersion[widget]
                 if deleteFiles:
                     try:
-                        os.remove(
-                            imageVersion.getLocation().encode(env.codeset))
+                        os.remove(imageVersion.getLocation())
                         # TODO: Delete from image cache too?
                     except OSError:
                         pass
@@ -348,7 +347,7 @@ class ImageVersionsList(gtk.ScrolledWindow):
                 # Can't happen.
                 assert True
             command = rotateCommand % {"location": imageVersion.getLocation()}
-            result = os.system(command.encode(env.codeset))
+            result = os.system(command.encode(env.localeEncoding))
             if result == 0:
                 imageVersion.contentChanged()
             else:
index 513e62c..066b105 100644 (file)
@@ -1,5 +1,6 @@
 import os
 import sys
+from kofoto.clientutils import expanduser
 from kofoto.config import DEFAULT_CONFIGFILE_LOCATION
 from kofoto.gkofoto.environment import env
 from kofoto.gkofoto.controller import Controller
@@ -7,7 +8,7 @@ from optparse import OptionParser
 
 def setupWindowsEnvironment():
     # Allow (default) datafile location to be determined under Windows 98.
-    if os.path.expanduser("~") == "~":
+    if expanduser("~") == "~":
         # Probably running under Windows 98 or similar OS where the
         # environment variables HOMEPATH and HOMEDRIVE (and HOME) are not
         # set. We have to fake it instead.
index 2f8bfc7..3ab6ea8 100644 (file)
@@ -25,8 +25,8 @@ class MainWindow(gtk.Window):
         self._toggleLock = False
         self.__currentObjectCollection = None
         self._currentView = None
-        self.__persistentState = PersistentState()
-        self.__imagePreloader = ImagePreloader(env.codeset, env.debug)
+        self.__persistentState = PersistentState(env)
+        self.__imagePreloader = ImagePreloader(env.debug)
         self.__sourceEntry = env.widgets["sourceEntry"]
         self.__filterEntry = env.widgets["filterEntry"]
         self.__filterEntry.set_text(self.__persistentState.filterText)
index 2e0fa9e..b921e84 100644 (file)
@@ -220,7 +220,7 @@ class ObjectCollection(object):
                 if deleteFiles and not obj.isAlbum():
                     for iv in obj.getImageVersions():
                         try:
-                            os.remove(iv.getLocation().encode(env.codeset))
+                            os.remove(iv.getLocation())
                             # TODO: Delete from image cache too?
                         except OSError:
                             pass
@@ -519,7 +519,7 @@ class ObjectCollection(object):
                 else:
                     commandString = env.rotateLeftCommand
                 command = commandString % { "location":location }
-                result = os.system(command.encode(env.codeset))
+                result = os.system(command.encode(env.localeEncoding))
                 if result == 0:
                     imageversion.contentChanged()
                     model = self.getUnsortedModel()
@@ -552,7 +552,7 @@ class ObjectCollection(object):
                 locations += location + " "
         if locations != "":
             command = env.openCommand % { "locations":locations }
-            result = os.system(command.encode(env.codeset) + " &")
+            result = os.system(command.encode(env.localeEncoding) + " &")
             if result != 0:
                 dialog = gtk.MessageDialog(
                     type=gtk.MESSAGE_ERROR,
@@ -591,7 +591,7 @@ class ObjectCollection(object):
                     obj.getPrimaryVersion(),
                     env.thumbnailSize[0],
                     env.thumbnailSize[1])[0]
-                pixbuf = gtk.gdk.pixbuf_new_from_file(thumbnailLocation.encode(env.codeset))
+                pixbuf = gtk.gdk.pixbuf_new_from_file(thumbnailLocation)
                 # TODO Set and use COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
             except IOError:
                 pixbuf = env.unknownImageIconPixbuf
index 1217b91..792e0a1 100644 (file)
@@ -1,30 +1,34 @@
-import ConfigParser
+import codecs
 import os
 import sys
+from kofoto.clientutils import expanduser
+from kofoto.config import Config
 
 class PersistentState(object):
-    def __init__(self):
-        self.__configParser = ConfigParser.ConfigParser()
+    def __init__(self, env):
+        self.__configParser = Config(env.localeEncoding)
+        self.__env = env
         cp = self.__configParser
 
         # Defaults:
         cp.add_section("state")
         cp.set("state", "filter-text", "")
 
-        home = os.path.expanduser("~")
+        home = expanduser("~")
         if sys.platform.startswith("win"):
             self.__stateFile = os.path.join(
-                home, "KofotoData", "state", "gkofoto.ini")
+                home, u"KofotoData", u"state", u"gkofoto.ini")
         else:
             self.__stateFile = os.path.join(
-                home, ".kofoto", "state", "gkofoto")
+                home, u".kofoto", u"state", u"gkofoto")
         if not os.path.isdir(os.path.dirname(self.__stateFile)):
             os.mkdir(os.path.dirname(self.__stateFile))
         if os.path.isfile(self.__stateFile):
-            self.__configParser.read(self.__stateFile)
+            cp.read(self.__stateFile)
 
     def save(self):
-        self.__configParser.write(open(self.__stateFile, "w"))
+        fp = codecs.open(self.__stateFile, "w", self.__env.localeEncoding)
+        self.__configParser.write(fp)
 
     def getFilterText(self):
         return self.__configParser.get("state", "filter-text")
index 7f82e10..4f4dc6e 100644 (file)
@@ -42,7 +42,7 @@ class RegisterImageVersionsDialog:
             base, filename = os.path.split(imageversion.getLocation())
             prefix, _ = os.path.splitext(filename)
             for candidateFilename in os.listdir(base):
-                if (re.match("%s[^a-zA-Z0-9].*" % prefix, candidateFilename)):
+                if re.match("%s[^a-zA-Z0-9].*" % prefix, candidateFilename):
                     candidatePath = os.path.join(base, candidateFilename)
                     if os.path.isfile(candidatePath):
                         try:
@@ -61,7 +61,8 @@ class RegisterImageVersionsDialog:
         changed = False
         for path in selectedRows:
             treeiter = self._fileListStore.get_iter(path)
-            location = self._fileListStore.get_value(treeiter, 0)
+            location = \
+                self._fileListStore.get_value(treeiter, 0).decode("utf-8")
             try:
                 imageVersion = env.shelf.createImageVersion(
                     self._image, location, ImageVersionType.Other)
@@ -98,7 +99,8 @@ class RegisterImageVersionsDialog:
              gtk.STOCK_OK, gtk.RESPONSE_OK))
         dialog.set_select_multiple(True)
         if dialog.run() == gtk.RESPONSE_OK:
-            self.__setFiles(dialog.get_filenames())
+            filenames = [x.decode("utf-8") for x in dialog.get_filenames()]
+            self.__setFiles(filenames)
         dialog.destroy()
 
     def __setFiles(self, filenames):
index f60ba4f..cf1535a 100644 (file)
@@ -6,7 +6,7 @@ import os
 import re
 from kofoto.outputengine import OutputEngine
 
-css = '''
+css = u'''
 html {
     color: #000000;
     background: #dddddd;
@@ -132,7 +132,7 @@ nextalbum_png = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\x00\x00\x00
 
 previousalbum_png = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\x00\x00\x00\x11\x08\x03\x00\x00\x00\xf0\xa6\x9c\x02\x00\x00\x01;PLTE\x00\x00\xd3\x00\x00\x00\x04\x00\x07\x10\x00\x1d\x17\x03&CCn\x1b\x134 \x0e-ov\xa6P_\x91\x16\x112\x06\x00\t\x1f\x0b*#\x10/#\x101$\x1107+J\x85\x8a\xba\x83\x92\xcaGZ\x8f\x13\x0f2uq\x9c\x88\x88\xb9\x89\x89\xba\x8b\x8b\xba\x8a\x8a\xbb\x8c\x8c\xbb\x8d\x8e\xc0\x97\x9d\xd4\x97\xa0\xd8~\x8e\xc9GY\x92\x16\x0f> \r.}}\xb2\x98\x9f\xd7\x98\xa1\xd7\x99\xa0\xd8\x96\x9f\xd7\x97\x9e\xd6\x92\x9d\xd5\x7f\x8e\xc8PY\x91\x17\x0e3\x1a\n+lp\xa9\x84\x90\xcf\x84\x92\xcf\x83\x93\xce\x85\x93\xd0\x84\x94\xcf\x86\x95\xcf\x86\x94\xcf\x87\x95\xd0n\x7f\xbf7I\x84\n\x04)\x11\x00)BM\x95Jd\xb1If\xb2Ic\xb0Jh\xb3Db\xaf1M\x96\x0e\x17O\x00\x00\r\r\x00%/>\x8a0J\x9f.H\x9f-H\x9e.K\x9f-J\x9c&?\x91\x12"c\x03\x00!\x0b\x00#,;\x87+B\x9a(B\x99)D\x9a+E\x9a*D\x99";\x8d\x10\x1ea\x08\x00\x1d!,n\x1d/|\x19+z\x1c.\x7f%=\x94\x00\x00\x03\x07\x00\x19\x04\x00\x1b\x08\x030\x1a/}!9\x8a\x04\x00\x19\x13"n\x0e\x1c_\x01\x00\x11\t\x10F\x03\x00\x1f\x00\x00\x01\x8b\x8fZv\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00\x01bKGD\x00\x88\x05\x1dH\x00\x00\x00\tpHYs\x00\x00\x0b\x12\x00\x00\x0b\x12\x01\xd2\xdd~\xfc\x00\x00\x00\x07tIME\x07\xd4\x01\x02\x137\r"\xc4\xd4\x02\x00\x00\x00\xf4IDATx\x9cc`\x80\x02F\x06\xec\x80\x99\t\xbb8\x1b+\x0bVq.N\x0evl\xe2"\xc2B\x82\x02\xfc\xfc||\xbc\xbc\xfc@\x0c\x04<\xdc q\x05y9Y\x19i))Iq1\t0!.&\xca\x03\x14\xd7\xd6\xd2\xd4\x90UWSVQQQ\x05\x13**\xcaJ\x8a\x0c\x0c\x16\xe6f\xfa\xa6\xa6\xa6&\xc6F\x06\xfa\x86`\xc2@_ER\x87\xc1\xc9\xd1\xc1\xde\xc6\x0e\x08l\x80\xc0\x16\x0cl\x8cT$\xf9\x19\x18|\xbc\xbd<= \xc0\xcd\x1d\x0cl\x0cT$\xf9\x80v\xf8\x84\x86\x04\x07\x05\x06\xc0\x81\x9b\x8d\xbe\x8a8/\x03D& :*\x12\n\x02\xdcm\rU\xc5\xc0\x12@\x99\xc4\x84\xf88\x08\x88\x04\xea0P\x91\x80H0\xf8\xa4$\'AX`\t\x98Q@\x90\x9e\x96\n\x95\x88\xf0w\xb5\xd6S\x16\x83I08e@\xe8\xd8p?\x17+]%\xb0\xcfQ\xe2#&\xcc\xd7\xd9RG\x91\x87\x1b\x00\xe4\xb53\x1e\x96h\xda\xbf\x00\x00\x00\x00IEND\xaeB`\x82'
 
-album_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
+album_template = u'''<?xml version="1.0" encoding="%(charenc)s"?>
 <!DOCTYPE html
      PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -172,7 +172,7 @@ album_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
 </html>
 '''
 
-thumbnails_frame_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
+thumbnails_frame_template = u'''<?xml version="1.0" encoding="%(charenc)s"?>
 <!DOCTYPE html
      PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -189,14 +189,14 @@ thumbnails_frame_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
 
 # At least Opera 6.12 behaves strangely with "text-align: center;" in
 # stylesheet, so use align="center" instead.
-thumbnails_frame_entry_template = '''<a name="%(number)s"></a>
+thumbnails_frame_entry_template = u'''<a name="%(number)s"></a>
 <a href="%(htmlref)s" class="toc" target="main">
 <div align="center">
 <img src="%(thumbimgref)s" class="toc" alt="" /></div>
 </a>
 '''
 
-subalbum_entry_template = '''<td align="center" valign="top">
+subalbum_entry_template = u'''<td align="center" valign="top">
 <p>%(title)s</p>
 <table border="0" cellspacing="0" cellpadding="0">
 <tr>
@@ -220,14 +220,14 @@ subalbum_entry_template = '''<td align="center" valign="top">
 </td>
 '''
 
-image_entry_template = '''<td align="left" valign="bottom">
+image_entry_template = u'''<td align="left" valign="bottom">
 <a href="%(frameref)s">
 <img class="thinborder" src="%(thumbimgref)s" alt="" />
 </a>
 </td>
 '''
 
-image_frameset_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
+image_frameset_template = u'''<?xml version="1.0" encoding="%(charenc)s"?>
 <!DOCTYPE html
      PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
@@ -252,7 +252,7 @@ This album needs frames. Sorry.
 </html>
 '''
 
-image_frame_template = '''<?xml version="1.0" encoding="%(charenc)s"?>
+image_frame_template = u'''<?xml version="1.0" encoding="%(charenc)s"?>
 <!DOCTYPE html
      PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -331,7 +331,7 @@ class OutputGenerator(OutputEngine):
                 tmpl = env.config.get("woolly", "auto_descriptions_template")
                 self.autoImageDescTags = re.findall("<(.*?)>", tmpl)
                 self.autoImageDescTemplate = re.sub(
-                    "<(.*?)>", r"%(\1)s", tmpl).encode(self.charEnc)
+                    "<(.*?)>", r"%(\1)s", tmpl)
         except ValueError:
             pass
 
@@ -347,9 +347,9 @@ class OutputGenerator(OutputEngine):
         if self.env.verbose:
             self.env.out("Generating index page, style sheet and icons...\n")
         self.symlinkFile(
-            "%s.html" % root.getTag().encode(self.charEnc),
+            "%s.html" % root.getTag(),
             "index.html")
-        self.writeFile("woolly.css", css)
+        self.writeFile("woolly.css", css, self.charEnc)
         for data, filename in [
                 (transparent_1x1_png, "1x1.png"),
                 (previous_png, "previous.png"),
@@ -372,7 +372,7 @@ class OutputGenerator(OutputEngine):
                 (frame_toprightlower_png, "frame-toprightlower.png"),
                 (frame_toprightupper_png, "frame-toprightupper.png")]:
             self.writeFile(
-                os.path.join(self.iconsdir, filename), data, 1)
+                os.path.join(self.iconsdir, filename), data, binary=True)
 
 
     def generateAlbum(self, album, subalbums, images, paths):
@@ -395,7 +395,7 @@ class OutputGenerator(OutputEngine):
             # Create uplink.
             if len(paths[0]) > 1:
                 uplink = '<link rel="up" href="%s-%dx%d.html" />' % (
-                    paths[0][-2].getTag().encode(self.charEnc),
+                    paths[0][-2].getTag(),
                     wlim,
                     hlim)
             else:
@@ -426,7 +426,7 @@ class OutputGenerator(OutputEngine):
                     subalbumtextElements.append(subalbum_entry_template % {
                         "iconsdir": self.iconsdir,
                         "htmlref": "%s-%dx%d.html" % (
-                            subalbum.getTag().encode(self.charEnc),
+                            subalbum.getTag(),
                             wlim,
                             hlim),
                         "thumbheight": thumbheight,
@@ -434,7 +434,7 @@ class OutputGenerator(OutputEngine):
                         "thumbimgref": thumbimgref,
                         "thumbwidth": thumbwidth,
                         "thumbwidth_minus_6": thumbwidth - 6,
-                        "title": title.encode(self.charEnc),
+                        "title": title,
                         })
                     number += 1
                 subalbumtextElements.append("</tr>\n")
@@ -468,13 +468,9 @@ class OutputGenerator(OutputEngine):
 
             # Album overview.
             desc = album.getAttribute(u"description") or u""
-            desc = desc.encode(self.charEnc)
             title = album.getAttribute(u"title") or album.getTag()
-            title = title.encode(self.charEnc)
 
-            filename = "%s-%dx%d.html" % (album.getTag().encode(self.charEnc),
-                                          wlim,
-                                          hlim)
+            filename = "%s-%dx%d.html" % (album.getTag(), wlim, hlim)
             self.writeFile(
                 filename,
                 album_template % {
@@ -487,7 +483,8 @@ class OutputGenerator(OutputEngine):
                     "thishref": filename,
                     "title": title,
                     "uplink": uplink,
-                })
+                },
+                self.charEnc)
             self._maybeMakeUTF8Symlink("%s-%dx%d.html" % (album.getTag(),
                                                           wlim,
                                                           hlim))
@@ -522,7 +519,8 @@ class OutputGenerator(OutputEngine):
                              "thumbnails-%dx%d.html" % (wlim, hlim)),
                 thumbnails_frame_template % {
                     "charenc": self.charEnc,
-                    "entries": thumbnailstext})
+                    "entries": thumbnailstext},
+                self.charEnc)
             self._maybeMakeUTF8Symlink(
                 os.path.join(str(album.getId()),
                              "thumbnails-%dx%d.html" % (wlim, hlim)))
@@ -532,10 +530,10 @@ class OutputGenerator(OutputEngine):
         # ------------------------------------------------------------
 
         self.symlinkFile(
-            "%s-%dx%d.html" % (album.getTag().encode(self.charEnc),
+            "%s-%dx%d.html" % (album.getTag(),
                                self.env.defaultsizelimit[0],
                                self.env.defaultsizelimit[1]),
-            "%s.html" % album.getTag().encode(self.charEnc))
+            "%s.html" % album.getTag())
         self._maybeMakeUTF8Symlink("%s.html" % album.getTag())
 
 
@@ -558,7 +556,7 @@ class OutputGenerator(OutputEngine):
             pathtext = self._generatePathText(wlim, hlim, paths, "../")
             uplink = \
                 '<link rel="up" href="../%s-%dx%d.html" target="_top" />' % (
-                    paths[0][-1].getTag().encode(self.charEnc),
+                    paths[0][-1].getTag(),
                     wlim,
                     hlim)
 
@@ -638,9 +636,7 @@ class OutputGenerator(OutputEngine):
                 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)
 
             imageCategories = list(image.getCategories())
             infotextElements = []
@@ -656,8 +652,7 @@ class OutputGenerator(OutputEngine):
                             if cat.isParentOf(imgcat, True):
                                 catlist.append(imgcat)
                         catdict[tag] = ", ".join(
-                            [x.getDescription().encode(self.charEnc)
-                             for x in catlist])
+                            [x.getDescription() for x in catlist])
                     descElement = self.autoImageDescTemplate % catdict
                 else:
                     descElement = ""
@@ -667,7 +662,7 @@ class OutputGenerator(OutputEngine):
                 ' width="100%">\n<tr>')
             firstrow = True
             for dispcat in self.displayCategories:
-                matching = [x.getDescription().encode(self.charEnc)
+                matching = [x.getDescription()
                             for x in imageCategories
                             if dispcat.isParentOf(x, True)]
                 if matching:
@@ -679,14 +674,13 @@ class OutputGenerator(OutputEngine):
                     infotextElements.append(
                         '<td align="left"><small><b>%s</b>:'
                         ' %s</small></td>' % (
-                            dispcat.getDescription().encode(self.charEnc),
+                            dispcat.getDescription(),
                             ", ".join(matching)))
             infotextElements.append('</td><td align="right">')
             timestamp = image.getAttribute(u"captured")
             if timestamp:
                 infotextElements.append(
-                    "<small>%s</small><br />" % (
-                    timestamp.encode(self.charEnc)))
+                    "<small>%s</small><br />" % timestamp)
             infotextElements.append("</td></tr></table>")
             infotext = "".join(infotextElements)
 
@@ -709,7 +703,8 @@ class OutputGenerator(OutputEngine):
                     "thumbnailsframewidth": \
                         self.env.thumbnailsizelimit[0] + 70,
                     "uplink": uplink,
-                    })
+                    },
+                self.charEnc)
 
             imgref, imgwidth, imgheight = self.getImageReference(
                 image, wlim, hlim)
@@ -735,7 +730,8 @@ class OutputGenerator(OutputEngine):
                     "smaller": smallertext,
                     "thumbnailsanchor": str(number),
                     "title": title,
-                    })
+                    },
+                self.charEnc)
 
 
     def _generatePathText(self, wlim, hlim, paths, pathprefix):
@@ -753,14 +749,13 @@ class OutputGenerator(OutputEngine):
                     '''<a href="%(pathprefix)s%(htmlref)s" target="_top">'''
                     '''%(title)s</a>''' % {
                         "htmlref": "%s-%dx%d.html" % (
-                            node.getTag().encode(self.charEnc),
+                            node.getTag(),
                             wlim,
                             hlim),
                         "pathprefix": pathprefix,
-                        "title": title.encode(self.charEnc),
+                        "title": title,
                         })
-            pathtextElements.append(
-                u" \xbb ".encode(self.charEnc).join(els))
+            pathtextElements.append(u" \xbb ".join(els))
             pathtextElements.append("</td>\n")
             pathtextElements.append(
                 "<td width=\"40%\" align=\"right\" style=\"text-align:"
@@ -786,13 +781,12 @@ class OutputGenerator(OutputEngine):
                         '&nbsp;<a href="%(pathprefix)s%(htmlref)s"'
                         ' target="_top">%(title)s</a>' % {
                             "htmlref": "%s-%dx%d.html" % (
-                                sibling.getTag().encode(self.charEnc),
+                                sibling.getTag(),
                                 wlim,
                                 hlim),
                             "iconsdir": self.iconsdir,
                             "pathprefix": pathprefix,
-                            "title": title.replace(" ", "&nbsp;").encode(
-                                self.charEnc)
+                            "title": title.replace(" ", "&nbsp;")
                             })
                 if thispos == len(children) - 1:
                     # No next sibling.
@@ -808,13 +802,12 @@ class OutputGenerator(OutputEngine):
                         '&nbsp;<a href="%(pathprefix)s%(htmlref)s"'
                         ' target="_top">%(title)s</a>' % {
                             "htmlref": "%s-%dx%d.html" % (
-                                sibling.getTag().encode(self.charEnc),
+                                sibling.getTag(),
                                 wlim,
                                 hlim),
                             "iconsdir": self.iconsdir,
                             "pathprefix": pathprefix,
-                            "title": title.replace(" ", "&nbsp;").encode(
-                                self.charEnc),
+                            "title": title.replace(" ", "&nbsp;"),
                             })
                 pathtextElements.append(prevalbumtext)
                 pathtextElements.append("\n")
index bb18725..0e30b2a 100644 (file)
@@ -2,6 +2,7 @@
 
 __all__ = ["OutputEngine"]
 
+import codecs
 import os
 import re
 import time
@@ -69,7 +70,7 @@ class OutputEngine:
             year = "undated"
             captured = image.getAttribute(u"captured")
             if captured:
-                m = re.match("^(\d{4})-?(\d{0,2})", captured)
+                m = re.match(u"^(\d{4})-?(\d{0,2})", captured)
                 if m:
                     year = m.group(1)
                     month = m.group(2)
@@ -102,7 +103,7 @@ class OutputEngine:
             htmlimgloc = os.path.join(
                 "@images",
                 "%sx%s" % (widthlimit, heightlimit),
-                helper(ext).encode(self.env.codeset))
+                helper(ext))
             # Generate a unique htmlimgloc/imgloc.
             i = 1
             while True:
@@ -127,20 +128,25 @@ class OutputEngine:
         return self.__imgrefMap[key]
 
 
-    def writeFile(self, filename, text, binary=False):
+    def writeFile(self, filename, text, encoding=None, 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.
+        text     -- The text to write. If binary is true, text must be a
+                    byte string (str), otherwise unicode.
+        encoding -- How to encode the text. Only used for non-binary text.
         binary   -- Whether the text is to be treated as binary.
         """
+        path = os.path.join(self.__dest, filename)
         if binary:
-            mode = "wb"
+            assert isinstance(text, str)
+            f = open(path, "wb")
         else:
-            mode = "w"
-        file(os.path.join(self.__dest, filename), mode).write(text)
+            assert isinstance(text, unicode)
+            f = codecs.open(path, "w", encoding)
+        f.write(text)
 
 
     def symlinkFile(self, source, destination):
@@ -178,7 +184,7 @@ class OutputEngine:
                 for child in album.getAlbumChildren():
                     addDescendants(albumset, child)
 
-        self.__dest = dest.encode(self.env.codeset)
+        self.__dest = dest
         try:
             os.mkdir(self.__dest)
         except OSError:
@@ -208,8 +214,8 @@ class OutputEngine:
                     childrentext = "1 child"
                 else:
                     childrentext = "%d children" % nchildren
-                self.env.out("Creating album %s (%d of %d) with %s...\n" % (
-                    album.getTag().encode(self.env.codeset),
+                self.env.out(u"Creating album %s (%d of %d) with %s...\n" % (
+                    album.getTag(),
                     i,
                     len(albumsToGenerate),
                     childrentext))
@@ -221,8 +227,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))
+            self.env.out(u"Generating album page for %s...\n" % album.getTag())
 
         # Design choice: This output engine sorts subalbums before
         # images.
@@ -237,9 +242,9 @@ class OutputEngine:
             child = imagechildren[ix]
             if self.env.verbose:
                 self.env.out(
-                    "Generating image page for image %d in album %s...\n" % (
+                    u"Generating image page for image %d in album %s...\n" % (
                         child.getId(),
-                        album.getTag().encode(self.env.codeset)))
+                        album.getTag()))
             self.generateImage(album, child, imagechildren, ix, paths)
 
 
index ecd83c8..6cc4714 100644 (file)
@@ -132,15 +132,12 @@ class Shelf:
     ##############################
     # Public methods.
 
-    def __init__(self, location, codeset):
+    def __init__(self, location):
         """Constructor.
 
-        Location is where the database is located. Codeset is the
-        character encoding to use when encoding filenames stored in
-        the database. (Thus, the codeset parameter does not specify
-        how data is stored in the database.)"""
+        Location is where the database is located.
+        """
         self.location = location
-        self.codeset = codeset
         self.transactionLock = threading.Lock()
         self.inTransaction = False
         self.objectcache = {}
@@ -592,10 +589,9 @@ class Shelf:
         except: # Work-around for buggy PIL.
             raise NotAnImageFileError, location
         width, height = pilimg.size
-        location = unicode(
-            os.path.realpath(location.encode(self.codeset)), self.codeset)
+        location = os.path.realpath(location)
         mtime = os.path.getmtime(location)
-        ivhash = computeImageHash(location.encode(self.codeset))
+        ivhash = computeImageHash(location)
         cursor = self.connection.cursor()
         try:
             cursor.execute(
@@ -1949,8 +1945,7 @@ class ImageVersion:
         """
         from kofoto import EXIF
         image = self.getImage()
-        tags = EXIF.process_file(
-            file(self.getLocation().encode(self.shelf.codeset), "rb"))
+        tags = EXIF.process_file(file(self.getLocation(), "rb"))
 
         for tag in ["Image DateTime",
                     "EXIF DateTimeOriginal",
index ce0d6e2..1ddcf8e 100755 (executable)
@@ -49,7 +49,6 @@ PICDIR = unicode(os.path.realpath(
 ######################################################################
 
 db = "shelf.tmp"
-codeset = "latin1"
 
 def removeTmpDb():
     for x in [db, db + "-journal"]:
@@ -63,7 +62,7 @@ class LockerThread(threading.Thread):
         self.ltContinue = ltContinue
 
     def run(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         self.mContinue.set()
@@ -131,7 +130,7 @@ class TestNegativeShelfOpens(unittest.TestCase):
 
     def test_NonexistingShelf(self):
         try:
-            s = Shelf(db, codeset)
+            s = Shelf(db)
             s.begin()
         except ShelfNotFoundError:
             pass
@@ -142,7 +141,7 @@ class TestNegativeShelfOpens(unittest.TestCase):
     def test_BadShelf(self):
         file(db, "w") # Create empty file.
         try:
-            s = Shelf(db, codeset)
+            s = Shelf(db)
             s.begin()
         except UnsupportedShelfError:
             pass
@@ -157,7 +156,7 @@ class TestNegativeShelfOpens(unittest.TestCase):
         mContinue.wait()
         try:
             try:
-                s = Shelf(db, codeset)
+                s = Shelf(db)
                 s.begin()
             except ShelfLockedError:
                 pass
@@ -172,13 +171,13 @@ class TestShelfCreation(unittest.TestCase):
         removeTmpDb()
 
     def test_CreateShelf1(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         assert os.path.exists(db)
 
     def test_CreateShelf2(self):
         file(db, "w") # Create empty file.
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         try:
             s.create()
         except FailedWritingError:
@@ -191,12 +190,12 @@ class TestShelfMemoryLeakage(unittest.TestCase):
         removeTmpDb()
 
     def test_MemoryLeak1(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         assert gc.collect() == 0
 
     def test_MemoryLeak2(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         s.getObject(0)
@@ -204,7 +203,7 @@ class TestShelfMemoryLeakage(unittest.TestCase):
         assert gc.collect() == 0
 
     def test_MemoryLeak3(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         s.getObject(0)
@@ -216,19 +215,19 @@ class TestShelfTransactions(unittest.TestCase):
         removeTmpDb()
 
     def test_commit(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         s.createAlbum(u"foo")
         assert s.getAlbumByTag(u"foo")
         s.commit()
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.begin()
         assert s.getAlbumByTag(u"foo")
         s.rollback()
 
     def test_rollback(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         s.createAlbum(u"foo")
@@ -242,7 +241,7 @@ class TestShelfTransactions(unittest.TestCase):
             assert False
 
     def test_isModified(self):
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         assert not s.isModified()
@@ -257,7 +256,7 @@ class TestShelfTransactions(unittest.TestCase):
         res = [False]
         def f(x):
             res[0] = True
-        s = Shelf(db, codeset)
+        s = Shelf(db)
         s.create()
         s.begin()
         s.registerModificationCallback(f)
@@ -275,7 +274,7 @@ class TestShelfTransactions(unittest.TestCase):
 
 class TestShelfFixture(unittest.TestCase):
     def setUp(self):
-        self.shelf = Shelf(db, codeset)
+        self.shelf = Shelf(db)
         self.shelf.create()
         self.shelf.begin()
         root = self.shelf.getRootAlbum()