From bec2674d387b3a72c149d9d9f16aeb6dd12f31ac Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 25 Jan 2015 14:33:04 +0800 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 40a9291cefab006b03f38701324678251eb7d8c7 Mon Sep 17 00:00:00 2001 From: gacekjk Date: Sat, 21 Feb 2015 22:21:52 +0100 Subject: [PATCH 13/27] added alert temperature handling --- i3pystatus/temp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/i3pystatus/temp.py b/i3pystatus/temp.py index 4639ccf..9e37ade 100644 --- a/i3pystatus/temp.py +++ b/i3pystatus/temp.py @@ -1,6 +1,3 @@ -import re -import glob - from i3pystatus import IntervalModule @@ -16,10 +13,14 @@ class Temperature(IntervalModule): "format string used for output. {temp} is the temperature in degrees celsius"), "color", "file", + "alert_temp", + "alert_color", ) format = "{temp} °C" color = "#FFFFFF" file = "/sys/class/thermal/thermal_zone0/temp" + alert_temp = 90 + alert_color = "#FF0000" def run(self): with open(self.file, "r") as f: @@ -27,5 +28,5 @@ class Temperature(IntervalModule): self.output = { "full_text": self.format.format(temp=temp), - "color": self.color, + "color": self.color if temp < self.alert_temp else self.alert_color, } From 912be61900e0dbf9da19eba48d81893a151bdd2d Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 21 Feb 2015 16:53:41 -0800 Subject: [PATCH 14/27] Output should not be urgent if available space is greater than the critical limit. --- i3pystatus/disk.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/i3pystatus/disk.py b/i3pystatus/disk.py index 90a6000..b67d176 100644 --- a/i3pystatus/disk.py +++ b/i3pystatus/disk.py @@ -38,6 +38,8 @@ class Disk(IntervalModule): self.output = {} return + critical = available < self.critical_limit + cdict = { "total": (stat.f_bsize * stat.f_blocks) / self.divisor, "free": (stat.f_bsize * stat.f_bfree) / self.divisor, @@ -51,6 +53,6 @@ class Disk(IntervalModule): self.output = { "full_text": self.format.format(**cdict), - "color": self.color if available > self.critical_limit else self.critical_color, - "urgent": available > self.critical_limit + "color": self.critical_color if critical else self.color, + "urgent": critical } From d61925ab5400cdf1d455b5545d3f176a6419f4ef Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 22 Feb 2015 10:06:44 +0800 Subject: [PATCH 15/27] 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 From cceb032576481d4c7569d49804dc493a2c22a592 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 14:10:36 +0100 Subject: [PATCH 16/27] rename text_len to max_field_len, add max_len parameter and some logic to truncate certain fields if output len exceeds max_len --- i3pystatus/mpd.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index 8946f19..f5db1e0 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -1,5 +1,6 @@ import socket from os.path import basename +from math import floor from i3pystatus import IntervalModule, formatp from i3pystatus.core.util import TimeWrapper @@ -34,8 +35,10 @@ class MPD(IntervalModule): ("format", "formatp string"), ("status", "Dictionary mapping pause, play and stop to output"), ("color", "The color of the text"), - ("text_len", "Defines max length for title, album and artist, if truncated ellipsis are appended as indicator"), - ("truncate_fields", "fileds that will be truncated if exceeding text_len"), + ("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. Value of 0 disables this."), + ("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. Value of 0 disables this."), + ("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len, whatever catches first takes effect"), + ) host = "localhost" @@ -48,7 +51,8 @@ class MPD(IntervalModule): "stop": "◾", } color = "#FFFFFF" - text_len = 25 + max_field_len = 25 + max_len = 100 truncate_fields = ("title", "album", "artist") on_leftclick = "switch_playpause" on_rightclick = "next_song" @@ -91,15 +95,24 @@ class MPD(IntervalModule): } - for key in self.truncate_fields: - if len(fdict[key]) > self.text_len: - fdict[key] = fdict[key][:self.text_len - 1] + "…" - if not fdict["title"] and "filename" in fdict: fdict["filename"] = '.'.join( basename(currentsong["file"]).split('.')[:-1]) else: fdict["filename"] = "" + + for key in self.truncate_fields: + if len(fdict[key]) > self.max_field_len: + fdict[key] = fdict[key][:self.max_field_len - 1] + "…" + + full_text_len = len(formatp(self.format, **fdict).strip()) + if full_text_len > self.max_len: + shrink = floor((self.max_len - full_text_len) + / len(self.truncate_fields)) - 1 + + for key in self.truncate_fields: + fdict[key] = fdict[key][:shrink] + "…" + self.output = { "full_text": formatp(self.format, **fdict).strip(), "color": self.color, From 87b57ce838ac0d2efea4dc46ed872b3ec023391e Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 14:14:58 +0100 Subject: [PATCH 17/27] some clarification in docs about the truncation order --- i3pystatus/mpd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index f5db1e0..baa524b 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -35,8 +35,8 @@ class MPD(IntervalModule): ("format", "formatp string"), ("status", "Dictionary mapping pause, play and stop to output"), ("color", "The color of the text"), - ("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. Value of 0 disables this."), - ("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. Value of 0 disables this."), + ("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. It's applied *before* max_len. Value of 0 disables this."), + ("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. It's applied *after* max_field_len. Value of 0 disables this."), ("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len, whatever catches first takes effect"), ) From 2bcef67c836b2a8a4f78fe000ffb61a1b3582a89 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 14:17:03 +0100 Subject: [PATCH 18/27] some changes to prevent recomputation of the output string if no truncation is done --- i3pystatus/mpd.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index baa524b..51d369d 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -105,7 +105,8 @@ class MPD(IntervalModule): if len(fdict[key]) > self.max_field_len: fdict[key] = fdict[key][:self.max_field_len - 1] + "…" - full_text_len = len(formatp(self.format, **fdict).strip()) + full_text = formatp(self.format, **fdict).strip() + full_text_len = len(full_text) if full_text_len > self.max_len: shrink = floor((self.max_len - full_text_len) / len(self.truncate_fields)) - 1 @@ -113,8 +114,10 @@ class MPD(IntervalModule): for key in self.truncate_fields: fdict[key] = fdict[key][:shrink] + "…" + full_text = formatp(self.format, **fdict).strip() + self.output = { - "full_text": formatp(self.format, **fdict).strip(), + "full_text": full_text, "color": self.color, } From d0692798b706c5d1d644ea2f59ca981f974bfd37 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 14:29:48 +0100 Subject: [PATCH 19/27] fix a documentation issue --- i3pystatus/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index 51d369d..d9b3b33 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -37,7 +37,7 @@ class MPD(IntervalModule): ("color", "The color of the text"), ("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. It's applied *before* max_len. Value of 0 disables this."), ("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. It's applied *after* max_field_len. Value of 0 disables this."), - ("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len, whatever catches first takes effect"), + ("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len."), ) From 68813026e632b478e812731eabd95b9101800390 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 14:54:01 +0100 Subject: [PATCH 20/27] add functionalaty for deactivate truncation in mpd module --- i3pystatus/mpd.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index d9b3b33..496ca5c 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -101,13 +101,14 @@ class MPD(IntervalModule): else: fdict["filename"] = "" - for key in self.truncate_fields: - if len(fdict[key]) > self.max_field_len: - fdict[key] = fdict[key][:self.max_field_len - 1] + "…" + if self.max_field_len > 0: + for key in self.truncate_fields: + if len(fdict[key]) > self.max_field_len: + fdict[key] = fdict[key][:self.max_field_len - 1] + "…" full_text = formatp(self.format, **fdict).strip() full_text_len = len(full_text) - if full_text_len > self.max_len: + if full_text_len > self.max_len and self.max_len > 0: shrink = floor((self.max_len - full_text_len) / len(self.truncate_fields)) - 1 From 787e8d262370b8daf721b3a41c56673d1842d0ea Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 22 Feb 2015 15:03:32 +0100 Subject: [PATCH 21/27] fixed build failure --- i3pystatus/mpd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index 496ca5c..95d5bab 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -109,8 +109,8 @@ class MPD(IntervalModule): full_text = formatp(self.format, **fdict).strip() full_text_len = len(full_text) if full_text_len > self.max_len and self.max_len > 0: - shrink = floor((self.max_len - full_text_len) - / len(self.truncate_fields)) - 1 + shrink = floor((self.max_len - full_text_len) / + len(self.truncate_fields)) - 1 for key in self.truncate_fields: fdict[key] = fdict[key][:shrink] + "…" From 6dbc2c78e5ef78c49a29ab1e3dc2f5f77d0c3d6e Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 1 Mar 2015 14:51:52 +0100 Subject: [PATCH 22/27] remove the module specific and hard coded interval in cpu_usage --- i3pystatus/cpu_usage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/i3pystatus/cpu_usage.py b/i3pystatus/cpu_usage.py index 8897c22..0f353bb 100644 --- a/i3pystatus/cpu_usage.py +++ b/i3pystatus/cpu_usage.py @@ -40,7 +40,6 @@ class CpuUsage(IntervalModule): def init(self): self.prev_total = defaultdict(int) self.prev_busy = defaultdict(int) - self.interval = 1 self.formatter = Formatter() def get_cpu_timings(self): From efd1c5f09ffd25912408873579618d04627dcc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Mand=C3=A1k?= Date: Sat, 7 Mar 2015 12:47:20 +0100 Subject: [PATCH 23/27] fixed #177 --- i3pystatus/clock.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/i3pystatus/clock.py b/i3pystatus/clock.py index 67a8fa7..61c9de4 100644 --- a/i3pystatus/clock.py +++ b/i3pystatus/clock.py @@ -32,13 +32,24 @@ class Clock(IntervalModule): on_downscroll = ["scroll_format", -1] def init(self): - lang, enc = os.environ.get('LANG', None).split('.', 1) - if lang != locale.getlocale(locale.LC_TIME)[0]: + env_lang = os.environ.get('LC_TIME', None) + if env_lang is None: + env_lang = os.environ.get('LANG', None) + + if env_lang is not None: + if env_lang.find('.') != -1: + lang = tuple(env_lang.split('.', 1)) + else: + lang = (env_lang, None) + else: + lang = (None, None) + + if lang != locale.getlocale(locale.LC_TIME): # affects datetime.time.strftime() in whole program - locale.setlocale(locale.LC_TIME, (lang, enc)) + locale.setlocale(locale.LC_TIME, lang) if self.format is None: - if lang == 'en_US': + if lang[0] == 'en_US': # MDY format - United States of America self.format = ["%a %b %-d %X"] else: From 013b15ffd4319dfec47bbd6fbda8c9713d01e86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Mand=C3=A1k?= Date: Sat, 7 Mar 2015 12:47:42 +0100 Subject: [PATCH 24/27] Fixed dbus error when not using player autodetection. --- i3pystatus/now_playing.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/i3pystatus/now_playing.py b/i3pystatus/now_playing.py index b75104c..acb0784 100644 --- a/i3pystatus/now_playing.py +++ b/i3pystatus/now_playing.py @@ -80,9 +80,13 @@ class NowPlaying(IntervalModule): def get_player(self): if self.player: player = "org.mpris.MediaPlayer2." + self.player + try: + return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") + except dbus.exceptions.DBusException: + raise NoPlayerException() else: player = self.find_player() - return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") + return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") def run(self): try: From 4209d7046c255a683df8fd3442402eb13533f62b Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 8 Mar 2015 08:38:08 +0800 Subject: [PATCH 25/27] Fixed bug that prevented color_up being shown if the user is not using network_traffic. --- i3pystatus/network.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/i3pystatus/network.py b/i3pystatus/network.py index afb9ddb..8857df2 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -337,7 +337,6 @@ class Network(IntervalModule, ColorRangeModule): format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="", essid="", freq="", quality="", quality_bar="") - color = None if self.network_traffic: network_usage = self.network_traffic.get_usage(self.interface) format_values.update(network_usage) @@ -351,22 +350,22 @@ class Network(IntervalModule, ColorRangeModule): format_values['network_graph'] = self.get_network_graph(kbs) format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) color = self.get_gradient(kbs, self.colors, self.upper_limit) + else: + color = None + + if sysfs_interface_up(self.interface, self.unknown_up): + if not color: + color = self.color_up + format_str = self.format_up + else: + color = self.color_down + format_str = self.format_down network_info = self.network_info.get_info(self.interface) format_values.update(network_info) - format_values['interface'] = self.interface - if sysfs_interface_up(self.interface, self.unknown_up): - if not self.dynamic_color: - color = self.color_up - self.output = { - "full_text": self.format_up.format(**format_values), - 'color': color, - } - else: - color = self.color_down - self.output = { - "full_text": self.format_down.format(**format_values), - 'color': color, - } + self.output = { + "full_text": format_str.format(**format_values), + 'color': color, + } From 194fc29e0214b51462b678796c8fa9c548a9eac9 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Thu, 12 Mar 2015 20:42:15 +0100 Subject: [PATCH 26/27] readded default intervall 1 but in the right way this time --- i3pystatus/cpu_usage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/i3pystatus/cpu_usage.py b/i3pystatus/cpu_usage.py index 0f353bb..a131850 100644 --- a/i3pystatus/cpu_usage.py +++ b/i3pystatus/cpu_usage.py @@ -29,6 +29,7 @@ class CpuUsage(IntervalModule): format = "{usage:02}%" format_all = "{core}:{usage:02}%" exclude_average = False + interval = 1 settings = ( ("format", "format string."), ("format_all", ("format string used for {usage_all} per core. " From 16b3799b91123b446607100028deaf2a8261b6dc Mon Sep 17 00:00:00 2001 From: facetoe Date: Mon, 16 Mar 2015 19:45:50 +0800 Subject: [PATCH 27/27] Remove 'u' prefix from unicode strings as it is no longer required in Python3 --- i3pystatus/core/util.py | 4 ++-- i3pystatus/github.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index 57e9467..9bc11fd 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -383,7 +383,7 @@ def make_graph(values, lower_limit=0.0, upper_limit=100.0, style="blocks"): extent = mx - mn if style == 'blocks': - bar = u'_▁▂▃▄▅▆▇█' + bar = '_▁▂▃▄▅▆▇█' bar_count = len(bar) - 1 if extent == 0: graph = '_' * len(values) @@ -436,7 +436,7 @@ def make_vertical_bar(percentage, width=1): :param width: How many characters wide the bar should be. :returns: Bar as a String """ - bar = u' _▁▂▃▄▅▆▇█' + bar = ' _▁▂▃▄▅▆▇█' percentage //= 10 if percentage < 0: output = bar[0] diff --git a/i3pystatus/github.py b/i3pystatus/github.py index bfcfac8..d4dbeba 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -16,7 +16,7 @@ class Github(IntervalModule): * `{unread_count}` - number of unread notifications, empty if 0 """ - unread_marker = u"●" + unread_marker = "●" unread = '' color = '#78EAF2' username = ''