Commits

Jason R. Coombs committed 397c61e Merge

Merge

  • Participants
  • Parent commits cc4cf2a, 27aa9a4

Comments (0)

Files changed (7)

 CHANGES
 =======
 
------
-0.7.2
------
+---
+0.8
+---
 
-* changes go here
+* 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

File keyring/backend.py

 
 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, abstractproperty
     @properties.NonDataProperty
     def file_path(self):
         """
-        The path to the file where passwords are stored.
+        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(os.path.expanduser('~'), self.filename)
+        return os.path.join(keyring.util.platform.data_root(), self.filename)
 
     @abstractproperty
     def filename(self):
         """
         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)
 
     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)
 
         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)
 

File keyring/core.py

 
 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.
     """
     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

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(CoreTestCase, LocationTestCase))
     return suite
 
 if __name__ == "__main__":

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
+
+def _data_root_win32():
+	return os.path.join(os.environ['LOCALAPPDATA'], 'Python Keyring')
+
+def _data_root_linux2():
+	"""
+	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_' + sys.platform, _data_root_linux2)
 
 setup_params = dict(
     name = 'keyring',
-    version = "0.7.2",
+    version = "0.8",
     description = "Store and access your passwords safely.",
     url = "http://bitbucket.org/kang/python-keyring-lib",
     keywords = "keyring Keychain GnomeKeyring Kwallet password storage",
 )
 
 
-if sys.version_info >= (3,0):
+if sys.version_info >= (3, 0):
     setup_params.update(
         use_2to3=True,
     )