示例#1
0
    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)
示例#2
0
文件: py3.py 项目: onny/py3status
    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)
示例#3
0
文件: py3.py 项目: blahgeek/py3status
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
示例#4
0
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":
示例#5
0
    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()))
示例#6
0
    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()))
示例#7
0
    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()))
示例#8
0
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