Commits

Jason R. Coombs  committed 9e817f9 Merge

Merged jelmer/python-keyring-lib (pull request #10)

  • Participants
  • Parent commits 0959e8c, 606167d

Comments (0)

Files changed (24)

-buildout = [svn]svn://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
+buildout = [svn]http://svn.zope.org/repos/main/zc.buildout/trunk/bootstrap
 151f79315c8250982ce2c8af27dad16009a0edb4 0.5.1
 f43ff1d34884a6b8cba6574960cdcbe126140c59 0.6.1
 a604d2b8549c8ac97f7537609191ba32292b3d25 0.6.2
+a367ade70d37206891f0653f68472fbcc59d97f1 0.7
+695fde92f9cf7dd7dd5ef6a03b37db1271d01dcc 0.7.1
+fff4872e0b6be02ee2c7a6802d8f2c7129d210aa 0.8
+0dcd2ef344a060b130833d5a13f3dab2d1fa8f15 0.8.1
--------
+=======
 CHANGES
--------
+=======
+
+-----
+0.8.1
+-----
+
+* Fix regression in keyring lib on Windows XP where the LOCALAPPDATA
+  environment variable is not present.
+
+---
+0.8
+---
+
+* Mac OS X keyring backend now uses subprocess calls to the `security`
+  command instead of calling the API, which with the latest updates, no
+  longer allows Python to invoke from a virtualenv. Fixes issue #13.
+* When using file-based storage, the keyring files are no longer stored
+  in the user's home directory, but are instead stored in platform-friendly
+  locations (`%localappdata%\Python Keyring` on Windows and according to
+  the freedesktop.org Base Dir Specification
+  (`$XDG_DATA_HOME/python_keyring` or `$HOME/.local/share/python_keyring`)
+  on other operating systems). This fixes #21.
+
+*Backward Compatibility Notice*
+
+Due to the new storage location for file-based keyrings, keyring 0.8
+supports backward compatibility by automatically moving the password
+files to the updated location. In general, users can upgrade to 0.8 and
+continue to operate normally. Any applications that customize the storage
+location or make assumptions about the storage location will need to take
+this change into consideration. Additionally, after upgrading to 0.8,
+it is not possible to downgrade to 0.7 without manually moving
+configuration files. In 1.0, the backward compatibilty
+will be removed.
+
+-----
+0.7.1
+-----
+
+* Removed non-ASCII characters from README and CHANGES docs (required by
+  distutils if we're to include them in the long_description). Fixes #55.
 
 ---
 0.7
 ---
 
+* Python 3 is now supported. All tests now pass under Python 3.2 on
+  Windows and Linux (although Linux backend support is limited). Fixes #28.
 * Extension modules on Mac and Windows replaced by pure-Python ctypes
-  implementations. Thanks to Jérôme Laheurte.
+  implementations. Thanks to Jerome Laheurte.
+* WinVaultKeyring now supports multiple passwords for the same service. Fixes
+  #47.
+* Most of the tests don't require user interaction anymore.
+* Entries stored in Gnome Keyring appears now with a meaningful name if you try
+  to browser your keyring (for ex. with Seahorse)
+* Tests from Gnome Keyring no longer pollute the user own keyring.
+* `keyring.util.escape` now accepts only unicode strings. Don't try to encode
+  strings passed to it.
 
 -----
 0.6.2

File CONTRIBUTORS.txt

 * Gary Poster
 * Patrick Mezard
 * Jérôme Laheurte
+* Jake Basile
 recursive-include demo *.py
 include *.py
 include *.txt
+exclude .hg/last-message.txt

File buildout.cfg

 
 [test]
 recipe = zc.recipe.testrunner
-eggs = keyring
+eggs = keyring [test]
 defaults = '--tests-pattern tests --exit-with-status'.split()
 working-directory = .
 
 
 distribute = 0.6.14
 setuptools = 0.6c11
+unittest2 = 0.5.1
 z3c.recipe.scripts = 1.0.1
 zc.buildout = 1.5.2
 zc.recipe.egg = 1.3.2

File keyring/__init__.py

 import logging
 logger = logging.getLogger('keyring')
 
-from core import set_keyring, get_keyring, set_password, get_password
+from keyring.core import set_keyring, get_keyring, set_password, get_password
 from keyring.getpassbackend import get_password as get_pass_get_password

File keyring/backend.py

 Created by Kang Zhang on 2009-07-09
 """
 
+import getpass
 import os
 import sys
 import ConfigParser
+import base64
 
 from keyring.util.escape import escape as escape_for_ini
+from keyring.util import properties
+import keyring.util.platform
+import keyring.util.loc_compat
 
 try:
-    from abc import ABCMeta, abstractmethod
+    from abc import ABCMeta, abstractmethod, abstractproperty
 except ImportError:
     # to keep compatible with older Python versions.
     class ABCMeta(type):
     def abstractmethod(funcobj):
         return funcobj
 
+    def abstractproperty(funcobj):
+        return property(funcobj)
+
 _KEYRING_SETTING = 'keyring-setting'
 _CRYPTED_PASSWORD = 'crypted-password'
 _BLOCK_SIZE = 32
 class GnomeKeyring(KeyringBackend):
     """Gnome Keyring"""
 
+    # Name of the keyring to store the passwords in.
+    # Use None for the default keyring.
+    KEYRING_NAME = None
+
     def supported(self):
         try:
             import gnomekeyring
         """
         import gnomekeyring
         try:
-            gnomekeyring.set_network_password_sync(None, username, service,
-                None, None, None, None, 0, password)
+            gnomekeyring.item_create_sync(
+                self.KEYRING_NAME, gnomekeyring.ITEM_NETWORK_PASSWORD,
+                "Password for '%s' on '%s'" % (username, service),
+                {'user': username, 'domain': service},
+                password, True)
         except gnomekeyring.CancelledError:
             # The user pressed "Cancel" when prompted to unlock their keyring.
             raise PasswordSetError("cancelled by user")
 
 def open_kwallet(kwallet_module=None, qt_module=None):
 
-    global kwallet
-    if not kwallet is None:
-        return kwallet
+    # If we specified the kwallet_module and/or qt_module, surely we won't need
+    # the cached kwallet object...
+    if kwallet_module is None and qt_module is None:
+        global kwallet
+        if not kwallet is None:
+            return kwallet
 
     # Allow for the injection of module-like objects for testing purposes.
     if kwallet_module is None:
     format.
     """
 
-    def __init__(self):
-        self.file_path = os.path.join(os.path.expanduser("~"), self.filename())
+    @properties.NonDataProperty
+    def file_path(self):
+        """
+        The path to the file where passwords are stored. This property
+        may be overridden by the subclass or at the instance level.
+        """
+        return os.path.join(keyring.util.platform.data_root(), self.filename)
 
-    @abstractmethod
+    @abstractproperty
     def filename(self):
-        """Return the filename used to store the passwords.
+        """The filename used to store the passwords.
         """
         pass
 
         """
         pass
 
+    def _relocate_file(self):
+        old_location = os.path.join(os.path.expanduser('~'), self.filename)
+        new_location = self.file_path
+        keyring.util.loc_compat.relocate_file(old_location, new_location)
+        # disable this function - it only needs to be run once
+        self._relocate_file = lambda: None
+
     def get_password(self, service, username):
         """Read the password from the file.
         """
+        self._relocate_file()
         service = escape_for_ini(service)
         username = escape_for_ini(username)
 
 
         # fetch the password
         try:
-            password_base64 = config.get(service, username)
+            password_base64 = config.get(service, username).encode()
             # decode with base64
-            password_encrypted = password_base64.decode("base64")
+            password_encrypted = base64.decodestring(password_base64)
             # decrypted the password
-            password = self.decrypt(password_encrypted)
+            password = self.decrypt(password_encrypted).decode('utf-8')
         except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
             password = None
         return password
     def set_password(self, service, username, password):
         """Write the password in the file.
         """
+        self._relocate_file()
         service = escape_for_ini(service)
         username = escape_for_ini(username)
 
         # encrypt the password
-        password_encrypted = self.encrypt(password)
+        password_encrypted = self.encrypt(password.encode('utf-8'))
         # load the password from the disk
         config = ConfigParser.RawConfigParser()
         if os.path.exists(self.file_path):
             config.read(self.file_path)
 
         # encode with base64
-        password_base64 = password_encrypted.encode("base64")
+        password_base64 = base64.encodestring(password_encrypted).decode()
         # write the modification
         if not config.has_section(service):
             config.add_section(service)
         config.set(service, username, password_base64)
+        # ensure the storage path exists
+        if not os.path.isdir(os.path.dirname(self.file_path)):
+            os.makedirs(os.path.dirname(self.file_path))
         config_file = open(self.file_path,'w')
         config.write(config_file)
 
 class UncryptedFileKeyring(BasicFileKeyring):
     """Uncrypted File Keyring"""
-    def filename(self):
-        """Return the filename of the password file. It should be
-        "keyring_pass.cfg" .
-        """
-        return "keyring_pass.cfg"
+
+    filename = 'keyring_pass.cfg'
 
     def encrypt(self, password):
         """Directly return the password itself.
 
 class CryptedFileKeyring(BasicFileKeyring):
     """PyCrypto File Keyring"""
-    def __init__(self):
-        super(CryptedFileKeyring, self).__init__()
-        self.crypted_password = None
 
-    def filename(self):
-        """Return the filename for the password file.
-        """
-        return "crypted_pass.cfg"
+    filename = 'crypted_pass.cfg'
+    crypted_password = None
 
     def supported(self):
         """Applicable for all platforms, but not recommend"
             status = -1
         return status
 
+    def _getpass(self, *args, **kwargs):
+        """Wrap getpass.getpass(), so that we can override it when testing.
+        """
+
+        return getpass.getpass(*args, **kwargs)
+
     def _init_file(self):
         """Init the password file, set the password for it.
         """
 
-        print("Please set a password for your new keyring")
         password = None
         while 1:
             if not password:
-                import getpass
-                password = getpass.getpass()
-                password2 = getpass.getpass('Password (again): ')
+                password = self._getpass("Please set a password for your new keyring")
+                password2 = self._getpass('Password (again): ')
                 if password != password2:
                     sys.stderr.write("Error: Your passwords didn't match\n")
                     password = None
         if not self._check_file():
             self._init_file()
 
-        print "Please input your password for the keyring"
-        import getpass
-        password = getpass.getpass()
+        password = self._getpass("Please input your password for the keyring")
 
         if not self._auth(password):
             sys.stderr.write("Wrong password for the keyring.\n")
 
 class Win32CryptoKeyring(BasicFileKeyring):
     """Win32 Cryptography Keyring"""
+
+    filename = 'wincrypto_pass.cfg'
+
     def __init__(self):
         super(Win32CryptoKeyring, self).__init__()
 
         try:
             from backends import win32_crypto
             self.crypt_handler = win32_crypto
-        except ImportError, e:
+        except ImportError:
             self.crypt_handler = None
 
-    def filename(self):
-        """Return the filename for the password storages file.
-        """
-        return "wincrypto_pass.cfg"
-
     def supported(self):
         """Recommended when other Windows backends are unavailable
         """
 
 
 class WinVaultKeyring(KeyringBackend):
+    """
+    WinVaultKeyring stores encrypted passwords using the Windows Credential
+    Manager.
+
+    Requires pywin32
+
+    This backend does some gymnastics to simulate multi-user support,
+    which WinVault doesn't support natively. See
+    https://bitbucket.org/kang/python-keyring-lib/issue/47/winvaultkeyring-only-ever-returns-last#comment-731977
+    for details on the implementation, but here's the gist:
+
+    Passwords are stored under the service name unless there is a collision
+    (another password with the same service name but different user name),
+    in which case the previous password is moved into a compound name:
+    {username}@{service}
+    """
     def __init__(self):
         super(WinVaultKeyring, self).__init__()
         try:
-            import pywintypes, win32cred
+            import pywintypes
+            import win32cred
             self.win32cred = win32cred
             self.pywintypes = pywintypes
         except ImportError:
         else:
             return 0
 
+    @staticmethod
+    def _compound_name(username, service):
+        return u'%(username)s@%(service)s' % vars()
+
     def get_password(self, service, username):
+        # first attempt to get the password under the service name
+        res = self._get_password(service)
+        if not res or res['UserName'] != username:
+            # It wasn't found so attempt to get it with the compound name
+            res = self._get_password(self._compound_name(username, service))
+        if not res:
+            return None
+        blob = res['CredentialBlob']
+        return blob.decode('utf-16')
+
+    def _get_password(self, target):
         try:
-            blob = self.win32cred.CredRead(Type=self.win32cred.CRED_TYPE_GENERIC,
-                                           TargetName=service)['CredentialBlob']
+            res = self.win32cred.CredRead(
+                Type=self.win32cred.CRED_TYPE_GENERIC,
+                TargetName=target,
+            )
         except self.pywintypes.error, e:
-            if e[:2] == (1168, 'CredRead'):
+            if e.winerror == 1168 and e.funcname == 'CredRead': # not found
                 return None
             raise
-        return blob.decode("utf16")
+        return res
 
     def set_password(self, service, username, password):
+        existing_pw = self._get_password(service)
+        if existing_pw:
+            # resave the existing password using a compound target
+            existing_username = existing_pw['UserName']
+            target = self._compound_name(existing_username, service)
+            self._set_password(target, existing_username,
+                existing_pw['CredentialBlob'].decode('utf-16'))
+        self._set_password(service, username, unicode(password))
+
+    def _set_password(self, target, username, password):
         credential = dict(Type=self.win32cred.CRED_TYPE_GENERIC,
-                          TargetName=service,
+                          TargetName=target,
                           UserName=username,
-                          CredentialBlob=unicode(password),
+                          CredentialBlob=password,
                           Comment="Stored using python-keyring",
                           Persist=self.win32cred.CRED_PERSIST_ENTERPRISE)
         self.win32cred.CredWrite(credential, 0)
 
     def delete_password(self, service, username):
+        compound = self._compound_name(username, service)
+        for target in service, compound:
+            existing_pw = self._get_password(target)
+            if existing_pw and existing_pw['UserName'] == username:
+                self._delete_password(target)
+
+    def _delete_password(self, target):
         self.win32cred.CredDelete(
             Type=self.win32cred.CRED_TYPE_GENERIC,
-            TargetName=service,
+            TargetName=target,
         )
 
 class Win32CryptoRegistry(KeyringBackend):
             hkey = OpenKey(HKEY_CURRENT_USER, key)
             password_base64 = QueryValueEx(hkey, username)[0]
             # decode with base64
-            password_encrypted = password_base64.decode("base64")
+            password_encrypted = base64.decodestring(password_base64)
             # decrypted the password
             password = self.crypt_handler.decrypt(password_encrypted)
         except EnvironmentError:
             password = None
         return password
 
-
     def set_password(self, service, username, password):
         """Write the password to the registry
         """
         # encrypt the password
         password_encrypted = self.crypt_handler.encrypt(password)
         # encode with base64
-        password_base64 = password_encrypted.encode("base64")
+        password_base64 = base64.encodestring(password_encrypted)
 
         # store the password
         from _winreg import HKEY_CURRENT_USER, CreateKey, SetValueEx, REG_SZ
         return None
     major, minor, build, platform, text = sys.getwindowsversion()
     try:
-        import pywintypes, win32cred
+        import pywintypes
+        import win32cred
         if (major, minor) >= (5, 1):
             # recommend for windows xp+
             return 'cred'
     """
     global _all_keyring
     if _all_keyring is None:
-        _all_keyring = [ OSXKeychain(), GnomeKeyring(), KDEKWallet(),
-                         CryptedFileKeyring(), UncryptedFileKeyring(),
-                         Win32CryptoKeyring(), Win32CryptoRegistry(),
-                         WinVaultKeyring(), SecretServiceKeyring() ]
+        _all_keyring = [OSXKeychain(), GnomeKeyring(), KDEKWallet(),
+                        CryptedFileKeyring(), UncryptedFileKeyring(),
+                        Win32CryptoKeyring(), Win32CryptoRegistry(),
+                        WinVaultKeyring(), SecretServiceKeyring()]
     return _all_keyring
-

File keyring/backends/osx_keychain.py

 #!/usr/bin/python
 
 import sys
+import subprocess
+import re
+import binascii
+
 if sys.platform != 'darwin':
     raise ImportError('Mac OS X only module')
 
-from ctypes import CDLL, CFUNCTYPE, c_void_p, c_uint32, \
-    c_int32, c_char_p, byref, POINTER, memmove, create_string_buffer
-
-# Types
-
-SecKeychainRef     = c_void_p
-SecKeychainItemRef = c_void_p
-OSStatus           = c_int32
-
-# Constants
-
-errSecSuccess                = 0
-errSecUnimplemented          = -4
-errSecParam                  = -50
-errSecAllocate               = -108
-
-errSecNotAvailable           = -25291
-errSecReadOnly               = -25292
-errSecAuthFailed             = -25293
-errSecNoSuchKeychain         = -25294
-errSecInvalidKeychain        = -25295
-errSecDuplicateKeychain      = -25296
-errSecDuplicateCallback      = -25297
-errSecInvalidCallback        = -25298
-errSecDuplicateItem          = -25299
-errSecItemNotFound           = -25300
-errSecBufferTooSmall         = -25301
-errSecDataTooLarge           = -25302
-errSecNoSuchAttr             = -25303
-errSecInvalidItemRef         = -25304
-errSecInvalidSearchRef       = -25305
-errSecNoSuchClass            = -25306
-errSecNoDefaultKeychain      = -25307
-errSecInteractionNotAllowed  = -25308
-errSecReadOnlyAttr           = -25309
-errSecWrongSecVersion        = -25310
-errSecKeySizeNotAllowed      = -25311
-errSecNoStorageModule        = -25312
-errSecNoCertificateModule    = -25313
-errSecNoPolicyModule         = -25314
-errSecInteractionRequired    = -25315
-errSecDataNotAvailable       = -25316
-errSecDataNotModifiable      = -25317
-errSecCreateChainFailed      = -25318
-errSecInvalidPrefsDomain     = -25319
-
-errSecACLNotSimple           = -25240
-errSecPolicyNotFound         = -25241
-errSecInvalidTrustSetting    = -25242
-errSecNoAccessForItem        = -25243
-errSecInvalidOwnerEdit       = -25244
-errSecTrustNotAvailable      = -25245
-errSecUnsupportedFormat      = -25256
-errSecUnknownFormat          = -25257
-errSecKeyIsSensitive         = -25258
-errSecMultiplePrivKeys       = -25259
-errSecPassphraseRequired     = -25260
-errSecInvalidPasswordRef     = -25261
-errSecInvalidTrustSettings   = -25262
-errSecNoTrustSettings        = -25263
-errSecPkcs12VerifyFailure    = -25264
-
-errSecDecode                 = -26275
-
-# Functions
-
-_dll = CDLL('/System/Library/Frameworks/Security.framework/Versions/A/Security')
-_core = CDLL('/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices')
-
-SecKeychainOpen = CFUNCTYPE(OSStatus, c_char_p, POINTER(SecKeychainRef))(('SecKeychainOpen', _dll))
-SecKeychainFindGenericPassword = CFUNCTYPE(OSStatus, SecKeychainRef, c_uint32,
-                                           c_char_p, c_uint32, c_char_p,
-                                           POINTER(c_uint32), POINTER(c_void_p),
-                                           POINTER(SecKeychainItemRef))(('SecKeychainFindGenericPassword', _dll))
-SecKeychainAddGenericPassword = CFUNCTYPE(OSStatus, SecKeychainRef, c_uint32, c_char_p,
-                                          c_uint32, c_char_p, c_uint32,
-                                          c_char_p, POINTER(SecKeychainItemRef))(('SecKeychainAddGenericPassword', _dll))
-SecKeychainItemModifyAttributesAndData = CFUNCTYPE(OSStatus, SecKeychainItemRef, c_void_p, c_uint32, c_void_p)(('SecKeychainItemModifyAttributesAndData', _dll))
-SecKeychainItemFreeContent = CFUNCTYPE(OSStatus, c_void_p, c_void_p)(('SecKeychainItemFreeContent', _dll))
-
 def password_set(realmstring, username, password):
     if username is None:
         username = ''
+    try:
+        # set up the call for security.
+        call = subprocess.Popen([
+                'security',
+                'add-generic-password',
+                '-a',
+                username,
+                '-s',
+                realmstring,
+                '-w',
+                password,
+                '-U'
+            ],
+            stderr = subprocess.PIPE,
+            stdout = subprocess.PIPE,
+        )
+        code = call.wait()
+        # check return code.
+        if code is not 0:
+            raise OSError('Can\'t store password in keychain')
+    except:
+        raise OSError("Can't store password in keychain")
 
-    keychain = SecKeychainRef()
-    if SecKeychainOpen('login.keychain', byref(keychain)):
-        raise OSError("Can't access the login keychain")
-
-    try:
-        item = SecKeychainItemRef()
-        status = SecKeychainFindGenericPassword(keychain, len(realmstring), realmstring, len(username), username, None, None, byref(item))
-        if status:
-            if status == errSecItemNotFound:
-                status = SecKeychainAddGenericPassword(keychain, len(realmstring), realmstring, len(username), username, len(password), password, None)
-        else:
-            status = SecKeychainItemModifyAttributesAndData(item, None, len(password), password)
-            _core.CFRelease(item)
-
-        if status:
-            raise OSError("Can't store password in keychain")
-    finally:
-        _core.CFRelease(keychain)
 
 def password_get(realmstring, username):
     if username is None:
         username = ''
-
-    keychain = SecKeychainRef()
-    if SecKeychainOpen('login.keychain', byref(keychain)):
-        raise OSError("Can't access the login keychain")
-
     try:
-        length = c_uint32()
-        data = c_void_p()
-        status = SecKeychainFindGenericPassword(keychain, len(realmstring), realmstring,
-                                                len(username), username,
-                                                byref(length), byref(data), None)
-        if status == 0:
-            password = create_string_buffer(length.value)
-            memmove(password, data.value, length.value)
-            password = password.raw
-            SecKeychainItemFreeContent(None, data)
-        elif status == errSecItemNotFound:
-            password = None
-        else:
+        # set up the call to security.
+        call = subprocess.Popen([
+                'security',
+                'find-generic-password',
+                '-g',
+                '-a',
+                username,
+                '-s',
+                realmstring
+            ],
+            stderr = subprocess.PIPE,
+            stdout = subprocess.PIPE,
+        )
+        code = call.wait()
+        if code is not 0:
             raise OSError("Can't fetch password from system")
-        return password
-    finally:
-        _core.CFRelease(keychain)
-
+        output = call.stderr.readlines()[0]
+        # check for empty password.
+        if output == 'password: \n':
+            return ''
+        # search for special password pattern.
+        matches = re.search('password:(?P<hex>.*?)"(?P<pw>.*)"', output)
+        if matches:
+            hex = matches.group('hex').strip()
+            pw = matches.group('pw')
+            if hex:
+                # it's a weird hex password, decode it.
+                return binascii.unhexlify(hex[2:])
+            else:
+                # it's a normal password, send it back.
+                return pw
+        # nothing was found, it doesn't exist.
+        return None
+    except:
+        raise OSError("Can't fetch password from system")

File keyring/cli.py

 """Simple command line interface to get/set password from a keyring"""
 
 import getpass
-import optparse
+from optparse import OptionParser
 import sys
 
 import keyring
 import keyring.core
 
 
-def input_password(prompt):
-    """Ask for a password to the user.
+class CommandLineTool(object):
+    def __init__(self):
+        self.parser = OptionParser(usage="%prog [get|set] SERVICE USERNAME")
+        self.parser.add_option("-p", "--keyring-path",
+                               dest="keyring_path", default=None,
+                               help="Path to the keyring backend")
+        self.parser.add_option("-b", "--keyring-backend",
+                               dest="keyring_backend", default=None,
+                               help="Name of the keyring backend")
 
-    This mostly exists to ease the testing process.
-    """
+    def run(self, argv):
+        opts, args = self.parser.parse_args(argv)
 
-    return getpass.getpass(prompt)
+        try:
+            kind, service, username = args
+        except ValueError:
+            if len(args) == 0:
+                # Be nice with the user if he just tries to launch the tool
+                self.parser.print_help()
+                return 1
+            else:
+                self.parser.error("Wrong number of arguments")
 
+        if opts.keyring_backend is not None:
+            try:
+                backend = keyring.core.load_keyring(opts.keyring_path,
+                                                    opts.keyring_backend)
+                keyring.set_keyring(backend)
+            except Exception, e:
+                # Tons of things can go wrong here:
+                #   ImportError when using "fjkljfljkl"
+                #   AttributeError when using "os.path.bar"
+                #   TypeError when using "__builtins__.str"
+                # So, we play on the safe side, and catch everything.
+                self.parser.error("Unable to load specified keyring: %s" % e)
 
-def output_password(password):
-    """Output the password to the user.
 
-    This mostly exists to ease the testing process.
-    """
+        if kind == 'get':
+            password = keyring.get_password(service, username)
+            if password is None:
+                return 1
 
-    print password
+            self.output_password(password)
+            return 0
+
+        elif kind == 'set':
+            password = self.input_password("Password for '%s' in '%s': " %
+                                           (username, service))
+            keyring.set_password(service, username, password)
+            return 0
+
+        else:
+            self.parser.error("You can only 'get' or 'set' a password.")
+            pass
+
+    def input_password(self, prompt):
+        """Ask for a password to the user.
+
+        This mostly exists to ease the testing process.
+        """
+
+        return getpass.getpass(prompt)
+
+
+    def output_password(self, password):
+        """Output the password to the user.
+
+        This mostly exists to ease the testing process.
+        """
+
+        print password
 
 
 def main(argv=None):
     """Main command line interface."""
 
-    parser = optparse.OptionParser(usage="%prog [get|set] SERVICE USERNAME")
-    parser.add_option("-p", "--keyring-path", dest="keyring_path", default=None,
-                      help="Path to the keyring backend")
-    parser.add_option("-b", "--keyring-backend", dest="keyring_backend", default=None,
-                      help="Name of the keyring backend")
 
     if argv is None:
         argv = sys.argv[1:]
 
-    opts, args = parser.parse_args(argv)
-
-    try:
-        kind, service, username = args
-    except ValueError:
-        if len(args) == 0:
-            # Be nice with the user if he just tries to launch the tool
-            parser.print_help()
-            return 1
-        else:
-            parser.error("Wrong number of arguments")
-
-    if opts.keyring_backend is not None:
-        try:
-            backend = keyring.core.load_keyring(opts.keyring_path, opts.keyring_backend)
-            keyring.set_keyring(backend)
-        except Exception, e:
-            # Tons of things can go wrong here:
-            #   ImportError when using "fjkljfljkl"
-            #   AttributeError when using "os.path.bar"
-            #   TypeError when using "__builtins__.str"
-            # So, we play on the safe side, and catch everything.
-            parser.error("Unable to load specified keyring: %s" % e)
-
-
-    if kind == 'get':
-        password = keyring.get_password(service, username)
-        if password is None:
-            return 1
-
-        output_password(password)
-        return 0
-
-    elif kind == 'set':
-        password = input_password("Password for '%s' in '%s': " %
-                                  (username, service))
-        keyring.set_password(service, username, password)
-        return 0
-
-    else:
-        parser.error("You can only 'get' or 'set' a password.")
+    cli = CommandLineTool()
+    return cli.run(argv)
 
 
 if __name__ == '__main__':

File keyring/core.py

 Created by Kang Zhang on 2009-07-09
 """
 import os
-import ConfigParser
+try:
+    import configparser as config_parser
+except ImportError:
+    import ConfigParser as config_parser
 import imp
 import sys
 
 from keyring import logger
 from keyring import backend
+from keyring.util import platform
+from keyring.util import loc_compat
+
 
 def set_keyring(keyring):
     """Set current keyring backend.
 
         keyrings = backend.get_all_keyring()
         # rank according to the supported result
-        keyrings.sort(lambda x, y: y.supported() - x.supported())
+        keyrings.sort(key = lambda x: -x.supported())
         # get the most recommended one
         keyring = keyrings[0]
 
         module = load_module(keyring_name, sys.path+[keyring_path])
 
     keyring_class = keyring_name.split('.')[-1].strip()
-    exec  "keyring_temp = module." + keyring_class + "() " in locals()
+    keyring_temp = getattr(module, keyring_class)()
 
     return keyring_temp
 
     """
     keyring = None
 
-    # search from current working directory and the home folder
-    keyring_cfg_list = [os.path.join(os.getcwd(), "keyringrc.cfg"),
-                        os.path.join(os.path.expanduser("~"), "keyringrc.cfg")]
+    filename = 'keyringrc.cfg'
+
+    local_path = os.path.join(os.getcwd(), filename)
+    legacy_path = os.path.join(os.path.expanduser("~"), filename)
+    config_path = os.path.join(platform.data_root(), filename)
+    loc_compat.relocate_file(legacy_path, config_path)
+
+    # search from current working directory and the data root
+    keyring_cfg_candidates = [local_path, config_path]
 
     # initialize the keyring_config with the first detected config file
     keyring_cfg = None
-    for path in keyring_cfg_list:
+    for path in keyring_cfg_candidates:
         keyring_cfg = path
         if os.path.exists(path):
             break
 
     if os.path.exists(keyring_cfg):
-        config = ConfigParser.RawConfigParser()
+        config = config_parser.RawConfigParser()
         config.read(keyring_cfg)
         # load the keyring-path option
         try:
                 keyring_path = config.get("backend", "keyring-path").strip()
             else:
                 keyring_path = None
-        except ConfigParser.NoOptionError:
+        except config_parser.NoOptionError:
             keyring_path = None
 
         # load the keyring class name, and then load this keyring
             if config.has_section("backend"):
                 keyring_name = config.get("backend", "default-keyring").strip()
             else:
-                raise ConfigParser.NoOptionError('backend', 'default-keyring')
+                raise config_parser.NoOptionError('backend', 'default-keyring')
 
             keyring = load_keyring(keyring_path, keyring_name)
-        except (ConfigParser.NoOptionError, ImportError):
+        except (config_parser.NoOptionError, ImportError):
             logger.warning("Keyring config file contains incorrect values.\n" +
                            "Config file: %s" % keyring_cfg)
 

File keyring/http.py

     handlers = [urllib2.HTTPBasicAuthHandler(PasswordMgr())]
     urllib2.install_opener(handlers)
     urllib2.urlopen(...)
-    
+
 This will prompt for a password if one is required and isn't already
 in the keyring. Then, it adds it to the keyring for subsequent use.
 """
         return getpass.getuser()
 
     def add_password(self, realm, authuri, password):
-        keyring.set_password(realm, user, passwd)
+        user = self.get_username(realm, authuri)
+        keyring.set_password(realm, user, password)
 
     def find_user_password(self, realm, authuri):
         user = self.get_username(realm, authuri)
-        passwd = keyring.get_password(realm, user)
-        if passwd is None:
+        password = keyring.get_password(realm, user)
+        if password is None:
             prompt = 'password for %(user)s@%(realm)s for '\
                 '%(authuri)s: ' % vars()
-            passwd = getpass.getpass(prompt)
-            keyring.set_password(realm, user, passwd)
-        return user, passwd
+            password = getpass.getpass(prompt)
+            keyring.set_password(realm, user, password)
+        return user, password
+
+    def clear_password(self, realm, authuri):
+        user = self.get_username(realm, authuri)
+        keyring.delete_password(realm, user)

File keyring/tests/test_backend.py

 created by Kang Zhang 2009-07-14
 """
 
-
-import commands
 import contextlib
 import os
 import random
 import string
 import sys
+import tempfile
 import types
-import unittest
+
+try:
+    # Python < 2.7 annd Python >= 3.0 < 3.1
+    import unittest2 as unittest
+except ImportError:
+    import unittest
 
 import keyring.backend
 from keyring.backend import PasswordSetError
         result += random.choice(source)
     return result
 
-def backup(file):
-    """Backup the file as file.bak
-    """
-    commands.getoutput( "mv %s{,.bak}" % file )
 
-def restore(file):
-    """Restore the file from file.bak
-    """
-    commands.getoutput( "mv %s{.bak,}" % file )
+def is_win32_crypto_supported():
+    try:
+        from keyring.backends import win32_crypto
+        if sys.platform in ['win32'] and sys.getwindowsversion()[-2] == 2:
+            return True
+    except ImportError:
+        pass
+    return False
 
-class BackendBasicTestCase(unittest.TestCase):
+def is_osx_keychain_supported():
+    return sys.platform in ('mac','darwin')
+
+def is_kwallet_supported():
+    supported = keyring.backend.KDEKWallet().supported()
+    if supported == -1:
+        return False
+    return True
+
+def is_crypto_supported():
+    try:
+        from Crypto.Cipher import AES
+        import crypt
+    except ImportError:
+        return False
+    return True
+
+def is_gnomekeyring_supported():
+    supported = keyring.backend.GnomeKeyring().supported()
+    if supported == -1:
+        return False
+    return True
+
+def is_qt4_supported():
+    try:
+        from PyQt4.QtGui import QApplication
+    except ImportError:
+        return False
+    return True
+
+def is_winvault_supported():
+    try:
+        from keyring.backend import WinVaultKeyring
+        if sys.platform in ['win32'] and sys.getwindowsversion().major >= 6:
+            return True
+    except ImportError:
+        pass
+    return False
+
+
+class BackendBasicTests(object):
     """Test for the keyring's basic funtions. password_set and password_get
     """
 
-    __test__ = False
-
     def setUp(self):
         self.keyring = self.init_keyring()
         self.credentials_created = set()
     def check_set_get(self, service, username, password):
         keyring = self.keyring
 
-        if self.supported() == -1: # skip the unsupported keyring
-            return
-
         # for the non-existent password
         self.assertEqual(keyring.get_password(service, username), None)
 
         multiple users. This test exercises that test for each of the
         backends.
         """
-        if self.supported() == -1: # skip the unsupported keyring
-            return
 
         keyring = self.keyring
         self.set_password('service1', 'user1', 'password1')
         self.assertEqual(keyring.get_password('service1', 'user1'),
             'password1')
 
-    def supported(self):
-        """Return the correct value for supported.
-        """
-        return -1
-
-    def test_supported(self):
-        """Test the keyring's supported value.
-        """
-        self.assertEqual(self.keyring.supported(), self.supported())
-
-class OSXKeychainTestCase(BackendBasicTestCase):
-    __test__ = True
+@unittest.skipUnless(is_osx_keychain_supported(),
+                     "Need OS X")
+class OSXKeychainTestCase(BackendBasicTests, unittest.TestCase):
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing OSXKeychain, following password prompts are for this keyring"
         return keyring.backend.OSXKeychain()
 
-    def supported(self):
-        if sys.platform in ('mac','darwin'):
-            return 1
-        return -1
 
-class GnomeKeyringTestCase(BackendBasicTestCase):
-    __test__ = True
+@unittest.skipUnless(is_gnomekeyring_supported(),
+                     "Need GnomeKeyring")
+class GnomeKeyringTestCase(BackendBasicTests, unittest.TestCase):
 
     def environ(self):
         return dict(GNOME_KEYRING_CONTROL='1',
                     DBUS_SESSION_BUS_ADDRESS='1')
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing GnomeKeyring, following password prompts are for this keyring"
-        return keyring.backend.GnomeKeyring()
+        k = keyring.backend.GnomeKeyring()
+
+        # Store passwords in the session (in-memory) keyring for the tests. This
+        # is going to be automatically cleared when the user logoff.
+        k.KEYRING_NAME = 'session'
+
+        return k
 
     def test_supported(self):
         with ImportBlesser('gnomekeyring'):
                 self.assertEqual(0, self.keyring.supported())
 
 
-class KDEKWalletTestCase(BackendBasicTestCase):
-    __test__ = True
+@unittest.skipUnless(is_kwallet_supported(),
+                     "Need KWallet")
+class KDEKWalletTestCase(BackendBasicTests, unittest.TestCase):
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing KDEKWallet, following password prompts are for this keyring"
         return keyring.backend.KDEKWallet()
 
-    def supported(self):
-        return self.keyring.supported()
-
 
 class UnOpenableKWallet(object):
     """A module-like object used to test KDE wallet fall-back."""
             None)
 
 
+@unittest.skipUnless(is_kwallet_supported() and
+                     is_qt4_supported(),
+                     "Need KWallet and Qt4")
 class KDEKWalletInQApplication(unittest.TestCase):
-
-
     def test_QApplication(self):
         try:
             from PyKDE4.kdeui import KWallet
             return
 
         app = QApplication([])
-        wallet=keyring.backend.open_kwallet()
-        self.assertTrue(isinstance(wallet,KWallet.Wallet),msg="The object wallet should be type<KWallet.Wallet> but it is: %s"%repr(wallet))
+        wallet = keyring.backend.open_kwallet()
+        self.assertTrue(isinstance(wallet, KWallet.Wallet),
+                        msg="The object wallet should be type "
+                        "<KWallet.Wallet> but it is: %s" % repr(wallet))
         app.exit()
 
 
-class FileKeyringTestCase(BackendBasicTestCase):
-    __test__ = False
+class FileKeyringTests(BackendBasicTests):
 
     def setUp(self):
-        """Backup the file before the test
-        """
-        super(FileKeyringTestCase, self).setUp()
-
-        self.file_path = os.path.join(os.path.expanduser("~"),
-            self.keyring.filename())
-        backup(self.file_path)
+        super(FileKeyringTests, self).setUp()
+        self.keyring = self.init_keyring()
+        self.keyring.file_path = self.tmp_keyring_file = tempfile.mktemp()
 
     def tearDown(self):
-        """Restore the keyring file.
-        """
-        restore(self.file_path)
+        try:
+            os.unlink(self.tmp_keyring_file)
+        except OSError, e:
+            if e.errno != 2: # No such file or directory
+                raise
 
     def test_encrypt_decrypt(self):
-        if self.supported() == -1: # skip the unsupported platform
-            return
+        password = random_string(20)
+        # keyring.encrypt expects bytes
+        password = password.encode('utf-8')
+        encrypted = self.keyring.encrypt(password)
 
-        password = random_string(20)
-        encyrpted = self.keyring.encrypt(password)
+        self.assertEqual(password, self.keyring.decrypt(encrypted))
 
-        self.assertEqual(password, self.keyring.decrypt(encyrpted))
 
-class UncryptedFileKeyringTestCase(FileKeyringTestCase):
-    __test__ = True
+class UncryptedFileKeyringTestCase(FileKeyringTests, unittest.TestCase):
+
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing UnecryptedFile, following password prompts are for this keyring"
         return keyring.backend.UncryptedFileKeyring()
 
-    def supported(self):
-        return 0
 
-class CryptedFileKeyringTestCase(FileKeyringTestCase):
-    __test__ = True
+@unittest.skipUnless(is_crypto_supported(),
+                     "Need Crypto module")
+class CryptedFileKeyringTestCase(FileKeyringTests, unittest.TestCase):
+
+    def setUp(self):
+        super(self.__class__, self).setUp()
+        self.keyring._getpass = lambda *args, **kwargs: "abcdef"
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing CryptedFile, following password prompts are for this keyring"
         return keyring.backend.CryptedFileKeyring()
 
-    def supported(self):
-        try:
-            from Crypto.Cipher import AES
-            import crypt
-            return 0
-        except ImportError:
-            pass
-        return -1
 
-class Win32CryptoKeyringTestCase(FileKeyringTestCase):
-    __test__ = True
+@unittest.skipUnless(is_win32_crypto_supported(),
+                     "Need Windows")
+class Win32CryptoKeyringTestCase(FileKeyringTests, unittest.TestCase):
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing Win32Crypto, following password prompts are for this keyring"
         return keyring.backend.Win32CryptoKeyring()
 
-    def supported(self):
-        try:
-            from keyring.backends import win32_crypto
-            if sys.platform in ['win32'] and sys.getwindowsversion()[-2] == 2:
-                return 1
-        except ImportError:
-            pass
-        return -1
 
-class WinVaultKeyringTestCase(BackendBasicTestCase):
-
+@unittest.skipUnless(is_winvault_supported(),
+                     "Need WinVault")
+class WinVaultKeyringTestCase(BackendBasicTests, unittest.TestCase):
     def tearDown(self):
         # clean up any credentials created
         for cred in self.credentials_created:
                 print >> sys.stderr, e
 
     def init_keyring(self):
-        print >> sys.stderr, "Testing WinVault, following password prompts are for this keyring"
         return keyring.backend.WinVaultKeyring()
 
-    def supported(self):
-        try:
-            from keyring.backend import WinVaultKeyring
-            if sys.platform in ['win32'] and sys.getwindowsversion().major >= 6:
-                return 1
-        except ImportError:
-            pass
-        return -1
-
 class SecretServiceKeyringTestCase(BackendBasicTestCase):
     __test__ = True
 

File keyring/tests/test_cli.py

 class CommandLineTestCase(unittest.TestCase):
     def setUp(self):
         self.old_keyring = keyring.get_keyring()
-        self.old_input_password = cli.input_password
-        self.old_output_password = cli.output_password
+
+        self.cli = cli.CommandLineTool()
+        self.cli.input_password = self.return_password
+        self.cli.output_password = self.save_password
+        self.cli.parser.error = self.mock_error
+        self.cli.parser.print_help = lambda: None
 
         keyring.set_keyring(SimpleKeyring())
+
         self.password = ""
         self.password_returned = None
-        cli.input_password = self.return_password
-        cli.output_password = self.save_password
+        self.last_error = None
 
     def tearDown(self):
         keyring.set_keyring(self.old_keyring)
-        cli.input_password = self.old_input_password
-        cli.output_password = self.old_output_password
 
     def return_password(self, *args, **kwargs):
         return self.password
     def save_password(self, password):
         self.password_returned = password
 
+    def mock_error(self, error):
+        self.last_error = error
+        raise SystemExit()
 
     def test_wrong_arguments(self):
-        self.assertEqual(1, cli.main([]))
+        self.assertEqual(1, self.cli.run([]))
 
-        self.assertRaises(SystemExit, cli.main, ["get"])
-        self.assertRaises(SystemExit, cli.main, ["get", "foo"])
-        self.assertRaises(SystemExit, cli.main, ["get", "foo", "bar", "baz"])
+        self.assertRaises(SystemExit, self.cli.run, ["get"])
+        self.assertRaises(SystemExit, self.cli.run, ["get", "foo"])
+        self.assertRaises(SystemExit, self.cli.run,
+                          ["get", "foo", "bar", "baz"])
 
-        self.assertRaises(SystemExit, cli.main, ["set"])
-        self.assertRaises(SystemExit, cli.main, ["set", "foo"])
-        self.assertRaises(SystemExit, cli.main, ["set", "foo", "bar", "baz"])
+        self.assertRaises(SystemExit, self.cli.run, ["set"])
+        self.assertRaises(SystemExit, self.cli.run, ["set", "foo"])
+        self.assertRaises(SystemExit, self.cli.run,
+                          ["set", "foo", "bar", "baz"])
 
-        self.assertRaises(SystemExit, cli.main, ["foo", "bar", "baz"])
+        self.assertRaises(SystemExit, self.cli.run, ["foo", "bar", "baz"])
 
     def test_get_unexistent_password(self):
-        self.assertEqual(1, cli.main(["get", "foo", "bar"]))
+        self.assertEqual(1, self.cli.run(["get", "foo", "bar"]))
         self.assertEqual(None, self.password_returned)
 
     def test_set_and_get_password(self):
         self.password = "plop"
-        self.assertEqual(0, cli.main(["set", "foo", "bar"]))
-        self.assertEqual(0, cli.main(["get", "foo", "bar"]))
+        self.assertEqual(0, self.cli.run(["set", "foo", "bar"]))
+        self.assertEqual(0, self.cli.run(["get", "foo", "bar"]))
         self.assertEqual("plop", self.password_returned)
 
     def test_load_builtin_backend(self):
-        self.assertEqual(1, cli.main(["get",
-                                      "-b", "keyring.backend.UncryptedFileKeyring",
-                                      "foo", "bar"]))
+        self.assertEqual(1, self.cli.run([
+            "get",
+            "-b", "keyring.backend.UncryptedFileKeyring",
+            "foo", "bar"]))
         backend = keyring.get_keyring()
         self.assertTrue(isinstance(backend,
                                    keyring.backend.UncryptedFileKeyring))
 
     def test_load_specific_backend_with_path(self):
         keyring_path = os.path.join(os.path.dirname(keyring.__file__), 'tests')
-        self.assertEqual(0, cli.main(["get",
-                                      "-b", "test_cli.FakeKeyring",
-                                      "-p", keyring_path,
-                                      "foo", "bar"]))
+        self.assertEqual(0, self.cli.run(["get",
+                                          "-b", "test_cli.FakeKeyring",
+                                          "-p", keyring_path,
+                                          "foo", "bar"]))
 
         backend = keyring.get_keyring()
         # Somehow, this doesn't work, because the full dotted name of the class
         self.assertEqual(FakeKeyring.PASSWORD, self.password_returned)
 
     def test_load_wrong_keyrings(self):
-        self.assertRaises(SystemExit, cli.main,
-                         ["get", "foo", "bar",
-                          "-b", "blablabla" # ImportError
-                         ])
-        self.assertRaises(SystemExit, cli.main,
-                         ["get", "foo", "bar",
-                          "-b", "os.path.blabla" # AttributeError
-                         ])
-        self.assertRaises(SystemExit, cli.main,
-                         ["get", "foo", "bar",
-                          "-b", "__builtin__.str" # TypeError
-                         ])
+        self.assertRaises(SystemExit, self.cli.run,
+                          ["get", "foo", "bar",
+                           "-b", "blablabla" # ImportError
+                          ])
+        self.assertRaises(SystemExit, self.cli.run,
+                          ["get", "foo", "bar",
+                           "-b", "os.path.blabla" # AttributeError
+                          ])
+        self.assertRaises(SystemExit, self.cli.run,
+                          ["get", "foo", "bar",
+                           "-b", "__builtin__.str" # TypeError
+                          ])
 
 
 

File keyring/tests/test_core.py

 import sys
 import tempfile
 import shutil
+import subprocess
 
 import keyring.backend
 import keyring.core
+import keyring.util.platform
 
 PASSWORD_TEXT = "This is password"
 PASSWORD_TEXT_2 = "This is password2"
         if personal_renamed:
             os.rename(personal_cfg+'.old', personal_cfg)
 
+class LocationTestCase(unittest.TestCase):
+    legacy_location = os.path.expanduser('~/keyringrc.cfg')
+    new_location = os.path.join(keyring.util.platform.data_root(),
+        'keyringrc.cfg')
+
+    @unittest.skipIf(os.path.exists(legacy_location),
+        "Location test requires non-existence of ~/keyringrc.cfg")
+    @unittest.skipIf(os.path.exists(new_location),
+        "Location test requires non-existence of %(new_location)s"
+        % vars())
+    def test_moves_compat(self):
+        """
+        When starting the keyring module and ~/keyringrc.cfg exists, it
+        should be moved and the user should be informed that it was
+        moved.
+        """
+        # create the legacy config
+        with open(self.legacy_location, 'w') as f:
+            f.write('[test config]\n')
+
+        # invoke load_config in a subprocess
+        cmd = [sys.executable, '-c', 'import keyring.core; keyring.core.load_config()']
+        proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+        stdout, stderr = proc.communicate()
+
+        try:
+            assert not os.path.exists(self.legacy_location)
+            assert os.path.exists(self.new_location)
+            with open(self.new_location) as f:
+                assert 'test config' in f.read()
+        finally:
+            if os.path.exists(self.legacy_location):
+                os.remove(self.legacy_location)
+            if os.path.exists(self.new_location):
+                os.remove(self.new_location)
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(CoreTestCase))
+    suite.addTest(unittest.makeSuite(LocationTestCase))
     return suite
 
 if __name__ == "__main__":

File keyring/tests/test_util.py

 
 
 import unittest
-import os
-import sys
-import tempfile
-import shutil
 
 from keyring.util import escape
 
 
     def check_escape_unescape(self, initial):
         escaped = escape.escape(initial)
-        self.assertTrue(all( c in (escape.LEGAL_CHARS + escape.ESCAPE_CHAR)
-                             for c in escaped))
+        self.assertTrue(all(c in (escape.LEGAL_CHARS + '_') for c in escaped))
         unescaped = escape.unescape(escaped)
         self.assertEqual(initial, unescaped)
 
     def test_escape_unescape(self):
         self.check_escape_unescape("aaaa")
         self.check_escape_unescape("aaaa bbbb cccc")
-        self.check_escape_unescape(u"Zażółć gęślą jaźń".encode("utf-8"))
+        self.check_escape_unescape(u"Zażółć gęślą jaźń")
         self.check_escape_unescape("(((P{{{{'''---; ;; '\"|%^")
 
+    def test_low_byte(self):
+        """
+        Ensure that encoding low bytes (ordinal less than hex F) encode as
+        as three bytes to avoid ambiguity. For example '\n' (hex A) should
+        encode as '_0A' and not '_A', the latter of which
+        isn't matched by the inverse operation.
+        """
+        self.check_escape_unescape('\n')
+        self.check_escape_unescape('\x000')
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(EscapeTestCase))

File keyring/util/escape.py

 alphanumeric usernames, services, or other values
 """
 
-import string, re
+import string
+import re
+import sys
 
 LEGAL_CHARS = (
     getattr(string, 'letters', None) # Python 2
     or getattr(string, 'ascii_letters') # Python 3
 ) + string.digits
-ESCAPE_CHAR = "_"
+
+ESCAPE_FMT = "_%02X"
+
+def _escape_char(c):
+    "Single char escape. Return the char, escaped if not already legal"
+    if isinstance(c, int):
+        c = unichr(c)
+    return c if c in LEGAL_CHARS else ESCAPE_FMT % ord(c)
 
 def escape(value):
-    """Escapes given value so the result consists of alphanumeric chars and
-    ESCAPE_CHAR only"""
-    def escape_char(c, legal = LEGAL_CHARS):
-        # Single char escape. Either normal char, or _<hexcode>
-        if c in legal:
-            return c
-        else:
-            return "%s%X" % (ESCAPE_CHAR, ord(c))
-    return "".join( escape_char(c) for c in value )
+    """
+    Escapes given string so the result consists of alphanumeric chars and
+    underscore only.
+    """
+    return "".join(_escape_char(c) for c in value.encode('utf-8'))
+
+def _unescape_code(regex_match):
+    ordinal = int(regex_match.group('code'), 16)
+    if sys.version_info >= (3,):
+        return bytes([ordinal])
+    return chr(ordinal)
 
 def unescape(value):
-    """Reverts escape"""
-    re_esc = re.compile("_([0-9A-F]{2})")
-    return re_esc.sub(lambda i: chr(int(i.group(1),16)), value)
+    """
+    Inverse of escape.
+    """
+    re_esc = re.compile(
+        # the pattern must be bytes to operate on bytes
+        ESCAPE_FMT.replace('%02X', '(?P<code>[0-9A-F]{2})').encode('ascii')
+    )
+    return re_esc.sub(_unescape_code, value.encode('ascii')).decode('utf-8')

File keyring/util/loc_compat.py

+import os
+import shutil
+import sys
+
+def relocate_file(old_location, new_location):
+    """
+    keyring 0.8 changes the default location for storage of
+    file-based keyring locations. This function is invoked to move
+    files stored in the old location to the new location.
+
+    TODO: remove this function for keyring 1.0.
+    """
+    if not os.path.exists(old_location):
+        # nothing to do; no legacy file found
+        return
+
+    if os.path.exists(new_location):
+        print >> sys.stderr, ("Password file found in legacy "
+            "location\n  %(old_location)s\nand new location\n"
+            "  %(new_location)s\nOld location will be ignored."
+            % vars())
+        return
+
+    # ensure the storage path exists
+    if not os.path.isdir(os.path.dirname(new_location)):
+        os.makedirs(os.path.dirname(new_location))
+    shutil.move(old_location, new_location)

File keyring/util/platform.py

+import os
+import sys
+
+# While we support Python 2.4, use a convoluted technique to import
+#  platform from the stdlib.
+# With Python 2.5 or later, just do "from __future__ import absolute_import"
+#  and "import platform"
+exec('__import__("platform", globals=dict())')
+platform = sys.modules['platform']
+
+def _data_root_Windows():
+	try:
+		root = os.environ['LOCALAPPDATA']
+	except KeyError:
+		# Windows XP
+		root = os.path.join(os.environ['USERPROFILE'], 'Local Settings')
+	return os.path.join(root, 'Python Keyring')
+
+def _data_root_Linux():
+	"""
+	Use freedesktop.org Base Dir Specfication to determine storage
+	location.
+	"""
+	fallback = os.path.expanduser('~/.local/share')
+	root = os.environ.get('XDG_DATA_HOME', None) or fallback
+	return os.path.join(root, 'python_keyring')
+
+# by default, use Unix convention
+data_root = globals().get('_data_root_' + platform.system(), _data_root_Linux)

File keyring/util/properties.py

+# borrowed from jaraco.util.dictlib
+class NonDataProperty(object):
+	"""Much like the property builtin, but only implements __get__,
+	making it a non-data property, and can be subsequently reset.
+
+	See http://users.rcn.com/python/download/Descriptor.htm for more
+	information.
+
+	>>> class X(object):
+	...   @NonDataProperty
+	...   def foo(self):
+	...     return 3
+	>>> x = X()
+	>>> x.foo
+	3
+	>>> x.foo = 4
+	>>> x.foo
+	4
+	"""
+
+	def __init__(self, fget):
+		assert fget is not None, "fget cannot be none"
+		assert callable(fget), "fget must be callable"
+		self.fget = fget
+
+	def __get__(self, obj, objtype=None):
+		if obj is None:
+			return self
+		return self.fget(obj)

File release.bat

-c:\Python26\Python.exe setup.py register bdist_wininst upload
+#!/usr/bin/env python
+
+"""
+release.py - releases keyring to the cheeseshop
+"""
+
+import subprocess
+import sys
+
+# we only upload a source distribution now as there are no C extensions
+subprocess.Popen([sys.executable, 'setup.py', 'sdist', 'upload'])

File release.sh

-#!/bin/sh
-# build a source distrbution and a binary-one.
-python setup.py register sdist upload
-#python setup.py register bdist upload
 Setup the Keyring Lib for Python.
 """
 
-import sys, os, subprocess
-from distutils.version import StrictVersion
+import sys
+import codecs
 
-def runcmd(cmd, env):
-    p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
-                         stderr=subprocess.PIPE, env=env)
-    out, err = p.communicate()
-    return out, err
+try:
+    import setuptools
+    setup_mod = setuptools
+    "where to find setup()"
+except ImportError:
+    import distutils.core
+    setup_mod = distutils.core
+
+def load(filename):
+    """
+    Read a text file and decode it.
+    """
+    f = codecs.open(filename, encoding='utf-8')
+    try:
+        result = f.read()
+    finally:
+        f.close()
+    if not encodes_as_ascii(result):
+        # see https://bitbucket.org/kang/python-keyring-lib/issue/55
+        raise ValueError("distutils requires ASCII")
+    return result
+
+def encodes_as_ascii(string):
+    try:
+        string.encode('ascii')
+    except UnicodeEncodeError:
+        return False
+    return True
 
 setup_params = dict(
     name = 'keyring',
-    version = "0.7",
+    version = "0.8.2",
     description = "Store and access your passwords safely.",
-    url = "http://home.python-keyring.org/",
+    url = "http://bitbucket.org/kang/python-keyring-lib",
     keywords = "keyring Keychain GnomeKeyring Kwallet password storage",
-    maintainer = "Kang Zhang",
-    maintainer_email = "jobo.zh@gmail.com",
+    author = "Kang Zhang",
+    author_email = "jobo.zh@gmail.com",
+    maintainer = 'Jason R. Coombs',
+    maintainer_email = 'jaraco@jaraco.com',
     license="PSF",
-    long_description = open('README').read() + open('CHANGES.txt').read(),
+    long_description = load('README') + load('CHANGES.txt'),
+    classifiers = [
+        "Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Developers",
+        "Programming Language :: Python :: 2.4",
+        "Programming Language :: Python :: 2.5",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+    ],
     platforms = ["Many"],
     packages = ['keyring', 'keyring.tests', 'keyring.util',
                 'keyring.backends'],
+    extras_require={'test': []},
 )
 
-if sys.version_info >= (3,0):
+
+if sys.version_info >= (3, 0):
     setup_params.update(
         use_2to3=True,
     )
 
+elif sys.version_info < (2, 7) or (
+    sys.version >= (3, 0) and sys.version < (3, 1)):
+    # Provide unittest2 for Python which doesn't contain the new unittest module
+    # (appears in Python 2.7 and Python 3.1)
+    setup_params.update(
+        tests_require=['unittest2'],
+    )
+    setup_params['extras_require']['test'].append('unittest2')
+
+
 if __name__ == '__main__':
-    try:
-        from setuptools import setup
-    except ImportError:
-        from distutils.core import setup
-    setup(**setup_params)
+    setup_mod.setup(**setup_params)