def __init__(self, module=None): self._audio = None self._config_setting = {} self._english_env = dict(os.environ) self._english_env["LC_ALL"] = "C" self._english_env["LANGUAGE"] = "C" self._format_placeholders = {} self._format_placeholders_cache = {} self._is_python_2 = sys.version_info < (3, 0) self._module = module self._report_exception_cache = set() self._thresholds = None self._threshold_gradients = {} self._uid = uuid4() if module: self._i3s_config = module._py3_wrapper.config["py3_config"][ "general"] self._module_full_name = module.module_full_name self._output_modules = module._py3_wrapper.output_modules self._py3status_module = module.module_class self._py3_wrapper = module._py3_wrapper # create formatter we only if need one but want to pass py3_wrapper so # that we can do logging etc. if not self._formatter: self.__class__._formatter = Formatter(module._py3_wrapper)
def __init__(self, module=None): self._audio = None self._config_setting = {} self._format_placeholders = {} self._format_placeholders_cache = {} self._is_python_2 = sys.version_info < (3, 0) self._module = module self._report_exception_cache = set() self._thresholds = None self._threshold_gradients = {} if module: self._i3s_config = module._py3_wrapper.config['py3_config'][ 'general'] self._module_full_name = module.module_full_name self._output_modules = module._py3_wrapper.output_modules self._py3status_module = module.module_class self._py3_wrapper = module._py3_wrapper # create formatter we only if need one but want to pass py3_wrapper so # that we can do logging etc. if not self._formatter: self.__class__._formatter = Formatter(module._py3_wrapper)
class Py3: """ Helper object that gets injected as self.py3 into Py3status modules that have not got that attribute set already. This allows functionality like: User notifications Forcing module to update (even other modules) Triggering events for modules Py3 is also used for testing in which case it does not get a module when being created. All methods should work in this situation. """ CACHE_FOREVER = PY3_CACHE_FOREVER LOG_ERROR = PY3_LOG_ERROR LOG_INFO = PY3_LOG_INFO LOG_WARNING = PY3_LOG_WARNING # Shared by all Py3 Instances _formatter = Formatter() _none_color = NoneColor() def __init__(self, module=None, i3s_config=None, py3status=None): self._audio = None self._colors = {} self._format_placeholders = {} self._format_placeholders_cache = {} self._i3s_config = i3s_config or {} self._module = module self._is_python_2 = sys.version_info < (3, 0) self._report_exception_cache = set() self._thresholds = None if py3status: self._py3status_module = py3status # we are running through the whole stack. # If testing then module is None. if module: self._output_modules = module._py3_wrapper.output_modules if not i3s_config: i3s_config = self._module.config['py3_config']['general'] self._i3s_config = i3s_config self._py3status_module = module.module_class def __getattr__(self, name): """ Py3 can provide COLOR constants eg COLOR_GOOD, COLOR_BAD, COLOR_DEGRADED but also any constant COLOR_XXX we find this color in the config if it exists """ if not name.startswith('COLOR_'): raise AttributeError return self._get_color_by_name(name) def _get_color_by_name(self, name): name = name.lower() if name not in self._colors: if self._module: color_fn = self._module._py3_wrapper.get_config_attribute color = color_fn(self._module.module_full_name, name) else: # running in test mode so config is not available color = self._i3s_config.get(name, False) if color: self._colors[name] = color elif color is False: # False indicates color is not defined self._colors[name] = None else: # None indicates that no color is wanted self._colors[name] = self._none_color return self._colors[name] def _get_color(self, color): if not color: return # fix any hex colors so they are #RRGGBB if color.startswith('#'): color = color.upper() if len(color) == 4: color = ('#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3]) return color name = 'color_%s' % color return self._get_color_by_name(name) def _thresholds_init(self): """ Initiate and check any thresholds set """ thresholds = getattr(self._py3status_module, 'thresholds', []) self._thresholds = {} if isinstance(thresholds, list): thresholds.sort() self._thresholds[None] = [(x[0], self._get_color(x[1])) for x in thresholds] return elif isinstance(thresholds, dict): for key, value in thresholds.items(): if isinstance(value, list): value.sort() self._thresholds[key] = [(x[0], self._get_color(x[1])) for x in value] def _get_module_info(self, module_name): """ THIS IS PRIVATE AND UNSUPPORTED. Get info for named module. Info comes back as a dict containing. 'module': the instance of the module, 'position': list of places in i3bar, usually only one item 'type': module type py3status/i3status """ if self._module: return self._output_modules.get(module_name) def _report_exception(self, msg, frame_skip=2): """ THIS IS PRIVATE AND UNSUPPORTED. logs an exception that occurs inside of a Py3 method. We only log the exception once to prevent spamming the logs and we do not notify the user. frame_skip is used to change the place in the code that the error is reported as coming from. We want to show it as coming from the py3status module where the Py3 method was called. """ # We use a hash to see if the message is being repeated. msg_hash = hash(msg) if msg_hash in self._report_exception_cache: return self._report_exception_cache.add(msg_hash) if self._module: # If we just report the error the traceback will end in the try # except block that we are calling from. # We want to show the traceback originating from the module that # called the Py3 method so get the correct error frame and pass this # along. error_frame = sys._getframe(0) while frame_skip: error_frame = error_frame.f_back frame_skip -= 1 self._module._py3_wrapper.report_exception( msg, notify_user=False, error_frame=error_frame ) def format_units(self, value, unit='B', optimal=5, auto=True, si=False): """ Takes a value and formats it for user output, we can choose the unit to use eg B, MiB, kbits/second. This is mainly for use with bytes/bits it converts the value into a human readable form. It has various additional options but they are really only for special cases. The function returns a tuple containing the new value (this is a number so that the user can still format it if required) and a unit that is the units that we have been converted to. By supplying unit to the function we can force those units to be used eg `unit=KiB` would force the output to be in Kibibytes. By default we use non-si units but if the unit is si eg kB then we will switch to si units. Units can also be things like `Mbit/sec`. If the auto parameter is False then we use the unit provided. This only makes sense when the unit is singular eg 'Bytes' and we want the result in bytes and not say converted to MBytes. optimal is used to control the size of the output value. We try to provide an output value of that number of characters (including decimal point), it may also be less due to rounding. If a fixed unit is used the output may be more than this number of characters. """ UNITS = 'KMGTPEZY' DECIMAL_SIZE = 1000 BINARY_SIZE = 1024 CUTOFF = 1000 can_round = False if unit: # try to guess the unit. Do we have a known prefix too it? if unit[0].upper() in UNITS: index = UNITS.index(unit[0].upper()) + 1 post = unit[1:] si = len(unit) > 1 and unit[1] != 'i' if si: post = post[1:] auto = False else: index = 0 post = unit if si: size = DECIMAL_SIZE else: size = BINARY_SIZE if auto: # we will try to use an appropriate prefix if value < CUTOFF: unit_out = post else: value /= size for prefix in UNITS: if abs(value) < CUTOFF: break value /= size if si: # si kilo is lowercase if prefix == 'K': prefix = 'k' else: post = 'i' + post unit_out = prefix + post can_round = True else: # we are using a fixed unit unit_out = unit size = pow(size, index) if size: value /= size can_round = True if can_round and optimal and value: # we will try to make the output value the desired size # we need to keep out value as a numeric type places = int(log10(abs(value))) if places >= optimal - 2: value = int(value) else: value = round(value, max(optimal - places - 2, 0)) return value, unit_out def is_color(self, color): """ Tests to see if a color is defined. Because colors can be set to None in the config and we want this to be respected in an expression like. color = self.py3.COLOR_MUTED or self.py3.COLOR_BAD The color is treated as True but sometimes we want to know if the color has a value set in which case the color should count as False. This function is a helper for this second case. """ return not (color is None or hasattr(color, 'none_color')) def i3s_config(self): """ returns the i3s_config dict. """ return self._i3s_config def is_python_2(self): """ True if the version of python being used is 2.x Can be helpful for fixing python 2 compatability issues """ return self._is_python_2 def is_my_event(self, event): """ Checks if an event triggered belongs to the module recieving it. This is mainly for containers who will also recieve events from any children they have. Returns True if the event name and instance match that of the module checking. """ if not self._module: return False return ( event.get('name') == self._module.module_name and event.get('instance') == self._module.module_inst ) def log(self, message, level=LOG_INFO): """ Log the message. The level must be one of LOG_ERROR, LOG_INFO or LOG_WARNING """ assert level in [ self.LOG_ERROR, self.LOG_INFO, self.LOG_WARNING ], 'level must be LOG_ERROR, LOG_INFO or LOG_WARNING' if self._module: # nicely format logs if we can using pretty print message = pformat(message) # start on new line if multi-line output if '\n' in message: message = '\n' + message message = 'Module `{}`: {}'.format( self._module.module_full_name, message) self._module._py3_wrapper.log(message, level) def update(self, module_name=None): """ Update a module. If module_name is supplied the module of that name is updated. Otherwise the module calling is updated. """ if not module_name and self._module: return self._module.force_update() else: module_info = self._get_module_info(module_name) if module_info: module_info['module'].force_update() def get_output(self, module_name): """ Return the output of the named module. This will be a list. """ output = [] module_info = self._get_module_info(module_name) if module_info: output = module_info['module'].get_latest() return output def trigger_event(self, module_name, event): """ Trigger an event on a named module. """ if module_name and self._module: self._module._py3_wrapper.events_thread.process_event( module_name, event) def prevent_refresh(self): """ Calling this function during the on_click() method of a module will request that the module is not refreshed after the event. By default the module is updated after the on_click event has been processed. """ if self._module: self._module.prevent_refresh = True def notify_user(self, msg, level='info', rate_limit=5): """ Send a notification to the user. level must be 'info', 'error' or 'warning'. rate_limit is the time period in seconds during which this message should not be repeated. """ if self._module: # force unicode for python2 str if self._is_python_2 and isinstance(msg, str): msg = msg.decode('utf-8') module_name = self._module.module_full_name self._module._py3_wrapper.notify_user( msg, level=level, rate_limit=rate_limit, module_name=module_name) def register_function(self, function_name, function): """ Register a function for the module. The following functions can be registered > __content_function()__ > > Called to discover what modules a container is displaying. This is > used to determine when updates need passing on to the container and > also when modules can be put to sleep. > > the function must return a set of module names that are being > displayed. > > Note: This function should only be used by containers. > > __urgent_function(module_names)__ > > This function will be called when one of the contents of a container > has changed from a non-urgent to an urgent state. It is used by the > group module to switch to displaying the urgent module. > > `module_names` is a list of modules that have become urgent > > Note: This function should only be used by containers. """ if self._module: my_info = self._get_module_info(self._module.module_full_name) my_info[function_name] = function def time_in(self, seconds=None, sync_to=None, offset=0): """ Returns the time a given number of seconds into the future. Helpful for creating the `cached_until` value for the module output. Note: from version 3.1 modules no longer need to explicitly set a `cached_until` in their response unless they wish to directly control it. seconds specifies the number of seconds that should occure before the update is required. sync_to causes the update to be syncronised to a time period. 1 would cause the update on the second, 60 to the nearest minute. By defalt we syncronise to the nearest second. 0 will disable this feature. offset is used to alter the base time used. A timer that started at a certain time could set that as the offset and any syncronisation would then be relative to that time. """ if seconds is None: # If we have a sync_to then seconds can be 0 if sync_to and sync_to > 0: seconds = 0 else: try: # use py3status modules cache_timeout seconds = self._py3status_module.cache_timeout except AttributeError: # use default cache_timeout seconds = self._module.config['cache_timeout'] # Unless explicitly set we sync to the nearest second # Unless the requested update is in less than a second if sync_to is None: if seconds and seconds < 1: sync_to = 0 else: sync_to = 1 requested = time() + seconds - offset # if sync_to then we find the sync time for the requested time if sync_to: requested = (requested + sync_to) - (requested % sync_to) return requested + offset def format_contains(self, format_string, name): """ Determines if `format_string` contains placeholder `name` `name` is tested against placeholders using fnmatch so the following patterns can be used: * matches everything ? matches any single character [seq] matches any character in seq [!seq] matches any character not in seq This is useful because a simple test like `'{placeholder}' in format_string` will fail if the format string contains placeholder formatting eg `'{placeholder:.2f}'` """ # We cache things to prevent parsing the format_string more than needed try: return self._format_placeholders_cache[format_string][name] except KeyError: pass if format_string not in self._format_placeholders: placeholders = self._formatter.get_placeholders(format_string) self._format_placeholders[format_string] = placeholders else: placeholders = self._format_placeholders[format_string] result = False for placeholder in placeholders: if fnmatch(placeholder, name): result = True break if format_string not in self._format_placeholders_cache: self._format_placeholders_cache[format_string] = {} self._format_placeholders_cache[format_string][name] = result return result def safe_format(self, format_string, param_dict=None, force_composite=False, attr_getter=None): """ Parser for advanced formatting. Unknown placeholders will be shown in the output eg `{foo}`. Square brackets `[]` can be used. The content of them will be removed from the output if there is no valid placeholder contained within. They can also be nested. A pipe (vertical bar) `|` can be used to divide sections the first valid section only will be shown in the output. A backslash `\` can be used to escape a character eg `\[` will show `[` in the output. `\?` is special and is used to provide extra commands to the format string, example `\?color=#FF00FF`. Multiple commands can be given using an ampersand `&` as a separator, example `\?color=#FF00FF&show`. `{<placeholder>}` will be converted, or removed if it is None or empty. Formating can also be applied to the placeholder eg `{number:03.2f}`. example format_string: `"[[{artist} - ]{title}]|{file}"` This will show `artist - title` if artist is present, `title` if title but no artist, and `file` if file is present but not artist or title. param_dict is a dictionary of palceholders that will be substituted. If a placeholder is not in the dictionary then if the py3status module has an attribute with the same name then it will be used. __Since version 3.3__ Composites can be included in the param_dict. The result returned from this function can either be a string in the case of simple parsing or a Composite if more complex. If force_composite parameter is True a composite will always be returned. attr_getter is a function that will when called with an attribute name as a parameter will return a value. """ try: return self._formatter.format( format_string, self._py3status_module, param_dict, force_composite=force_composite, attr_getter=attr_getter, ) except Exception: self._report_exception( u'Invalid format `{}`'.format(format_string) ) return 'invalid format' def build_composite(self, format_string, param_dict=None, composites=None, attr_getter=None): """ __deprecated in 3.3__ use safe_format(). Build a composite output using a format string. Takes a format_string and treats it the same way as `safe_format` but also takes a composites dict where each key/value is the name of the placeholder and either an output eg `{'full_text': 'something'}` or a list of outputs. """ if param_dict is None: param_dict = {} # merge any composites into the param_dict. # as they are no longer dealt with separately if composites: for key, value in composites.items(): param_dict[key] = Composite(value) try: return self._formatter.format( format_string, self._py3status_module, param_dict, force_composite=True, attr_getter=attr_getter, ) except Exception: self._report_exception( u'Invalid format `{}`'.format(format_string) ) return [{'full_text': 'invalid format'}] def composite_update(self, item, update_dict, soft=False): """ Takes a Composite (item) if item is a type that can be converted into a Composite then this is done automatically. Updates all entries it the Composite with values from update_dict. Updates can be soft in which case existing values are not overwritten. A Composite object will be returned. """ return Composite.composite_update(item, update_dict, soft=False) def composite_join(self, separator, items): """ Join a list of items with a separator. This is used in joining strings, responses and Composites. A Composite object will be returned. """ return Composite.composite_join(separator, items) def composite_create(self, item): """ Create and return a Composite. The item may be a string, dict, list of dicts or a Composite. """ return Composite(item) def is_composite(self, item): """ Check if item is a Composite and return True if it is. """ return isinstance(item, Composite) def check_commands(self, cmd_list): """ Checks to see if commands in list are available using `which`. Returns the first available command. If a string is passed then that command will be checked for. """ # if a string is passed then convert it to a list. This prevents an # easy mistake that could be made if isinstance(cmd_list, basestring): cmd_list = [cmd_list] for cmd in cmd_list: if self.command_run('which {}'.format(cmd)) == 0: return cmd def command_run(self, command): """ Runs a command and returns the exit code. The command can either be supplied as a sequence or string. An Exception is raised if an error occurs """ # convert the command to sequence if a string if isinstance(command, basestring): command = shlex.split(command) try: return Popen(command, stdout=PIPE, stderr=PIPE).wait() except Exception as e: msg = "Command '{cmd}' {error}" raise Exception(msg.format(cmd=command[0], error=e)) def command_output(self, command, shell=False): """ Run a command and return its output as unicode. The command can either be supplied as a sequence or string. An Exception is raised if an error occurs """ # convert the command to sequence if a string if isinstance(command, basestring): command = shlex.split(command) try: process = Popen(command, stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=shell) except Exception as e: msg = "Command '{cmd}' {error}" raise Exception(msg.format(cmd=command[0], error=e)) output, error = process.communicate() if self._is_python_2: output = output.decode('utf-8') error = error.decode('utf-8') retcode = process.poll() if retcode: # under certain conditions a successfully run command may get a # return code of -15 even though correct output was returned see # #664. This issue seems to be related to arch linux but the # reason is not entirely clear. if retcode == -15: msg = 'Command `{cmd}` returned SIGTERM (ignoring)' self.log(msg.format(cmd=command)) else: msg = "Command '{cmd}' returned non-zero exit status {error}" raise Exception(msg.format(cmd=command[0], error=retcode)) if error: msg = "Command '{cmd}' had error {error}" raise Exception(msg.format(cmd=command[0], error=error)) return output def play_sound(self, sound_file): """ Plays sound_file if possible. """ self.stop_sound() cmd = self.check_commands(['paplay', 'play']) if cmd: sound_file = os.path.expanduser(sound_file) c = shlex.split('{} {}'.format(cmd, sound_file)) self._audio = Popen(c) def stop_sound(self): """ Stops any currently playing sounds for this module. """ if self._audio: self._audio.kill() self._audio = None def threshold_get_color(self, value, name=None): """ Obtain color for a value using thresholds. The value will be checked against any defined thresholds. These should have been set in the i3status configuration. If more than one threshold is needed for a module then the name can also be supplied. If the user has not supplied a named threshold but has defined a general one that will be used. """ # If first run then process the threshold data. if self._thresholds is None: self._thresholds_init() color = None try: value = float(value) except ValueError: color = self._get_color('error') or self._get_color('bad') # if name not in thresholds info then use defaults name_used = name if name_used not in self._thresholds: name_used = None if color is None: for threshold in self._thresholds.get(name_used, []): if value >= threshold[0]: color = threshold[1] else: break # save color so it can be accessed via safe_format() if name: color_name = 'color_threshold_%s' % name else: color_name = 'color_threshold' setattr(self._py3status_module, color_name, color) return color
Run formatter tests """ import platform import sys from pprint import pformat import pytest from py3status.composite import Composite from py3status.formatter import Formatter from py3status.py3 import NoneColor is_pypy = platform.python_implementation() == "PyPy" f = Formatter() python2 = sys.version_info < (3, 0) param_dict = { "name": u"Björk", "number": 42, "pi": 3.14159265359, "yes": True, "no": False, "empty":
def load_methods(self, module, user_modules): """ Read the given user-written py3status class file and store its methods. Those methods will be executed, so we will deliberately ignore: - private methods starting with _ - decorated methods such as @property or @staticmethod - 'on_click' methods as they'll be called upon a click_event - 'kill' methods as they'll be called upon this thread's exit """ # user provided modules take precedence over py3status provided modules if self.module_name in user_modules: include_path, f_name = user_modules[self.module_name] self._py3_wrapper.log('loading module "{}" from {}{}'.format( module, include_path, f_name)) class_inst = self.load_from_file(include_path + f_name) # load from py3status provided modules else: self._py3_wrapper.log( 'loading module "{}" from py3status.modules.{}'.format( module, self.module_name)) class_inst = self.load_from_namespace(self.module_name) if class_inst: self.module_class = class_inst try: # containers have items attribute set to a list of contained # module instance names. If there are no contained items then # ensure that we have a empty list. if class_inst.Meta.container: class_inst.items = [] except AttributeError: pass # module configuration mod_config = self.config['py3_config'].get(module, {}) # process any deprecated configuration settings try: deprecated = class_inst.Meta.deprecated except AttributeError: deprecated = None if deprecated: def deprecation_log(item): # log the deprecation # currently this is just done to the log file as the user # does not need to take any action. if 'msg' in item: msg = item['msg'] param = item.get('param') if param: msg = '`{}` {}'.format(param, msg) msg = 'DEPRECATION WARNING: {} {}'.format( self.module_full_name, msg) self._py3_wrapper.log(msg) if 'rename' in deprecated: # renamed parameters for item in deprecated['rename']: param = item['param'] new_name = item['new'] if param in mod_config: if new_name not in mod_config: mod_config[new_name] = mod_config[param] # remove from config del mod_config[param] deprecation_log(item) if 'format_fix_unnamed_param' in deprecated: # format update where {} was previously allowed for item in deprecated['format_fix_unnamed_param']: param = item['param'] placeholder = item['placeholder'] if '{}' in mod_config.get(param, ''): mod_config[param] = mod_config[param].replace( '{}', '{%s}' % placeholder) deprecation_log(item) if 'rename_placeholder' in deprecated: # rename placeholders placeholders = {} for item in deprecated['rename_placeholder']: placeholders[item['placeholder']] = item['new'] format_strings = item['format_strings'] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholders( format_string, placeholders) mod_config[format_param] = format if 'update_placeholder_format' in deprecated: # update formats for placeholders if a format is not set for item in deprecated['update_placeholder_format']: placeholder_formats = item.get('placeholder_formats', {}) if 'function' in item: placeholder_formats.update( item['function'](mod_config)) format_strings = item['format_strings'] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) mod_config[format_param] = format if 'substitute_by_value' in deprecated: # one parameter sets the value of another for item in deprecated['substitute_by_value']: param = item['param'] value = item['value'] substitute = item['substitute'] substitute_param = substitute['param'] substitute_value = substitute['value'] if (mod_config.get(param) == value and substitute_param not in mod_config): mod_config[substitute_param] = substitute_value deprecation_log(item) if 'function' in deprecated: # parameter set by function for item in deprecated['function']: updates = item['function'](mod_config) for name, value in updates.items(): if name not in mod_config: mod_config[name] = value if 'remove' in deprecated: # obsolete parameters forcibly removed for item in deprecated['remove']: param = item['param'] if param in mod_config: del mod_config[param] deprecation_log(item) # apply module configuration for config, value in mod_config.items(): # names starting with '.' are private if not config.startswith('.'): setattr(self.module_class, config, value) # process any update_config settings try: update_config = class_inst.Meta.update_config except AttributeError: update_config = None if update_config: if 'update_placeholder_format' in update_config: # update formats for placeholders if a format is not set for item in update_config['update_placeholder_format']: placeholder_formats = item.get('placeholder_formats', {}) format_strings = item['format_strings'] for format_param in format_strings: format_string = getattr(class_inst, format_param, None) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) setattr(class_inst, format_param, format) # Add the py3 module helper if modules self.py3 is not defined if not hasattr(self.module_class, 'py3'): setattr(self.module_class, 'py3', Py3(self)) # get the available methods for execution for method in sorted(dir(class_inst)): if method.startswith('_'): continue else: m_type = type(getattr(class_inst, method)) if 'method' in str(m_type): params_type = self._params_type(method, class_inst) if method == 'on_click': self.click_events = params_type elif method == 'kill': self.has_kill = params_type elif method == 'post_config_hook': self.has_post_config_hook = True else: # the method_obj stores infos about each method # of this module. method_obj = { 'cached_until': time(), 'call_type': params_type, 'instance': None, 'last_output': { 'name': method, 'full_text': '' }, 'method': method, 'name': None } self.methods[method] = method_obj # done, log some debug info if self.config['debug']: self._py3_wrapper.log( 'module "{}" click_events={} has_kill={} methods={}'.format( module, self.click_events, self.has_kill, self.methods.keys()))
def load_methods(self, module, user_modules): """ Read the given user-written py3status class file and store its methods. Those methods will be executed, so we will deliberately ignore: - private methods starting with _ - decorated methods such as @property or @staticmethod - 'on_click' methods as they'll be called upon a click_event - 'kill' methods as they'll be called upon this thread's exit """ if not self.module_class: # user provided modules take precedence over py3status provided modules if self.module_name in user_modules: include_path, f_name = user_modules[self.module_name] module_path = os.path.join(include_path, f_name) self._py3_wrapper.log('loading module "{}" from {}'.format( module, module_path)) self.module_class = self.load_from_file(module_path) # load from py3status provided modules else: self._py3_wrapper.log( 'loading module "{}" from py3status.modules.{}'.format( module, self.module_name)) self.module_class = self.load_from_namespace(self.module_name) class_inst = self.module_class if class_inst: try: # containers have items attribute set to a list of contained # module instance names. If there are no contained items then # ensure that we have a empty list. if class_inst.Meta.container: class_inst.items = [] except AttributeError: pass # module configuration fn = self._py3_wrapper.get_config_attribute mod_config = self.config["py3_config"].get(module, {}) # resources if self.config.get("resources"): module = self.module_full_name resources = fn(module, "resources") if not hasattr(resources, "none_setting"): exception = True if isinstance(resources, list): exception = False for resource in resources: if not isinstance(resource, tuple) or len(resource) != 3: exception = True break if exception: err = "Invalid `resources` attribute, " err += "should be a list of 3-tuples. " raise TypeError(err) from fnmatch import fnmatch for resource in resources: key, resource, value = resource for setting in self.config["resources"]: if fnmatch(setting, resource): value = self.config["resources"][setting] break self.config["py3_config"][module][key] = value # process any deprecated configuration settings try: deprecated = class_inst.Meta.deprecated except AttributeError: deprecated = None if deprecated: def deprecation_log(item): # log the deprecation # currently this is just done to the log file as the user # does not need to take any action. if "msg" in item: msg = item["msg"] param = item.get("param") if param: msg = "`{}` {}".format(param, msg) msg = "DEPRECATION WARNING: {} {}".format( self.module_full_name, msg) self._py3_wrapper.log(msg) if "rename" in deprecated: # renamed parameters for item in deprecated["rename"]: param = item["param"] new_name = item["new"] if param in mod_config: if new_name not in mod_config: mod_config[new_name] = mod_config[param] # remove from config del mod_config[param] deprecation_log(item) if "format_fix_unnamed_param" in deprecated: # format update where {} was previously allowed for item in deprecated["format_fix_unnamed_param"]: param = item["param"] placeholder = item["placeholder"] if "{}" in mod_config.get(param, ""): mod_config[param] = mod_config[param].replace( "{}", "{%s}" % placeholder) deprecation_log(item) if "rename_placeholder" in deprecated: # rename placeholders placeholders = {} for item in deprecated["rename_placeholder"]: placeholders[item["placeholder"]] = item["new"] format_strings = item["format_strings"] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholders( format_string, placeholders) mod_config[format_param] = format if "update_placeholder_format" in deprecated: # update formats for placeholders if a format is not set for item in deprecated["update_placeholder_format"]: placeholder_formats = item.get("placeholder_formats", {}) if "function" in item: placeholder_formats.update( item["function"](mod_config)) format_strings = item["format_strings"] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) mod_config[format_param] = format if "substitute_by_value" in deprecated: # one parameter sets the value of another for item in deprecated["substitute_by_value"]: param = item["param"] value = item["value"] substitute = item["substitute"] substitute_param = substitute["param"] substitute_value = substitute["value"] if (mod_config.get(param) == value and substitute_param not in mod_config): mod_config[substitute_param] = substitute_value deprecation_log(item) if "function" in deprecated: # parameter set by function for item in deprecated["function"]: updates = item["function"](mod_config) for name, value in updates.items(): if name not in mod_config: mod_config[name] = value if "remove" in deprecated: # obsolete parameters forcibly removed for item in deprecated["remove"]: param = item["param"] if param in mod_config: del mod_config[param] deprecation_log(item) # apply module configuration for config, value in mod_config.items(): # names starting with '.' are private if not config.startswith("."): setattr(self.module_class, config, value) # process any update_config settings try: update_config = class_inst.Meta.update_config except AttributeError: update_config = None if update_config: if "update_placeholder_format" in update_config: # update formats for placeholders if a format is not set for item in update_config["update_placeholder_format"]: placeholder_formats = item.get("placeholder_formats", {}) format_strings = item["format_strings"] for format_param in format_strings: format_string = getattr(class_inst, format_param, None) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) setattr(class_inst, format_param, format) # Add the py3 module helper if modules self.py3 is not defined if not hasattr(self.module_class, "py3"): setattr(self.module_class, "py3", Py3(self)) # Subscribe to udev events if on_udev_* dynamic variables are # configured on the module for param in dir(self.module_class): if param.startswith("on_udev_"): trigger_action = getattr(self.module_class, param) self.add_udev_trigger(trigger_action, param[8:]) # allow_urgent param = fn(self.module_full_name, "allow_urgent") if hasattr(param, "none_setting"): param = True self.allow_urgent = param # urgent background urgent_background = fn(self.module_full_name, "urgent_background") if not hasattr(urgent_background, "none_setting"): color = self.module_class.py3._get_color(urgent_background) if not color: err = "Invalid `urgent_background` attribute, should be " err += "a color. Got `{}`.".format(urgent_background) raise ValueError(err) self.i3bar_gaps_urgent_options["background"] = color # urgent foreground urgent_foreground = fn(self.module_full_name, "urgent_foreground") if not hasattr(urgent_foreground, "none_setting"): color = self.module_class.py3._get_color(urgent_foreground) if not color: err = "Invalid `urgent_foreground` attribute, should be " err += "a color. Got `{}`.".format(urgent_foreground) raise ValueError(err) self.i3bar_gaps_urgent_options["foreground"] = color # urgent urgent_borders urgent_border = fn(self.module_full_name, "urgent_border") if not hasattr(urgent_border, "none_setting"): color = self.module_class.py3._get_color(urgent_border) if not color: err = "Invalid `urgent_border` attribute, should be a color. " err += "Got `{}`.".format(urgent_border) raise ValueError(err) self.i3bar_gaps_urgent_options["border"] = color urgent_borders = ["top", "right", "bottom", "left"] for name in ["urgent_border_" + x for x in urgent_borders]: param = fn(self.module_full_name, name) if hasattr(param, "none_setting"): param = 1 elif not isinstance(param, int): err = "Invalid `{}` attribute, ".format(name) err += "should be an int. " err += "Got `{}`.".format(param) raise TypeError(err) self.i3bar_gaps_urgent_options[name[7:]] = param # get the available methods for execution for method in sorted(dir(class_inst)): if method.startswith("_"): continue else: m_type = type(getattr(class_inst, method)) if "method" in str(m_type): params_type = self._params_type(method, class_inst) if method == "on_click": self.click_events = params_type elif method == "kill": self.has_kill = params_type elif method == "post_config_hook": self.has_post_config_hook = True else: # the method_obj stores infos about each method # of this module. method_obj = { "cached_until": time(), "call_type": params_type, "instance": None, "last_output": { "name": method, "full_text": "" }, "method": method, "name": None, } self.methods[method] = method_obj # done, log some debug info if self.config["debug"]: self._py3_wrapper.log( 'module "{}" click_events={} has_kill={} methods={}'.format( module, self.click_events, self.has_kill, self.methods.keys()))
def load_methods(self, module, user_modules): """ Read the given user-written py3status class file and store its methods. Those methods will be executed, so we will deliberately ignore: - private methods starting with _ - decorated methods such as @property or @staticmethod - 'on_click' methods as they'll be called upon a click_event - 'kill' methods as they'll be called upon this thread's exit """ if not self.module_class: # user provided modules take precedence over py3status provided modules if self.module_name in user_modules: include_path, f_name = user_modules[self.module_name] self._py3_wrapper.log('loading module "{}" from {}{}'.format( module, include_path, f_name)) self.module_class = self.load_from_file(include_path + f_name) # load from py3status provided modules else: self._py3_wrapper.log( 'loading module "{}" from py3status.modules.{}'.format( module, self.module_name)) self.module_class = self.load_from_namespace(self.module_name) class_inst = self.module_class if class_inst: try: # containers have items attribute set to a list of contained # module instance names. If there are no contained items then # ensure that we have a empty list. if class_inst.Meta.container: class_inst.items = [] except AttributeError: pass # module configuration mod_config = self.config["py3_config"].get(module, {}) # process any deprecated configuration settings try: deprecated = class_inst.Meta.deprecated except AttributeError: deprecated = None if deprecated: def deprecation_log(item): # log the deprecation # currently this is just done to the log file as the user # does not need to take any action. if "msg" in item: msg = item["msg"] param = item.get("param") if param: msg = "`{}` {}".format(param, msg) msg = "DEPRECATION WARNING: {} {}".format( self.module_full_name, msg) self._py3_wrapper.log(msg) if "rename" in deprecated: # renamed parameters for item in deprecated["rename"]: param = item["param"] new_name = item["new"] if param in mod_config: if new_name not in mod_config: mod_config[new_name] = mod_config[param] # remove from config del mod_config[param] deprecation_log(item) if "format_fix_unnamed_param" in deprecated: # format update where {} was previously allowed for item in deprecated["format_fix_unnamed_param"]: param = item["param"] placeholder = item["placeholder"] if "{}" in mod_config.get(param, ""): mod_config[param] = mod_config[param].replace( "{}", "{%s}" % placeholder) deprecation_log(item) if "rename_placeholder" in deprecated: # rename placeholders placeholders = {} for item in deprecated["rename_placeholder"]: placeholders[item["placeholder"]] = item["new"] format_strings = item["format_strings"] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholders( format_string, placeholders) mod_config[format_param] = format if "update_placeholder_format" in deprecated: # update formats for placeholders if a format is not set for item in deprecated["update_placeholder_format"]: placeholder_formats = item.get("placeholder_formats", {}) if "function" in item: placeholder_formats.update( item["function"](mod_config)) format_strings = item["format_strings"] for format_param in format_strings: format_string = mod_config.get(format_param) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) mod_config[format_param] = format if "substitute_by_value" in deprecated: # one parameter sets the value of another for item in deprecated["substitute_by_value"]: param = item["param"] value = item["value"] substitute = item["substitute"] substitute_param = substitute["param"] substitute_value = substitute["value"] if (mod_config.get(param) == value and substitute_param not in mod_config): mod_config[substitute_param] = substitute_value deprecation_log(item) if "function" in deprecated: # parameter set by function for item in deprecated["function"]: updates = item["function"](mod_config) for name, value in updates.items(): if name not in mod_config: mod_config[name] = value if "remove" in deprecated: # obsolete parameters forcibly removed for item in deprecated["remove"]: param = item["param"] if param in mod_config: del mod_config[param] deprecation_log(item) # apply module configuration for config, value in mod_config.items(): # names starting with '.' are private if not config.startswith("."): setattr(self.module_class, config, value) # process any update_config settings try: update_config = class_inst.Meta.update_config except AttributeError: update_config = None if update_config: if "update_placeholder_format" in update_config: # update formats for placeholders if a format is not set for item in update_config["update_placeholder_format"]: placeholder_formats = item.get("placeholder_formats", {}) format_strings = item["format_strings"] for format_param in format_strings: format_string = getattr(class_inst, format_param, None) if not format_string: continue format = Formatter().update_placeholder_formats( format_string, placeholder_formats) setattr(class_inst, format_param, format) # Add the py3 module helper if modules self.py3 is not defined if not hasattr(self.module_class, "py3"): setattr(self.module_class, "py3", Py3(self)) # allow_urgent # get the value form the config or use the module default if # supplied. fn = self._py3_wrapper.get_config_attribute param = fn(self.module_full_name, "allow_urgent") if hasattr(param, "none_setting"): param = True self.allow_urgent = param # get the available methods for execution for method in sorted(dir(class_inst)): if method.startswith("_"): continue else: m_type = type(getattr(class_inst, method)) if "method" in str(m_type): params_type = self._params_type(method, class_inst) if method == "on_click": self.click_events = params_type elif method == "kill": self.has_kill = params_type elif method == "post_config_hook": self.has_post_config_hook = True else: # the method_obj stores infos about each method # of this module. method_obj = { "cached_until": time(), "call_type": params_type, "instance": None, "last_output": { "name": method, "full_text": "" }, "method": method, "name": None, } self.methods[method] = method_obj # done, log some debug info if self.config["debug"]: self._py3_wrapper.log( 'module "{}" click_events={} has_kill={} methods={}'.format( module, self.click_events, self.has_kill, self.methods.keys()))
class Py3: """ Helper object that gets injected as self.py3 into Py3status modules that have not got that attribute set already. This allows functionality like: User notifications Forcing module to update (even other modules) Triggering events for modules Py3 is also used for testing in which case it does not get a module when being created. All methods should work in this situation. """ CACHE_FOREVER = PY3_CACHE_FOREVER LOG_ERROR = PY3_LOG_ERROR LOG_INFO = PY3_LOG_INFO LOG_WARNING = PY3_LOG_WARNING # All Py3 Instances can share a formatter _formatter = Formatter() def __init__(self, module=None, i3s_config=None, py3status=None): self._audio = None self._colors = {} self._i3s_config = i3s_config or {} self._module = module self._is_python_2 = sys.version_info < (3, 0) if py3status: self._py3status_module = py3status # we are running through the whole stack. # If testing then module is None. if module: self._output_modules = module._py3_wrapper.output_modules if not i3s_config: config = self._module.i3status_thread.config['general'] self._i3s_config = config self._py3status_module = module.module_class def __getattr__(self, name): """ Py3 can provide COLOR constants eg COLOR_GOOD, COLOR_BAD, COLOR_DEGRADED but also any constant COLOR_XXX we find this color in the config if it exists """ if not name.startswith('COLOR_'): raise AttributeError name = name.lower() if name not in self._colors: if self._module: color_fn = self._module._py3_wrapper.get_config_attribute color = color_fn(self._module.module_full_name, name) else: color = self._i3s_config.get(name) self._colors[name] = color return self._colors[name] def _get_module_info(self, module_name): """ THIS IS PRIVATE AND UNSUPPORTED. Get info for named module. Info comes back as a dict containing. 'module': the instance of the module, 'position': list of places in i3bar, usually only one item 'type': module type py3status/i3status """ if self._module: return self._output_modules.get(module_name) def i3s_config(self): """ returns the i3s_config dict. """ return self._i3s_config def is_python_2(self): """ True if the version of python being used is 2.x Can be helpful for fixing python 2 compatability issues """ return self._is_python_2 def is_my_event(self, event): """ Checks if an event triggered belongs to the module recieving it. This is mainly for containers who will also recieve events from any children they have. Returns True if the event name and instance match that of the module checking. """ if not self._module: return False return (event.get('name') == self._module.module_name and event.get('instance') == self._module.module_inst) def log(self, message, level=LOG_INFO): """ Log the message. The level must be one of LOG_ERROR, LOG_INFO or LOG_WARNING """ assert level in [self.LOG_ERROR, self.LOG_INFO, self.LOG_WARNING ], 'level must be LOG_ERROR, LOG_INFO or LOG_WARNING' if self._module: message = 'Module `{}`: {}'.format(self._module.module_full_name, message) self._module._py3_wrapper.log(message, level) def update(self, module_name=None): """ Update a module. If module_name is supplied the module of that name is updated. Otherwise the module calling is updated. """ if not module_name and self._module: return self._module.force_update() else: module_info = self._get_module_info(module_name) if module_info: module_info['module'].force_update() def get_output(self, module_name): """ Return the output of the named module. This will be a list. """ output = [] module_info = self._get_module_info(module_name) if module_info: output = module_info['module'].get_latest() return output def trigger_event(self, module_name, event): """ Trigger an event on a named module. """ if module_name and self._module: self._module._py3_wrapper.events_thread.process_event( module_name, event) def prevent_refresh(self): """ Calling this function during the on_click() method of a module will request that the module is not refreshed after the event. By default the module is updated after the on_click event has been processed. """ if self._module: self._module.prevent_refresh = True def notify_user(self, msg, level='info', rate_limit=5): """ Send a notification to the user. level must be 'info', 'error' or 'warning'. rate_limit is the time period in seconds during which this message should not be repeated. """ if self._module: # force unicode for python2 str if self._is_python_2 and isinstance(msg, str): msg = msg.decode('utf-8') module_name = self._module.module_full_name self._module._py3_wrapper.notify_user(msg, level=level, rate_limit=rate_limit, module_name=module_name) def register_content_function(self, content_function): """ Register a function that can be called to discover what modules a container is displaying. This is used to determine when updates need passing on to the container and also when modules can be put to sleep. the function must return a set of module names that are being displayed. Note: This function should only be used by containers. """ if self._module: my_info = self._get_module_info(self._module.module_full_name) my_info['content_function'] = content_function def time_in(self, seconds=None, sync_to=None, offset=0): """ Returns the time a given number of seconds into the future. Helpful for creating the `cached_until` value for the module output. Note: form version 3.1 modules no longer need to explicitly set a `cached_until` in their response unless they wish to directly control it. seconds specifies the number of seconds that should occure before the update is required. sync_to causes the update to be syncronised to a time period. 1 would cause the update on the second, 60 to the nearest minute. By defalt we syncronise to the nearest second. 0 will disable this feature. offset is used to alter the base time used. A timer that started at a certain time could set that as the offset and any syncronisation would then be relative to that time. """ if seconds is None: # If we have a sync_to then seconds can be 0 if sync_to and sync_to > 0: seconds = 0 else: try: # use py3status modules cache_timeout seconds = self._py3status_module.cache_timeout except AttributeError: # use default cache_timeout seconds = self._module.config['cache_timeout'] # Unless explicitly set we sync to the nearest second # Unless the requested update is in less than a second if sync_to is None: if seconds and seconds < 1: sync_to = 0 else: sync_to = 1 requested = time() + seconds - offset # if sync_to then we find the sync time for the requested time if sync_to: requested = (requested + sync_to) - (requested % sync_to) return requested + offset def safe_format(self, format_string, param_dict=None): """ Parser for advanced formating. Unknown placeholders will be shown in the output eg `{foo}` Square brackets `[]` can be used. The content of them will be removed from the output if there is no valid placeholder contained within. They can also be nested. A pipe (vertical bar) `|` can be used to divide sections the first valid section only will be shown in the output. A backslash `\` can be used to escape a character eg `\[` will show `[` in the output. Note: `\?` is reserved for future use and is removed. `{<placeholder>}` will be converted, or removed if it is None or empty. Formating can also be applied to the placeholder eg `{number:03.2f}`. example format_string: "[[{artist} - ]{title}]|{file}" This will show `artist - title` if artist is present, `title` if title but no artist, and `file` if file is present but not artist or title. param_dict is a dictionary of palceholders that will be substituted. If a placeholder is not in the dictionary then if the py3status module has an attribute with the same name then it will be used. """ try: return self._formatter.format(format_string, self._py3status_module, param_dict) except Exception: return 'invalid format' def build_composite(self, format_string, param_dict=None, composites=None): """ Build a composite output using a format string. Takes a format_string and treats it the same way as `safe_format` but also takes a composites dict where each key/value is the name of the placeholder and either an output eg `{'full_text': 'something'}` or a list of outputs. """ try: return self._formatter.format( format_string, self._py3status_module, param_dict, composites, ) except Exception: return [{'full_text': 'invalid format'}] def check_commands(self, cmd_list): """ Checks to see if commands in list are available using `which`. Returns the first available command. """ devnull = open(os.devnull, 'w') for cmd in cmd_list: c = shlex.split('which {}'.format(cmd)) if call(c, stdout=devnull, stderr=devnull) == 0: return cmd def play_sound(self, sound_file): """ Plays sound_file if possible. """ self.stop_sound() cmd = self.check_commands(['paplay', 'play']) if cmd: sound_file = os.path.expanduser(sound_file) c = shlex.split('{} {}'.format(cmd, sound_file)) self._audio = Popen(c) def stop_sound(self): """ Stops any currently playing sounds for this module. """ if self._audio: self._audio.kill() self._audio = None