From bec2674d387b3a72c149d9d9f16aeb6dd12f31ac Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 25 Jan 2015 14:33:04 +0800 Subject: [PATCH 01/13] Added prototype for protected settings. --- i3pystatus/core/settings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index 6d67ae6..85b87ba 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -18,6 +18,7 @@ class SettingsBase: Settings are stored as attributes of self. """ + __PROTECTED_SETTINGS = ["password", "email", "username"] settings = ( ("log_level", "Set to true to log error to .i3pystatus- file"), ) @@ -75,8 +76,25 @@ class SettingsBase: self.logger = logging.getLogger(self.__name__) self.logger.setLevel(self.log_level) + + for setting_name in self.__PROTECTED_SETTINGS: + if hasattr(self, setting_name) and not getattr(self, setting_name): + setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) + if setting: + setattr(self, setting_name, setting) + self.init() + @staticmethod + def get_protected_setting(setting_name): + import getpass + try: + import keyring + except ImportError: + keyring = None + else: + return keyring.get_password(setting_name, getpass.getuser()) + def init(self): """Convenience method which is called after all settings are set From f9fe7653b3128ff223e41dc2a732ba8b1eb62403 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 25 Jan 2015 16:07:31 +0800 Subject: [PATCH 02/13] Added prototype utility for setting protected credentials. --- setting_util.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 setting_util.py diff --git a/setting_util.py b/setting_util.py new file mode 100755 index 0000000..a6da126 --- /dev/null +++ b/setting_util.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +import glob +import inspect +import os +import keyring +import getpass +import sys +import signal +from i3pystatus import Module, SettingsBase +from i3pystatus.core import ClassFinder +from collections import defaultdict, OrderedDict + +try: + input = raw_input +except NameError: + pass + + +def signal_handler(signal, frame): + sys.exit(0) +signal.signal(signal.SIGINT, signal_handler) + + +def get_int_in_range(prompt, _range): + while True: + answer = input(prompt) + try: + n = int(answer.strip()) + if n in _range: + return n + else: + print("Value out of range!") + except ValueError: + print("Invalid input!") + +modules = [os.path.basename(m.replace('.py', '')) + for m in glob.glob(os.path.join(os.path.dirname(__file__), "i3pystatus", "*.py")) + if not os.path.basename(m).startswith('_')] + +protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS +class_finder = ClassFinder(Module) +credential_modules = defaultdict(dict) +for module_name in modules: + try: + module = class_finder.get_module(module_name) + clazz = class_finder.get_class(module) + members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')] + if any([hasattr(clazz, setting) for setting in protected_settings]): + credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members)) + credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) + except ImportError: + continue + +choices = [k for k in credential_modules.keys()] +for idx, module in enumerate(choices, start=1): + print("%s - %s" % (idx, module)) + +index = get_int_in_range("Choose module:\n> ", range(1, len(choices) + 1)) +module_name = choices[index - 1] +module = credential_modules[module_name] + +for idx, setting in enumerate(module['credentials'], start=1): + print("%s - %s" % (idx, setting)) + +choices = module['credentials'] +index = get_int_in_range("Choose setting for %s:\n> " % module_name, range(1, len(choices) + 1)) +setting = choices[index - 1] + +answer = getpass.getpass("Enter value for %s:\n> " % setting) +answer2 = getpass.getpass("Re-enter value\n> ") +if answer == answer2: + key = "%s.%s" % (module['key'], setting) + keyring.set_password(key, getpass.getuser(), answer) + print("%s set!" % setting) +else: + print("Values don't match - nothing set.") From 215b85e431134e6bcccb7912e4933f7c1e2a2a92 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 25 Jan 2015 20:07:37 +0800 Subject: [PATCH 03/13] Added prototype support for custom keyring backends. --- i3pystatus/core/settings.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index 85b87ba..fe60a73 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -2,6 +2,7 @@ from i3pystatus.core.util import KeyConstraintDict from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError import inspect import logging +import getpass class SettingsBase: @@ -19,6 +20,7 @@ class SettingsBase: """ __PROTECTED_SETTINGS = ["password", "email", "username"] + settings = ( ("log_level", "Set to true to log error to .i3pystatus- file"), ) @@ -45,7 +47,6 @@ class SettingsBase: return kwargs def merge_with_parents_settings(): - settings = tuple() # getmro returns base classes according to Method Resolution Order @@ -76,22 +77,27 @@ class SettingsBase: self.logger = logging.getLogger(self.__name__) self.logger.setLevel(self.log_level) + self.set_protected_settings() + self.init() + def set_protected_settings(self): for setting_name in self.__PROTECTED_SETTINGS: if hasattr(self, setting_name) and not getattr(self, setting_name): + print("%s.%s" % (self.__name__, setting_name)) setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) if setting: setattr(self, setting_name, setting) - self.init() + def get_protected_setting(self, setting_name): + # If a custom keyring backend has been defined, use it. + if hasattr(self, 'keyring_backend') and self.keyring_backend: + return self.keyring_backend.get_password(setting_name, getpass.getuser()) - @staticmethod - def get_protected_setting(setting_name): - import getpass + # Otherwise try and use defualt keyring. try: import keyring except ImportError: - keyring = None + pass else: return keyring.get_password(setting_name, getpass.getuser()) From b0d5fdba75ae168403db1b2b2a825713d6a196b8 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 25 Jan 2015 20:08:06 +0800 Subject: [PATCH 04/13] Added example custom keyring backend. --- i3pystatus/core/netrc_backend.py | 27 +++++++++++++++++++++++++++ i3pystatus/core/settings.py | 1 - 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 i3pystatus/core/netrc_backend.py diff --git a/i3pystatus/core/netrc_backend.py b/i3pystatus/core/netrc_backend.py new file mode 100644 index 0000000..342b2e3 --- /dev/null +++ b/i3pystatus/core/netrc_backend.py @@ -0,0 +1,27 @@ +import os + +__author__ = 'facetoe' +from keyring.backend import KeyringBackend + + +# This is an example custom keyring backend. It should probably be somewhere else... +class NetrcBackend(KeyringBackend): + def get_password(self, service, username): + from netrc import netrc + sections = service.split('.') + setting = sections[-1] + if setting == 'password': + key = ".".join(sections[:-1]) + setting_tuple = netrc().authenticators(key) + if setting_tuple: + login, account, password = setting_tuple + return password + + def set_password(self, service, username, password): + raise Exception("Setting password not supported!") + + def priority(cls): + netrc_path = os.path.isfile(os.path.expanduser("~/.netrc")) + if not os.path.isfile(netrc_path): + raise Exception("No .netrc found at: %s" % netrc_path) + return 0.5 diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index fe60a73..4d132ba 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -83,7 +83,6 @@ class SettingsBase: def set_protected_settings(self): for setting_name in self.__PROTECTED_SETTINGS: if hasattr(self, setting_name) and not getattr(self, setting_name): - print("%s.%s" % (self.__name__, setting_name)) setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) if setting: setattr(self, setting_name, setting) From c588181045d91f1d982dea3d7462e68b7931181a Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 1 Feb 2015 08:30:16 +0800 Subject: [PATCH 05/13] Supporting Python2 doesn't make sense. --- setting_util.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/setting_util.py b/setting_util.py index a6da126..b5c941c 100755 --- a/setting_util.py +++ b/setting_util.py @@ -10,12 +10,6 @@ from i3pystatus import Module, SettingsBase from i3pystatus.core import ClassFinder from collections import defaultdict, OrderedDict -try: - input = raw_input -except NameError: - pass - - def signal_handler(signal, frame): sys.exit(0) signal.signal(signal.SIGINT, signal_handler) From 887c45119b9ab6909df81ee78985f937885c1cd7 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 1 Feb 2015 09:15:26 +0800 Subject: [PATCH 06/13] Look for protected settings that are in the required tuple. --- i3pystatus/core/settings.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index 4d132ba..ff8104a 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -6,7 +6,6 @@ import getpass class SettingsBase: - """ Support class for providing a nice and flexible settings interface @@ -48,19 +47,24 @@ class SettingsBase: def merge_with_parents_settings(): settings = tuple() - # getmro returns base classes according to Method Resolution Order for cls in inspect.getmro(self.__class__): if hasattr(cls, "settings"): settings = settings + cls.settings return settings + self.__name__ = "{}.{}".format( + self.__module__, self.__class__.__name__) + settings = merge_with_parents_settings() settings = self.flatten_settings(settings) sm = KeyConstraintDict(settings, self.required) settings_source = get_argument_dict(args, kwargs) + protected = self.get_protected_settings() + settings_source.update(protected) + try: sm.update(settings_source) except KeyError as exc: @@ -72,20 +76,21 @@ class SettingsBase: raise ConfigMissingError( type(self).__name__, missing=exc.keys) from exc - self.__name__ = "{}.{}".format( - self.__module__, self.__class__.__name__) - self.logger = logging.getLogger(self.__name__) self.logger.setLevel(self.log_level) - self.set_protected_settings() self.init() - def set_protected_settings(self): + def get_protected_settings(self): + found_settings = dict() for setting_name in self.__PROTECTED_SETTINGS: - if hasattr(self, setting_name) and not getattr(self, setting_name): + setting = None + if hasattr(self, 'required') and setting_name in getattr(self, 'required'): setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) - if setting: - setattr(self, setting_name, setting) + elif hasattr(self, setting_name) and not getattr(self, setting_name): + setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) + if setting: + found_settings.update({setting_name: setting}) + return found_settings def get_protected_setting(self, setting_name): # If a custom keyring backend has been defined, use it. From 1b8e3fe2e4374757c63e681de34bffa62e4219d0 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 1 Feb 2015 09:19:31 +0800 Subject: [PATCH 07/13] Update setting util to locate modules with protected settings in required tuple. --- setting_util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/setting_util.py b/setting_util.py index b5c941c..97cf663 100755 --- a/setting_util.py +++ b/setting_util.py @@ -42,6 +42,16 @@ for module_name in modules: if any([hasattr(clazz, setting) for setting in protected_settings]): credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members)) credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) + elif hasattr(clazz, 'required'): + protected = [] + required = getattr(clazz, 'required') + for setting in protected_settings: + if setting in required: + protected.append(setting) + if protected: + credential_modules[clazz.__name__]['credentials'] = protected + credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) + except ImportError: continue From 9324f06a3c494c48431c02cbeff5bdb80c1af0f0 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 1 Feb 2015 09:31:13 +0800 Subject: [PATCH 08/13] Added keyring_backend variable. --- i3pystatus/github.py | 1 + i3pystatus/mail/imap.py | 1 + i3pystatus/modsde.py | 1 + i3pystatus/pyload.py | 1 + i3pystatus/reddit.py | 1 + i3pystatus/whosonlocation.py | 1 + 6 files changed, 6 insertions(+) diff --git a/i3pystatus/github.py b/i3pystatus/github.py index a40c306..b5196c3 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -23,6 +23,7 @@ class Github(IntervalModule): password = '' format = '{unread}' interval = 600 + keyring_backend = None on_leftclick = 'open_github' diff --git a/i3pystatus/mail/imap.py b/i3pystatus/mail/imap.py index 94f3b20..2ae58d9 100644 --- a/i3pystatus/mail/imap.py +++ b/i3pystatus/mail/imap.py @@ -20,6 +20,7 @@ class IMAP(Backend): "mailbox", ) required = ("host", "username", "password") + keyring_backend = None port = 993 ssl = True diff --git a/i3pystatus/modsde.py b/i3pystatus/modsde.py index ff5f9b0..f1eae4f 100644 --- a/i3pystatus/modsde.py +++ b/i3pystatus/modsde.py @@ -24,6 +24,7 @@ class ModsDeChecker(IntervalModule): "color", "username", "password" ) required = ("username", "password") + keyring_backend = None color = "#7181fe" offset = 0 diff --git a/i3pystatus/pyload.py b/i3pystatus/pyload.py index f960114..67d77bf 100644 --- a/i3pystatus/pyload.py +++ b/i3pystatus/pyload.py @@ -32,6 +32,7 @@ class pyLoad(IntervalModule): "username", "password" ) required = ("username", "password") + keyring_backend = None address = "http://127.0.0.1:8000" format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s" diff --git a/i3pystatus/reddit.py b/i3pystatus/reddit.py index f448ecd..374555e 100644 --- a/i3pystatus/reddit.py +++ b/i3pystatus/reddit.py @@ -48,6 +48,7 @@ class Reddit(IntervalModule): format = "[{submission_subreddit}] {submission_title} ({submission_domain})" username = "" password = "" + keyring_backend = None subreddit = "" sort_by = "hot" color = "#FFFFFF" diff --git a/i3pystatus/whosonlocation.py b/i3pystatus/whosonlocation.py index dad46dc..6147945 100644 --- a/i3pystatus/whosonlocation.py +++ b/i3pystatus/whosonlocation.py @@ -71,6 +71,7 @@ class WOL(IntervalModule): 'email', 'password' ) + keyring_backend = None color_on_site = '#00FF00' color_off_site = '#ff0000' From 7fa5c10787b8c6d5749879cd1caaad534029d805 Mon Sep 17 00:00:00 2001 From: facetoe Date: Fri, 13 Feb 2015 19:38:58 +0800 Subject: [PATCH 09/13] Added keyring_backend to the settings tuple. --- i3pystatus/core/netrc_backend.py | 2 -- i3pystatus/github.py | 1 + i3pystatus/mail/imap.py | 1 + i3pystatus/modsde.py | 1 + i3pystatus/pyload.py | 3 ++- i3pystatus/reddit.py | 1 + i3pystatus/whosonlocation.py | 2 +- 7 files changed, 7 insertions(+), 4 deletions(-) diff --git a/i3pystatus/core/netrc_backend.py b/i3pystatus/core/netrc_backend.py index 342b2e3..c59ddfa 100644 --- a/i3pystatus/core/netrc_backend.py +++ b/i3pystatus/core/netrc_backend.py @@ -1,6 +1,4 @@ import os - -__author__ = 'facetoe' from keyring.backend import KeyringBackend diff --git a/i3pystatus/github.py b/i3pystatus/github.py index b5196c3..bfcfac8 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -29,6 +29,7 @@ class Github(IntervalModule): settings = ( ('format', 'format string'), + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), ('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'), ("username", ""), ("password", ""), diff --git a/i3pystatus/mail/imap.py b/i3pystatus/mail/imap.py index 2ae58d9..6f91937 100644 --- a/i3pystatus/mail/imap.py +++ b/i3pystatus/mail/imap.py @@ -16,6 +16,7 @@ class IMAP(Backend): settings = ( "host", "port", "username", "password", + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), "ssl", "mailbox", ) diff --git a/i3pystatus/modsde.py b/i3pystatus/modsde.py index f1eae4f..acb3a2d 100644 --- a/i3pystatus/modsde.py +++ b/i3pystatus/modsde.py @@ -20,6 +20,7 @@ class ModsDeChecker(IntervalModule): settings = ( ("format", """Use {unread} as the formatter for number of unread posts"""), + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), ("offset", """subtract number of posts before output"""), "color", "username", "password" ) diff --git a/i3pystatus/pyload.py b/i3pystatus/pyload.py index 67d77bf..a528eab 100644 --- a/i3pystatus/pyload.py +++ b/i3pystatus/pyload.py @@ -29,7 +29,8 @@ class pyLoad(IntervalModule): "format", "captcha_true", "captcha_false", "download_true", "download_false", - "username", "password" + "username", "password", + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), ) required = ("username", "password") keyring_backend = None diff --git a/i3pystatus/reddit.py b/i3pystatus/reddit.py index 374555e..3535fbd 100644 --- a/i3pystatus/reddit.py +++ b/i3pystatus/reddit.py @@ -35,6 +35,7 @@ class Reddit(IntervalModule): ("format", "Format string used for output."), ("username", "Reddit username."), ("password", "Reddit password."), + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), ("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."), ("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."), ("color", "Standard color."), diff --git a/i3pystatus/whosonlocation.py b/i3pystatus/whosonlocation.py index 6147945..e866fc3 100644 --- a/i3pystatus/whosonlocation.py +++ b/i3pystatus/whosonlocation.py @@ -5,7 +5,6 @@ from bs4 import BeautifulSoup class WhosOnLocation(): - email = None password = None session = None @@ -68,6 +67,7 @@ class WOL(IntervalModule): password = None settings = ( + ('keyring_backend', 'alternative keyring backend for retrieving credentials'), 'email', 'password' ) From 61a8669eca86d0c07f8c16fe5717966f638fc63b Mon Sep 17 00:00:00 2001 From: facetoe Date: Sat, 14 Feb 2015 10:07:30 +0800 Subject: [PATCH 10/13] Removed POC NetrcBackend. --- i3pystatus/core/netrc_backend.py | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 i3pystatus/core/netrc_backend.py diff --git a/i3pystatus/core/netrc_backend.py b/i3pystatus/core/netrc_backend.py deleted file mode 100644 index c59ddfa..0000000 --- a/i3pystatus/core/netrc_backend.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -from keyring.backend import KeyringBackend - - -# This is an example custom keyring backend. It should probably be somewhere else... -class NetrcBackend(KeyringBackend): - def get_password(self, service, username): - from netrc import netrc - sections = service.split('.') - setting = sections[-1] - if setting == 'password': - key = ".".join(sections[:-1]) - setting_tuple = netrc().authenticators(key) - if setting_tuple: - login, account, password = setting_tuple - return password - - def set_password(self, service, username, password): - raise Exception("Setting password not supported!") - - def priority(cls): - netrc_path = os.path.isfile(os.path.expanduser("~/.netrc")) - if not os.path.isfile(netrc_path): - raise Exception("No .netrc found at: %s" % netrc_path) - return 0.5 From c051e019591d09942d68a0e3fcfb237c5c84426a Mon Sep 17 00:00:00 2001 From: facetoe Date: Sat, 14 Feb 2015 10:32:32 +0800 Subject: [PATCH 11/13] Added documentation of keyring feature. --- docs/configuration.rst | 10 ++++++++++ docs/module.rst | 3 +++ i3pystatus/core/settings.py | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index 2aa6416..abd4cab 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -122,3 +122,13 @@ Also change your i3wm config to the following: position top workspace_buttons yes } + +Settings that require credentials can utilize the keyring module to keep sensitive information out of config files. +To take advantage of this feature, simply use the setting_util.py script to set the credentials for a module. Once this +is done you can add the module to your config without specifying the credentials, eg: + +:: + + status.register('github') + +i3pystatus will locate and set the credentials during the module loading process. Currently supported credentals are "password", "email" and "username". \ No newline at end of file diff --git a/docs/module.rst b/docs/module.rst index 6b195f3..31be7be 100644 --- a/docs/module.rst +++ b/docs/module.rst @@ -14,6 +14,9 @@ tools for this which make this even easier: periodically. - Settings (already built into above classes) allow you to easily specify user-modifiable attributes of your class for configuration. +- For modules that require credentials, it is recommended to add a + keyring_backend setting to allow users to specify their own backends + for retrieving sensitive credentials. Required settings and default values are also handled. diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index ff8104a..aa78c45 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -93,6 +93,10 @@ class SettingsBase: return found_settings def get_protected_setting(self, setting_name): + """ + Retrieves a protected setting from keyring + :param setting_name: setting_name must be in the format package.module.Class.setting + """ # If a custom keyring backend has been defined, use it. if hasattr(self, 'keyring_backend') and self.keyring_backend: return self.keyring_backend.get_password(setting_name, getpass.getuser()) From cb8f4225bd10f642a8b286657bf8c8d7de3b3150 Mon Sep 17 00:00:00 2001 From: facetoe Date: Mon, 16 Feb 2015 21:09:59 +0800 Subject: [PATCH 12/13] Fixed bug that prevented user-defined keyring being used. --- i3pystatus/core/settings.py | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index aa78c45..9406b82 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -62,7 +62,7 @@ class SettingsBase: sm = KeyConstraintDict(settings, self.required) settings_source = get_argument_dict(args, kwargs) - protected = self.get_protected_settings() + protected = self.get_protected_settings(settings_source) settings_source.update(protected) try: @@ -80,34 +80,43 @@ class SettingsBase: self.logger.setLevel(self.log_level) self.init() - def get_protected_settings(self): + def get_protected_settings(self, settings_source): + """ + Attempt to retrieve protected settings from keyring if they are not already set. + """ + user_backend = settings_source.get('keyring_backend') found_settings = dict() for setting_name in self.__PROTECTED_SETTINGS: - setting = None - if hasattr(self, 'required') and setting_name in getattr(self, 'required'): - setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) - elif hasattr(self, setting_name) and not getattr(self, setting_name): - setting = self.get_protected_setting("%s.%s" % (self.__name__, setting_name)) - if setting: - found_settings.update({setting_name: setting}) + # Nothing to do if the setting is already defined. + if settings_source.get(setting_name): + continue + + setting = None + identifier = "%s.%s" % (self.__name__, setting_name) + if hasattr(self, 'required') and setting_name in getattr(self, 'required'): + setting = self.get_setting_from_keyring(identifier, user_backend) + elif hasattr(self, setting_name): + setting = self.get_setting_from_keyring(identifier, user_backend) + if setting: + found_settings.update({setting_name: setting}) return found_settings - def get_protected_setting(self, setting_name): + def get_setting_from_keyring(self, setting_identifier, keyring_backend=None): """ Retrieves a protected setting from keyring - :param setting_name: setting_name must be in the format package.module.Class.setting + :param setting_identifier: must be in the format package.module.Class.setting """ # If a custom keyring backend has been defined, use it. - if hasattr(self, 'keyring_backend') and self.keyring_backend: - return self.keyring_backend.get_password(setting_name, getpass.getuser()) + if keyring_backend: + return keyring_backend.get_password(setting_identifier, getpass.getuser()) - # Otherwise try and use defualt keyring. + # Otherwise try and use default keyring. try: import keyring except ImportError: pass else: - return keyring.get_password(setting_name, getpass.getuser()) + return keyring.get_password(setting_identifier, getpass.getuser()) def init(self): """Convenience method which is called after all settings are set From d61925ab5400cdf1d455b5545d3f176a6419f4ef Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 22 Feb 2015 10:06:44 +0800 Subject: [PATCH 13/13] Added documentation on adding specific keyrings. --- docs/configuration.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index abd4cab..be4bcf4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -129,6 +129,16 @@ is done you can add the module to your config without specifying the credentials :: + # Use the default keyring to retrieve credentials. To determine which backend is the default on your system, run + # python -c 'import keyring; print(keyring.get_keyring())' status.register('github') +If you don't want to use the default you can set a specific keyring like so: + +:: + + from keyring.backends.file import PlaintextKeyring + status.register('github', keyring_backend=PlaintextKeyring()) + + i3pystatus will locate and set the credentials during the module loading process. Currently supported credentals are "password", "email" and "username". \ No newline at end of file