<commit_before>"""Contains the BallController class which manages and tracks all the balls in a pinball machine."""

import asyncio

from mpf.core.delays import DelayManager
from mpf.core.machine import MachineController
from mpf.core.utility_functions import Util
from mpf.core.mpf_controller import MpfController

class BallController(MpfController):

    """Base class for the Ball Controller which is used to keep track of all the balls in a pinball machine.

    Parameters
    ----------

    machine : :class:`MachineController`
        A reference to the instance of the MachineController object.
    """

    def __init__(self, machine: MachineController) -> None:
        """Initialise ball controller."""
        super().__init__(machine)

        self.delay = DelayManager(self.machine.delayRegistry)

        self.num_balls_known = 0

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)
        self.machine.events.add_handler('machine_reset_phase_2',
                                        self._initialize)
        self.machine.events.add_handler('init_phase_4',
                                        self._init4, priority=100)

        self.machine.events.add_handler('shutdown',
                                        self._stop)

        self._add_new_balls_task = None                                         # type: asyncio.Task
        self._captured_balls = asyncio.Queue(loop=self.machine.clock.loop)      # type: asyncio.Queue

    def _init4(self, **kwargs):
        del kwargs

        # see if there are non-playfield devices
        for device in self.machine.ball_devices:
            if device.is_playfield():
                continue
            # found a device
            break
        else:
            # there is no non-playfield device. end this.
            return

        self._add_new_balls_task = self.machine.clock.loop.create_task(self._add_new_balls_to_playfield())
        self._add_new_balls_task.add_done_callback(self._done)

    def _stop(self, **kwargs):
        del kwargs
        if self._add_new_balls_task:
            self._add_new_balls_task.cancel()

    @staticmethod
    def _done(future):
        try:
            future.result()
        except asyncio.CancelledError:
            pass

    def _get_total_balls_in_devices(self):
        balls = 0
        # get count for all ball devices
        for device in self.machine.ball_devices:
            if device.is_playfield():
                continue

            balls += device.counter.count_balls_sync()
        return balls

    def add_captured_ball(self, source):
        """Inform ball controller about a capured ball (which might be new)."""
        self._captured_balls.put_nowait(source)

    @asyncio.coroutine
    def _add_new_balls_to_playfield(self):
        # initial count
        self.num_balls_known = yield from self._count_all_balls_in_devices()

        while True:
            capture = yield from self._captured_balls.get()
            balls = yield from self._count_all_balls_in_devices()
            if balls > self.num_balls_known:
                self.num_balls_known += 1
                capture.balls += 1
                capture.available_balls += 1
                self.info_log("Found a new ball which was captured from %s. Known balls: %s", capture.name,
                              self.num_balls_known)

            self._balance_playfields()

    def _balance_playfields(self):
        # find negative counts
        for playfield_target in self.machine.playfields:
            if playfield_target.balls < 0:
                for playfield_source in self.machine.playfields:
                    if playfield_source.balls > 0:
                        playfield_source.balls -= 1
                        playfield_source.available_balls -= 1
                        playfield_target.balls += 1
                        playfield_target.available_balls += 1
                        # post event
                        self.machine.events.post("playfield_jump", source=playfield_source, target=playfield_target)
                        break

    @asyncio.coroutine
    def _count_all_balls_in_devices(self):
        """Count balls in all devices."""
        while True:
            # wait until all devices are stable
            # prepare futures in case we have to wait to prevent race between count and building futures
            futures = []
            for device in self.machine.ball_devices:
                if not device.is_playfield():
                    futures.append(Util.ensure_future(device.counter.wait_for_ball_activity(),
                                                      loop=self.machine.clock.loop))

            try:
                return self._get_total_balls_in_devices()
            except ValueError:
                yield from Util.first(futures, self.machine.clock.loop)
                continue

    def _count_balls(self):
        self.debug_log("Counting Balls")
        balls = 0

        for device in self.machine.ball_devices:
            # generally we do not count ball devices without switches
            if 'ball_switches' not in device.config:
                continue
            # special handling for troughs (needed for gottlieb)
            elif not device.config['ball_switches'] and 'trough' in device.tags:
                balls += device.balls
            else:
                for switch in device.config['ball_switches']:
                    if self.machine.switch_controller.is_active(
                            switch.name, ms=device.config['entrance_count_delay']):
                        balls += 1
                    elif self.machine.switch_controller.is_inactive(
                            switch.name, ms=device.config['exit_count_delay']):
                        continue
                    else:
                        raise ValueError("switches not stable")

        return balls

    def _initialize(self, **kwargs):

        # If there are no ball devices, then the ball controller has no work to
        # do and will create errors, so we just abort.
        del kwargs
        if not hasattr(self.machine, 'ball_devices'):
            return

        for device in self.machine.ball_devices:
            if 'drain' in device.tags:  # device is used to drain balls from pf
                self.machine.events.add_handler('balldevice_' + device.name +
                                                '_ball_enter',
                                                self._ball_drained_handler, priority=20)

    def dump_ball_counts(self):
        """Dump ball count of all devices."""
        for device in self.machine.ball_devices:
            self.info_log("%s contains %s balls. Tags %s", device.name, device.balls, device.tags)

    def request_to_start_game(self, **kwargs):
        """Method registered for the *request_to_start_game* event.

        Checks to make sure that the balls are in all the right places and
        returns. If too many balls are missing (based on the config files 'Min
        Balls' setting), it will return False to reject the game start request.
        """
        del kwargs
        try:
            balls = self._count_balls()
        except ValueError:
            balls = -1
        self.debug_log("Received request to start game.")
        if balls < self.machine.config['machine']['min_balls']:
            self.dump_ball_counts()
            self.warning_log("BallController denies game start. Not enough "
                             "balls. %s found. %s required", balls, self.machine.config['machine']['min_balls'])
            return False

        if self.machine.config['game']['allow_start_with_ball_in_drain']:
            allowed_positions = ['home', 'trough', 'drain']
        else:
            allowed_positions = ['home', 'trough']

        if self.machine.config['game']['allow_start_with_loose_balls']:
            return

        elif not self.are_balls_collected(allowed_positions):
            self.collect_balls('home')
            self.dump_ball_counts()
            self.warning_log("BallController denies game start. Balls are not "
                             "in their home positions.")
            return False

    def are_balls_collected(self, target):
        """Check to see if all the balls are contained in devices tagged with the parameter that was passed.

        Note if you pass a target that's not used in any ball devices, this
        method will return True. (Because you're asking if all balls are
        nowhere, and they always are. :)

        Args:
            target: String or list of strings of the tags you'd like to
                collect the balls to. Default of None will be replaced with
                'home' and 'trough'.
        """
        self.debug_log("Checking to see if all the balls are in devices tagged"
                       " with '%s'", target)

        if isinstance(target, str):
            target = Util.string_to_list(target)

        count = 0
        devices = set()

        for tag in target:
            for device in self.machine.ball_devices.items_tagged(tag):
                devices.add(device)

        if len(devices) == 0:
            # didn't find any devices matching that tag, so we return True
            return True

        for device in devices:
            count += device.balls
            self.debug_log('Found %s ball(s) in %s. Found %s total',
                           device.balls, device.name, count)

        if count == self.machine.ball_controller.num_balls_known:
            self.debug_log("Yes, all balls are collected")
            return True
        else:
            self.debug_log("No, all balls are not collected. Balls Counted: %s. "
                           "Total balls known: %s", count,
                           self.machine.ball_controller.num_balls_known)
            return False

    def collect_balls(self, target='home, trough'):
        """Used to ensure that all balls are in contained in ball devices with the tag or list of tags you pass.

        Typically this would be used after a game ends, or when the machine is
        reset or first starts up, to ensure that all balls are in devices
        tagged with 'home' and/or 'trough'.

        Args:
            target: A string of the tag name or a list of tags names of the
                ball devices you want all the balls to end up in. Default is
                ['home', 'trough'].

        """
        tag_list = Util.string_to_list(target)

        self.debug_log("Collecting all balls to devices with tags '%s'",
                       tag_list)

        target_devices = set()
        source_devices = set()
        balls_to_collect = False

        for tag in tag_list:
            for device in self.machine.ball_devices.items_tagged(tag):
                target_devices.add(device)

        for device in self.machine.ball_devices:
            if device not in target_devices:
                if device.available_balls:
                    source_devices.add(device)
                    balls_to_collect = True

        if balls_to_collect:
            self.debug_log("Ejecting all balls from: %s", source_devices)

            self.machine.events.post('collecting_balls')
            '''event: collecting_balls

            desc: Posted by the ball controller when it starts the collecting
                balls process.

            '''

            for device in target_devices:
                self.machine.events.remove_handler(self._collecting_balls_entered_callback)
                self.machine.events.add_handler(
                    'balldevice_{}_ball_enter'.format(device.name),
                    self._collecting_balls_entered_callback,
                    target=target, priority=10)

            for device in source_devices:
                if not device.is_playfield():
                    if "drain" in device.tags:
                        device.eject_all(device.find_next_trough())
                    else:
                        device.eject_all()
        else:
            self.debug_log("All balls are collected")
            self._collecting_balls_complete()

    def _collecting_balls_entered_callback(self, target, new_balls, unclaimed_balls, **kwargs):
        del kwargs
        del new_balls
        if self.are_balls_collected(target=target):
            self._collecting_balls_complete()

        return {'unclaimed_balls': unclaimed_balls}

    def _collecting_balls_complete(self):
        self.machine.events.remove_handler(self._collecting_balls_entered_callback)
        self.machine.events.post('collecting_balls_complete')
        '''event: collecting_balls_complete

        desc: Posted by the ball controller when it has finished the collecting
            balls process.

        '''

    def _ball_drained_handler(self, new_balls, unclaimed_balls, device, **kwargs):
        del kwargs
        del new_balls
        self.machine.events.post_relay('ball_drain',
                                       device=device,
                                       balls=unclaimed_balls)
        '''event: ball_drain

        desc: A ball (or balls) has just drained. (More specifically, ball(s)
        have entered a ball device tagged with "drain".)

        This is a relay event.

        args:

        device: The ball device object that received the ball(s)

        balls: The number of balls that have just drained. Any balls remaining
        after the relay will be processed as newly-drained balls.

        '''
<commit_msg>explicit order<commit_after>"""Contains the BallController class which manages and tracks all the balls in a pinball machine."""

import asyncio

from mpf.core.delays import DelayManager
from mpf.core.machine import MachineController
from mpf.core.utility_functions import Util
from mpf.core.mpf_controller import MpfController

class BallController(MpfController):

    """Base class for the Ball Controller which is used to keep track of all the balls in a pinball machine.

    Parameters
    ----------

    machine : :class:`MachineController`
        A reference to the instance of the MachineController object.
    """

    def __init__(self, machine: MachineController) -> None:
        """Initialise ball controller."""
        super().__init__(machine)

        self.delay = DelayManager(self.machine.delayRegistry)

        self.num_balls_known = 0

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)
        self.machine.events.add_handler('machine_reset_phase_2',
                                        self._initialize)
        self.machine.events.add_handler('init_phase_4',
                                        self._init4, priority=100)

        self.machine.events.add_handler('shutdown',
                                        self._stop)

        self._add_new_balls_task = None                                         # type: asyncio.Task
        self._captured_balls = asyncio.Queue(loop=self.machine.clock.loop)      # type: asyncio.Queue

    def _init4(self, **kwargs):
        del kwargs

        # see if there are non-playfield devices
        for device in self.machine.ball_devices:
            if device.is_playfield():
                continue
            # found a device
            break
        else:
            # there is no non-playfield device. end this.
            return

        self._add_new_balls_task = self.machine.clock.loop.create_task(self._add_new_balls_to_playfield())
        self._add_new_balls_task.add_done_callback(self._done)

    def _stop(self, **kwargs):
        del kwargs
        if self._add_new_balls_task:
            self._add_new_balls_task.cancel()

    @staticmethod
    def _done(future):
        try:
            future.result()
        except asyncio.CancelledError:
            pass

    def _get_total_balls_in_devices(self):
        balls = 0
        # get count for all ball devices
        for device in self.machine.ball_devices:
            if device.is_playfield():
                continue

            balls += device.counter.count_balls_sync()
        return balls

    def add_captured_ball(self, source):
        """Inform ball controller about a capured ball (which might be new)."""
        self._captured_balls.put_nowait(source)

    @asyncio.coroutine
    def _add_new_balls_to_playfield(self):
        # initial count
        self.num_balls_known = yield from self._count_all_balls_in_devices()

        while True:
            capture = yield from self._captured_balls.get()
            balls = yield from self._count_all_balls_in_devices()
            if balls > self.num_balls_known:
                self.num_balls_known += 1
                capture.balls += 1
                capture.available_balls += 1
                self.info_log("Found a new ball which was captured from %s. Known balls: %s", capture.name,
                              self.num_balls_known)

            self._balance_playfields()

    def _balance_playfields(self):
        # find negative counts
        for playfield_target in self.machine.playfields:
            if playfield_target.balls < 0:
                for playfield_source in self.machine.playfields:
                    if playfield_source.balls > 0:
                        playfield_source.balls -= 1
                        playfield_source.available_balls -= 1
                        playfield_target.balls += 1
                        playfield_target.available_balls += 1
                        # post event
                        self.machine.events.post("playfield_jump", source=playfield_source, target=playfield_target)
                        break

    @asyncio.coroutine
    def _count_all_balls_in_devices(self):
        """Count balls in all devices."""
        while True:
            # wait until all devices are stable
            # prepare futures in case we have to wait to prevent race between count and building futures
            futures = []
            for device in self.machine.ball_devices:
                if not device.is_playfield():
                    futures.append(Util.ensure_future(device.counter.wait_for_ball_activity(),
                                                      loop=self.machine.clock.loop))

            try:
                return self._get_total_balls_in_devices()
            except ValueError:
                yield from Util.first(futures, self.machine.clock.loop)
                continue

    def _count_balls(self):
        self.debug_log("Counting Balls")
        balls = 0

        for device in self.machine.ball_devices:
            # generally we do not count ball devices without switches
            if 'ball_switches' not in device.config:
                continue
            # special handling for troughs (needed for gottlieb)
            elif not device.config['ball_switches'] and 'trough' in device.tags:
                balls += device.balls
            else:
                for switch in device.config['ball_switches']:
                    if self.machine.switch_controller.is_active(
                            switch.name, ms=device.config['entrance_count_delay']):
                        balls += 1
                    elif self.machine.switch_controller.is_inactive(
                            switch.name, ms=device.config['exit_count_delay']):
                        continue
                    else:
                        raise ValueError("switches not stable")

        return balls

    def _initialize(self, **kwargs):

        # If there are no ball devices, then the ball controller has no work to
        # do and will create errors, so we just abort.
        del kwargs
        if not hasattr(self.machine, 'ball_devices'):
            return

        for device in self.machine.ball_devices:
            prio = 0
            if 'drain' in device.tags:  # device is used to drain balls from pf
                prio += 1   # order handlers
                self.machine.events.add_handler('balldevice_' + device.name +
                                                '_ball_enter',
                                                self._ball_drained_handler, priority=20 + prio)

    def dump_ball_counts(self):
        """Dump ball count of all devices."""
        for device in self.machine.ball_devices:
            self.info_log("%s contains %s balls. Tags %s", device.name, device.balls, device.tags)

    def request_to_start_game(self, **kwargs):
        """Method registered for the *request_to_start_game* event.

        Checks to make sure that the balls are in all the right places and
        returns. If too many balls are missing (based on the config files 'Min
        Balls' setting), it will return False to reject the game start request.
        """
        del kwargs
        try:
            balls = self._count_balls()
        except ValueError:
            balls = -1
        self.debug_log("Received request to start game.")
        if balls < self.machine.config['machine']['min_balls']:
            self.dump_ball_counts()
            self.warning_log("BallController denies game start. Not enough "
                             "balls. %s found. %s required", balls, self.machine.config['machine']['min_balls'])
            return False

        if self.machine.config['game']['allow_start_with_ball_in_drain']:
            allowed_positions = ['home', 'trough', 'drain']
        else:
            allowed_positions = ['home', 'trough']

        if self.machine.config['game']['allow_start_with_loose_balls']:
            return

        elif not self.are_balls_collected(allowed_positions):
            self.collect_balls('home')
            self.dump_ball_counts()
            self.warning_log("BallController denies game start. Balls are not "
                             "in their home positions.")
            return False

    def are_balls_collected(self, target):
        """Check to see if all the balls are contained in devices tagged with the parameter that was passed.

        Note if you pass a target that's not used in any ball devices, this
        method will return True. (Because you're asking if all balls are
        nowhere, and they always are. :)

        Args:
            target: String or list of strings of the tags you'd like to
                collect the balls to. Default of None will be replaced with
                'home' and 'trough'.
        """
        self.debug_log("Checking to see if all the balls are in devices tagged"
                       " with '%s'", target)

        if isinstance(target, str):
            target = Util.string_to_list(target)

        count = 0
        devices = set()

        for tag in target:
            for device in self.machine.ball_devices.items_tagged(tag):
                devices.add(device)

        if len(devices) == 0:
            # didn't find any devices matching that tag, so we return True
            return True

        for device in devices:
            count += device.balls
            self.debug_log('Found %s ball(s) in %s. Found %s total',
                           device.balls, device.name, count)

        if count == self.machine.ball_controller.num_balls_known:
            self.debug_log("Yes, all balls are collected")
            return True
        else:
            self.debug_log("No, all balls are not collected. Balls Counted: %s. "
                           "Total balls known: %s", count,
                           self.machine.ball_controller.num_balls_known)
            return False

    def collect_balls(self, target='home, trough'):
        """Used to ensure that all balls are in contained in ball devices with the tag or list of tags you pass.

        Typically this would be used after a game ends, or when the machine is
        reset or first starts up, to ensure that all balls are in devices
        tagged with 'home' and/or 'trough'.

        Args:
            target: A string of the tag name or a list of tags names of the
                ball devices you want all the balls to end up in. Default is
                ['home', 'trough'].

        """
        tag_list = Util.string_to_list(target)

        self.debug_log("Collecting all balls to devices with tags '%s'",
                       tag_list)

        target_devices = set()
        source_devices = set()
        balls_to_collect = False

        for tag in tag_list:
            for device in self.machine.ball_devices.items_tagged(tag):
                target_devices.add(device)

        for device in self.machine.ball_devices:
            if device not in target_devices:
                if device.available_balls:
                    source_devices.add(device)
                    balls_to_collect = True

        if balls_to_collect:
            self.debug_log("Ejecting all balls from: %s", source_devices)

            self.machine.events.post('collecting_balls')
            '''event: collecting_balls

            desc: Posted by the ball controller when it starts the collecting
                balls process.

            '''

            for device in target_devices:
                self.machine.events.remove_handler(self._collecting_balls_entered_callback)
                self.machine.events.add_handler(
                    'balldevice_{}_ball_enter'.format(device.name),
                    self._collecting_balls_entered_callback,
                    target=target, priority=10)

            for device in source_devices:
                if not device.is_playfield():
                    if "drain" in device.tags:
                        device.eject_all(device.find_next_trough())
                    else:
                        device.eject_all()
        else:
            self.debug_log("All balls are collected")
            self._collecting_balls_complete()

    def _collecting_balls_entered_callback(self, target, new_balls, unclaimed_balls, **kwargs):
        del kwargs
        del new_balls
        if self.are_balls_collected(target=target):
            self._collecting_balls_complete()

        return {'unclaimed_balls': unclaimed_balls}

    def _collecting_balls_complete(self):
        self.machine.events.remove_handler(self._collecting_balls_entered_callback)
        self.machine.events.post('collecting_balls_complete')
        '''event: collecting_balls_complete

        desc: Posted by the ball controller when it has finished the collecting
            balls process.

        '''

    def _ball_drained_handler(self, new_balls, unclaimed_balls, device, **kwargs):
        del kwargs
        del new_balls
        self.machine.events.post_relay('ball_drain',
                                       device=device,
                                       balls=unclaimed_balls)
        '''event: ball_drain

        desc: A ball (or balls) has just drained. (More specifically, ball(s)
        have entered a ball device tagged with "drain".)

        This is a relay event.

        args:

        device: The ball device object that received the ball(s)

        balls: The number of balls that have just drained. Any balls remaining
        after the relay will be processed as newly-drained balls.

        '''
<|endoftext|>


<commit_before>""" Contains the MatrixLight parent classes. """

from operator import itemgetter

from mpf.core.system_wide_device import SystemWideDevice

class MatrixLight(SystemWideDevice):
    """ Represents a light connected to a traditional lamp matrix in a pinball
    machine.

    This light could be an incandescent lamp or a replacement single-color
    LED. The key is that they're connected up to a lamp matrix.

    """

    config_section = 'matrix_lights'
    collection = 'lights'
    class_label = 'light'
    machine = None

    lights_to_update = set()

    @classmethod
    def device_class_init(cls, machine):

        cls.machine = machine

        machine.validate_machine_config_section('matrix_light_settings')

        # todo make time configurable
        machine.clock.schedule_interval(cls.update_matrix_lights, 0, -100)

        machine.mode_controller.register_stop_method(cls.mode_stop)

    @classmethod
    def update_matrix_lights(cls, dt):
        del dt
        # called periodically (default at the end of every frame) to actually
        # write the new light states to the hardware
        if MatrixLight.lights_to_update:
            for light in MatrixLight.lights_to_update:
                light.update_hw_light()

            MatrixLight.lights_to_update = set()

    @classmethod
    def mode_stop(cls, mode):
        for light in cls.machine.lights:
            light.remove_from_stack_by_mode(mode)

    def __init__(self, machine, name):
        super().__init__(machine, name)

        self.x = None
        self.y = None

        self.fade_in_progress = False
        self.default_fade_ms = None

        self.registered_handlers = list()

        self.stack = list()
        """A list of dicts which represents different commands that have come
        in to set this light to a certain brightness (and/or fade). Each entry
        in the list contains the following key/value pairs:

        priority: The relative priority of this brightness command. Higher
            numbers take precedent, and the highest priority entry will be the
            command that's currently active. In the event of a tie,
            whichever entry was added last wins (based on 'start_time' below).
        start_time: The clock time when this command was added. Primarily used
            to calculate fades, but also used as a tie-breaker for multiple
            entries with the same priority.
        start_brightness: Brightness this light when this command came in.
        dest_time: Clock time that represents when a fade (from
            start_brightness to dest_brightness ) will be done. If this is 0,
            that means there is no fade. When a fade is complete, this value is
             reset to 0.
        dest_brightness: Brightness of the destination this light is fading
            to. If a command comes in with no fade, then this will be the same
            as the 'brightness' below.
        brightness: The current brightness of the light based on this command.
            (0-255) This value is updated automatically as fades progress, and
            it's the value that's actually written to the hardware.
        key: An arbitrary unique identifier to keep multiple entries in the
            stack separate. If a new brightness command comes in with a key
            that already exists for an entry in the stack, that entry will be
            replaced by the new entry. The key is also used to remove entries
            from the stack (e.g. when shows or modes end and they want to
            remove their commands from the light).
        mode: Optional mode where the brightness was set. Used to remove
            entries when a mode ends.

        """

    def _initialize(self):
        self.load_platform_section('matrix_lights')

        self.hw_driver = self.platform.configure_matrixlight(self.config)

        if self.config['fade_ms'] is not None:
            self.default_fade_ms = self.config['fade_ms']
        elif self.machine.config['matrix_light_settings']:
            self.default_fade_ms = (
                self.machine.config['matrix_light_settings']
                                    ['default_light_fade_ms'])
        else:
            self.default_fade_ms = 0

        if 'x' in self.config:
            self.x = self.config['x']

        if 'y' in self.config:
            self.y = self.config['y']

    def on(self, brightness=255, fade_ms=0, priority=0, key=None, mode=None,
           **kwargs):
        """Adds or updates a brightness entry in this lights's stack, which is
        how you tell this light how bright you want it to be.

        Args:
            brightness: How bright this light should be, as an int between 0
                and 255. 0 is off. 255 is full on. Note that matrix lights in
                older (even WPC) machines had slow matrix update speeds, and
                effective brightness levels will be far less than 255.
            fade_ms: Int of the number of ms you want this light to fade to the
                brightness in. A value of 0 means it's instant. A value of
                None (the default) means that it will use this lights's and/or
                the machine's default fade_ms setting.
            priority: Int value of the priority of these incoming settings. If
                this light has current settings in the stack at a higher
                priority, the settings you're adding here won't take effect.
                However they're still added to the stack, so if the higher
                priority settings are removed, then the next-highest apply.
            key: An arbitrary identifier (can be any immutable object) that's
                used to identify these settings for later removal. If any
                settings in the stack already have this key, those settings
                will be replaced with these new settings.
            mode: Optional mode instance of the mode that is setting this
                brightness. When a mode ends, entries from the stack with that
                mode will automatically be removed.
            **kwargs: Not used. Only included so this method can be used as
                an event callback since events could pass random kwargs.
        """

        del kwargs

        if self.debug:
            self.log.debug("Received on() command. brightness: %s, fade_ms: %s"
                           "priority: %s, key: %s", brightness, fade_ms,
                           priority, key)

        if fade_ms is None:
            fade_ms = self.default_fade_ms

        if priority < self._get_priority_from_key(key):
            if self.debug:
                self.log.debug("Incoming priority is lower than an existing "
                               "stack item with the same key. Not adding to "
                               "stack.")

            return

        self._add_to_stack(brightness, fade_ms, priority, key, mode)

    def _add_to_stack(self, brightness, fade_ms, priority, key, mode):
        curr_brightness = self.get_brightness()

        self.remove_from_stack_by_key(key)

        if fade_ms:
            new_brightness = curr_brightness
            dest_time = self.machine.clock.get_time() + (fade_ms / 1000)
        else:
            new_brightness = brightness
            dest_time = 0

        self.stack.append(dict(priority=priority,
                               start_time=self.machine.clock.get_time(),
                               start_brightness=curr_brightness,
                               dest_time=dest_time,
                               dest_brightness=brightness,
                               brightness=new_brightness,
                               key=key,
                               mode=mode))

        self.stack.sort(key=itemgetter('priority', 'start_time'), reverse=True)

        if self.debug:
            self.log.debug("+-------------- Adding to stack ----------------+")
            self.log.debug("priority: %s", priority)
            self.log.debug("start_time: %s", self.machine.clock.get_time())
            self.log.debug("start_brightness: %s", curr_brightness)
            self.log.debug("dest_time: %s", dest_time)
            self.log.debug("dest_brightness: %s", brightness)
            self.log.debug("brightness: %s", new_brightness)
            self.log.debug("key: %s", key)

        MatrixLight.lights_to_update.add(self)

    def clear_stack(self):
        """Removes all entries from the stack and resets this light to 'off'."""
        self.stack[:] = []

        if self.debug:
            self.log.debug("Clearing Stack")

        MatrixLight.lights_to_update.add(self)

    def remove_from_stack_by_key(self, key):
        """Removes a group of brightness settings from the stack.

        Args:
            key: The key of the settings to remove (based on the 'key'
                parameter that was originally passed to the brightness() method.)

        This method triggers a light update, so if the highest priority settings
        were removed, the light will be updated with whatever's below it. If no
        settings remain after these are removed, the light will turn off.

        """

        if self.debug:
            self.log.debug("Removing key '%s' from stack", key)

        self.stack[:] = [x for x in self.stack if x['key'] != key]
        MatrixLight.lights_to_update.add(self)

    def remove_from_stack_by_mode(self, mode):
        """Removes a group of brightness settings from the stack.

        Args:
            key: The key of the settings to remove (based on the 'key'
                parameter that was originally passed to the brightness() method.)

        This method triggers a light update, so if the highest priority settings
        were removed, the light will be updated with whatever's below it. If no
        settings remain after these are removed, the light will turn off.

        """

        if self.debug:
            self.log.debug("Removing mode '%s' from stack", mode)

        self.stack[:] = [x for x in self.stack if x['mode'] != mode]
        MatrixLight.lights_to_update.add(self)

    def get_brightness(self):
        """Returns an RGBColor() instance of the 'color' setting of the highest
        color setting in the stack. This is usually the same color as the
        physical LED, but not always (since physical LEDs are updated once per
        frame, this value could vary.

        Also note the color returned is the "raw" color that does has not had
        the color correction profile applied.

        """
        try:
            return self.stack[0]['brightness']
        except IndexError:
            return 0

    def _get_priority_from_key(self, key):
        try:
            return [x for x in self.stack if x['key'] == key][0]['priority']
        except IndexError:
            return 0

    def update_hw_light(self):
        """Physically updates the light hardware object based on the
        'brightness' setting of the highest priority setting from the stack.

        This method is automatically called whenever a brightness change has been
        made (including when fades are active).

        """
        if not self.stack:
            self.off()

        # if there's a current fade, but the new command doesn't have one
        if not self.stack[0]['dest_time'] and self.fade_in_progress:
            self._stop_fade_task()

        # If the new command has a fade, but the fade task isn't running
        if self.stack[0]['dest_time'] and not self.fade_in_progress:
            self._setup_fade()

        # If there's no current fade and no new fade, or a current fade and new
        # fade
        else:
            self.hw_driver.on(self.stack[0]['brightness'])

            if self.registered_handlers:
                # Handlers are not sent brightness corrected brightnesss
                # todo make this a config option?
                for handler in self.registered_handlers:
                    handler(light_name=self.name,
                            brightness=self.stack[0]['brightness'])

    def off(self, fade_ms=0, priority=0, key=None, mode=None, **kwargs):
        """Turns this light off.

        Args:
            fade_ms: Int of the number of ms you want this light to fade to the
                brightness in. A value of 0 means it's instant. A value of
                None (the default) means that it will use this lights's and/or
                the machine's default fade_ms setting.
            priority: Int value of the priority of these incoming settings. If
                this light has current settings in the stack at a higher
                priority, the settings you're adding here won't take effect.
                However they're still added to the stack, so if the higher
                priority settings are removed, then the next-highest apply.
            key: An arbitrary identifier (can be any immutable object) that's
                used to identify these settings for later removal. If any
                settings in the stack already have this key, those settings
                will be replaced with these new settings.
            mode: Optional mode instance of the mode that is setting this
                brightness. When a mode ends, entries from the stack with that
                mode will automatically be removed.
            **kwargs: Not used. Only included so this method can be used as
                an event callback since events could pass random kwargs.
        """
        self.on(brightness=0, fade_ms=fade_ms, priority=priority, key=key,
                mode=mode)

    def add_handler(self, callback):
        """Registers a handler to be called when this light changes state."""
        self.registered_handlers.append(callback)

    def remove_handler(self, callback=None):
        """Removes a handler from the list of registered handlers."""
        if not callback:  # remove all
            self.registered_handlers = []
            return

        if callback in self.registered_handlers:
            self.registered_handlers.remove(callback)

    def _setup_fade(self):

        if self.fade_in_progress:
            return

        self.fade_in_progress = True

        if self.debug:
            self.log.debug("Setting up the fade task")

        self.machine.clock.schedule_interval(self._fade_task, 0)

    def _fade_task(self, dt):
        del dt

        # not sure why this is needed, but sometimes the fade task tries to
        # run even though self.fade_in_progress is False. Maybe
        # clock.unschedule doesn't happen right away?
        if not self.fade_in_progress:
            return

        try:
            brightness_settings = self.stack[0]
        except IndexError:
            self._stop_fade_task()
            return

        # todo
        if not brightness_settings['dest_time']:
            return

        # figure out the ratio of how far along we are
        try:
            ratio = ((self.machine.clock.get_time() -
                      brightness_settings['start_time']) /
                     (brightness_settings['dest_time'] -
                      brightness_settings['start_time']))
        except ZeroDivisionError:
            ratio = 1.0

        if self.debug:
            self.log.debug("Fade task, ratio: %s", ratio)

        if ratio >= 1.0:  # fade is done
            self._end_fade()
            brightness_settings['brightness'] = (
                brightness_settings['dest_brightness'])
        else:
            brightness_settings['brightness'] = (
                brightness_settings['start_brightness'] +
                int((brightness_settings['dest_brightness'] -
                brightness_settings['start_brightness']) * ratio))

        MatrixLight.lights_to_update.add(self)

    def _end_fade(self):
        # stops the fade and instantly sets the light to its destination brightness
        self._stop_fade_task()
        self.stack[0]['dest_time'] = 0

    def _stop_fade_task(self):
        # stops the fade task. Light is left in whatever state it was in
        self.fade_in_progress = False
        self.machine.clock.unschedule(self._fade_task)

        if self.debug:
            self.log.debug("Stopping fade task")
<commit_msg>CS fix<commit_after>""" Contains the MatrixLight parent classes. """

from operator import itemgetter

from mpf.core.system_wide_device import SystemWideDevice

class MatrixLight(SystemWideDevice):
    """ Represents a light connected to a traditional lamp matrix in a pinball
    machine.

    This light could be an incandescent lamp or a replacement single-color
    LED. The key is that they're connected up to a lamp matrix.

    """

    config_section = 'matrix_lights'
    collection = 'lights'
    class_label = 'light'
    machine = None

    lights_to_update = set()

    @classmethod
    def device_class_init(cls, machine):

        cls.machine = machine

        machine.validate_machine_config_section('matrix_light_settings')

        # todo make time configurable
        machine.clock.schedule_interval(cls.update_matrix_lights, 0, -100)

        machine.mode_controller.register_stop_method(cls.mode_stop)

    @classmethod
    def update_matrix_lights(cls, dt):
        del dt
        # called periodically (default at the end of every frame) to actually
        # write the new light states to the hardware
        if MatrixLight.lights_to_update:
            for light in MatrixLight.lights_to_update:
                light.update_hw_light()

            MatrixLight.lights_to_update = set()

    @classmethod
    def mode_stop(cls, mode):
        for light in cls.machine.lights:
            light.remove_from_stack_by_mode(mode)

    def __init__(self, machine, name):
        super().__init__(machine, name)

        self.x = None
        self.y = None

        self.fade_in_progress = False
        self.default_fade_ms = None

        self.registered_handlers = list()

        self.stack = list()
        """A list of dicts which represents different commands that have come
        in to set this light to a certain brightness (and/or fade). Each entry
        in the list contains the following key/value pairs:

        priority: The relative priority of this brightness command. Higher
            numbers take precedent, and the highest priority entry will be the
            command that's currently active. In the event of a tie,
            whichever entry was added last wins (based on 'start_time' below).
        start_time: The clock time when this command was added. Primarily used
            to calculate fades, but also used as a tie-breaker for multiple
            entries with the same priority.
        start_brightness: Brightness this light when this command came in.
        dest_time: Clock time that represents when a fade (from
            start_brightness to dest_brightness ) will be done. If this is 0,
            that means there is no fade. When a fade is complete, this value is
             reset to 0.
        dest_brightness: Brightness of the destination this light is fading
            to. If a command comes in with no fade, then this will be the same
            as the 'brightness' below.
        brightness: The current brightness of the light based on this command.
            (0-255) This value is updated automatically as fades progress, and
            it's the value that's actually written to the hardware.
        key: An arbitrary unique identifier to keep multiple entries in the
            stack separate. If a new brightness command comes in with a key
            that already exists for an entry in the stack, that entry will be
            replaced by the new entry. The key is also used to remove entries
            from the stack (e.g. when shows or modes end and they want to
            remove their commands from the light).
        mode: Optional mode where the brightness was set. Used to remove
            entries when a mode ends.

        """

    def _initialize(self):
        self.load_platform_section('matrix_lights')

        self.hw_driver = self.platform.configure_matrixlight(self.config)

        if self.config['fade_ms'] is not None:
            self.default_fade_ms = self.config['fade_ms']
        elif self.machine.config['matrix_light_settings']:
            self.default_fade_ms = (
                self.machine.config['matrix_light_settings']['default_light_fade_ms'])
        else:
            self.default_fade_ms = 0

        if 'x' in self.config:
            self.x = self.config['x']

        if 'y' in self.config:
            self.y = self.config['y']

    def on(self, brightness=255, fade_ms=0, priority=0, key=None, mode=None,
           **kwargs):
        """Adds or updates a brightness entry in this lights's stack, which is
        how you tell this light how bright you want it to be.

        Args:
            brightness: How bright this light should be, as an int between 0
                and 255. 0 is off. 255 is full on. Note that matrix lights in
                older (even WPC) machines had slow matrix update speeds, and
                effective brightness levels will be far less than 255.
            fade_ms: Int of the number of ms you want this light to fade to the
                brightness in. A value of 0 means it's instant. A value of
                None (the default) means that it will use this lights's and/or
                the machine's default fade_ms setting.
            priority: Int value of the priority of these incoming settings. If
                this light has current settings in the stack at a higher
                priority, the settings you're adding here won't take effect.
                However they're still added to the stack, so if the higher
                priority settings are removed, then the next-highest apply.
            key: An arbitrary identifier (can be any immutable object) that's
                used to identify these settings for later removal. If any
                settings in the stack already have this key, those settings
                will be replaced with these new settings.
            mode: Optional mode instance of the mode that is setting this
                brightness. When a mode ends, entries from the stack with that
                mode will automatically be removed.
            **kwargs: Not used. Only included so this method can be used as
                an event callback since events could pass random kwargs.
        """

        del kwargs

        if self.debug:
            self.log.debug("Received on() command. brightness: %s, fade_ms: %s"
                           "priority: %s, key: %s", brightness, fade_ms,
                           priority, key)

        if fade_ms is None:
            fade_ms = self.default_fade_ms

        if priority < self._get_priority_from_key(key):
            if self.debug:
                self.log.debug("Incoming priority is lower than an existing "
                               "stack item with the same key. Not adding to "
                               "stack.")

            return

        self._add_to_stack(brightness, fade_ms, priority, key, mode)

    def _add_to_stack(self, brightness, fade_ms, priority, key, mode):
        curr_brightness = self.get_brightness()

        self.remove_from_stack_by_key(key)

        if fade_ms:
            new_brightness = curr_brightness
            dest_time = self.machine.clock.get_time() + (fade_ms / 1000)
        else:
            new_brightness = brightness
            dest_time = 0

        self.stack.append(dict(priority=priority,
                               start_time=self.machine.clock.get_time(),
                               start_brightness=curr_brightness,
                               dest_time=dest_time,
                               dest_brightness=brightness,
                               brightness=new_brightness,
                               key=key,
                               mode=mode))

        self.stack.sort(key=itemgetter('priority', 'start_time'), reverse=True)

        if self.debug:
            self.log.debug("+-------------- Adding to stack ----------------+")
            self.log.debug("priority: %s", priority)
            self.log.debug("start_time: %s", self.machine.clock.get_time())
            self.log.debug("start_brightness: %s", curr_brightness)
            self.log.debug("dest_time: %s", dest_time)
            self.log.debug("dest_brightness: %s", brightness)
            self.log.debug("brightness: %s", new_brightness)
            self.log.debug("key: %s", key)

        MatrixLight.lights_to_update.add(self)

    def clear_stack(self):
        """Removes all entries from the stack and resets this light to 'off'."""
        self.stack[:] = []

        if self.debug:
            self.log.debug("Clearing Stack")

        MatrixLight.lights_to_update.add(self)

    def remove_from_stack_by_key(self, key):
        """Removes a group of brightness settings from the stack.

        Args:
            key: The key of the settings to remove (based on the 'key'
                parameter that was originally passed to the brightness() method.)

        This method triggers a light update, so if the highest priority settings
        were removed, the light will be updated with whatever's below it. If no
        settings remain after these are removed, the light will turn off.

        """

        if self.debug:
            self.log.debug("Removing key '%s' from stack", key)

        self.stack[:] = [x for x in self.stack if x['key'] != key]
        MatrixLight.lights_to_update.add(self)

    def remove_from_stack_by_mode(self, mode):
        """Removes a group of brightness settings from the stack.

        Args:

        This method triggers a light update, so if the highest priority settings
        were removed, the light will be updated with whatever's below it. If no
        settings remain after these are removed, the light will turn off.

        """

        if self.debug:
            self.log.debug("Removing mode '%s' from stack", mode)

        self.stack[:] = [x for x in self.stack if x['mode'] != mode]
        MatrixLight.lights_to_update.add(self)

    def get_brightness(self):
        """Returns an RGBColor() instance of the 'color' setting of the highest
        color setting in the stack. This is usually the same color as the
        physical LED, but not always (since physical LEDs are updated once per
        frame, this value could vary.

        Also note the color returned is the "raw" color that does has not had
        the color correction profile applied.

        """
        try:
            return self.stack[0]['brightness']
        except IndexError:
            return 0

    def _get_priority_from_key(self, key):
        try:
            return [x for x in self.stack if x['key'] == key][0]['priority']
        except IndexError:
            return 0

    def update_hw_light(self):
        """Physically updates the light hardware object based on the
        'brightness' setting of the highest priority setting from the stack.

        This method is automatically called whenever a brightness change has been
        made (including when fades are active).

        """
        if not self.stack:
            self.off()

        # if there's a current fade, but the new command doesn't have one
        if not self.stack[0]['dest_time'] and self.fade_in_progress:
            self._stop_fade_task()

        # If the new command has a fade, but the fade task isn't running
        if self.stack[0]['dest_time'] and not self.fade_in_progress:
            self._setup_fade()

        # If there's no current fade and no new fade, or a current fade and new
        # fade
        else:
            self.hw_driver.on(self.stack[0]['brightness'])

            if self.registered_handlers:
                # Handlers are not sent brightness corrected brightnesss
                # todo make this a config option?
                for handler in self.registered_handlers:
                    handler(light_name=self.name,
                            brightness=self.stack[0]['brightness'])

    def off(self, fade_ms=0, priority=0, key=None, mode=None, **kwargs):
        """Turns this light off.

        Args:
            fade_ms: Int of the number of ms you want this light to fade to the
                brightness in. A value of 0 means it's instant. A value of
                None (the default) means that it will use this lights's and/or
                the machine's default fade_ms setting.
            priority: Int value of the priority of these incoming settings. If
                this light has current settings in the stack at a higher
                priority, the settings you're adding here won't take effect.
                However they're still added to the stack, so if the higher
                priority settings are removed, then the next-highest apply.
            key: An arbitrary identifier (can be any immutable object) that's
                used to identify these settings for later removal. If any
                settings in the stack already have this key, those settings
                will be replaced with these new settings.
            mode: Optional mode instance of the mode that is setting this
                brightness. When a mode ends, entries from the stack with that
                mode will automatically be removed.
            **kwargs: Not used. Only included so this method can be used as
                an event callback since events could pass random kwargs.
        """
        del kwargs
        self.on(brightness=0, fade_ms=fade_ms, priority=priority, key=key,
                mode=mode)

    def add_handler(self, callback):
        """Registers a handler to be called when this light changes state."""
        self.registered_handlers.append(callback)

    def remove_handler(self, callback=None):
        """Removes a handler from the list of registered handlers."""
        if not callback:  # remove all
            self.registered_handlers = []
            return

        if callback in self.registered_handlers:
            self.registered_handlers.remove(callback)

    def _setup_fade(self):

        if self.fade_in_progress:
            return

        self.fade_in_progress = True

        if self.debug:
            self.log.debug("Setting up the fade task")

        self.machine.clock.schedule_interval(self._fade_task, 0)

    def _fade_task(self, dt):
        del dt

        # not sure why this is needed, but sometimes the fade task tries to
        # run even though self.fade_in_progress is False. Maybe
        # clock.unschedule doesn't happen right away?
        if not self.fade_in_progress:
            return

        try:
            brightness_settings = self.stack[0]
        except IndexError:
            self._stop_fade_task()
            return

        # todo
        if not brightness_settings['dest_time']:
            return

        # figure out the ratio of how far along we are
        try:
            ratio = ((self.machine.clock.get_time() -
                      brightness_settings['start_time']) /
                     (brightness_settings['dest_time'] -
                      brightness_settings['start_time']))
        except ZeroDivisionError:
            ratio = 1.0

        if self.debug:
            self.log.debug("Fade task, ratio: %s", ratio)

        if ratio >= 1.0:  # fade is done
            self._end_fade()
            brightness_settings['brightness'] = (
                brightness_settings['dest_brightness'])
        else:
            brightness_settings['brightness'] = (
                brightness_settings['start_brightness'] +
                int((brightness_settings['dest_brightness'] -
                    brightness_settings['start_brightness']) * ratio))

        MatrixLight.lights_to_update.add(self)

    def _end_fade(self):
        # stops the fade and instantly sets the light to its destination brightness
        self._stop_fade_task()
        self.stack[0]['dest_time'] = 0

    def _stop_fade_task(self):
        # stops the fade task. Light is left in whatever state it was in
        self.fade_in_progress = False
        self.machine.clock.unschedule(self._fade_task)

        if self.debug:
            self.log.debug("Stopping fade task")
<|endoftext|>


<commit_before>import os
import re
import tempfile

import shutil

from mpf.tests.MpfFakeGameTestCase import MpfFakeGameTestCase
from mpf.tests.MpfMachineTestCase import MockConfigPlayers

class MpfDocTestCase(MockConfigPlayers, MpfFakeGameTestCase):

    def __init__(self, config_string, methodName='test_config_parsing'):
        super().__init__(methodName)
        machine_config, mode_configs, show_configs, self.tests = self.prepare_config(config_string)
        self.config_dir = tempfile.mkdtemp()

        # create machine config
        os.mkdir(os.path.join(self.config_dir, "config"))
        with open(os.path.join(self.config_dir, "config", "config.yaml"), "w") as f:
            f.write(machine_config)

        # create shows
        os.mkdir(os.path.join(self.config_dir, "shows"))
        for show_name, show_config in show_configs.items():
            with open(os.path.join(self.config_dir, "shows", show_name + ".yaml"), "w") as f:
                f.write(show_config)

        # create modes
        os.mkdir(os.path.join(self.config_dir, "modes"))
        for mode_name, mode_config in mode_configs.items():
            os.mkdir(os.path.join(self.config_dir, "modes", mode_name))
            os.mkdir(os.path.join(self.config_dir, "modes", mode_name, "config"))
            with open(os.path.join(self.config_dir, "modes", mode_name, "config", mode_name + ".yaml"), "w") as f:
                f.write(mode_config)

        # cleanup at the end
        self.addCleanup(self._delete_tmp_dir, self.config_dir)

    def get_enable_plugins(self):
        return True

    def getOptions(self):
        options = super().getOptions()
        # no cache since we are in a tmp folder anyway
        options['no_load_cache'] = True,
        options['create_config_cache'] = False
        return options

    def prepare_config(self, config_string):
        # inline invisible comments from documentation
        config_string = re.sub(r'^#! ([^\n]+)', '\\1', config_string, flags=re.MULTILINE)

        # first find sections
        configs = re.split(r'^##! (config: \w+|test|show: \w+|mode: \w+)\n', config_string, flags=re.MULTILINE)
        machine_config = configs.pop(0)

        # add config_version if missing
        if not machine_config.startswith("#config_version=5"):
            machine_config = "#config_version=5\n" + machine_config

        modes = {}
        shows = {}
        tests = []
        while configs:
            section_type = configs.pop(0)
            section_config = configs.pop(0)
            if section_type.startswith("test"):
                # unit test
                tests = section_config.splitlines()
            elif section_type.startswith("mode:") or section_type.startswith("config:"):
                # normal mode
                _, mode_name = section_type.split(": ", 2)
                if not section_config.startswith("#config_version=5"):
                    section_config = "#config_version=5\n" + section_config
                modes[mode_name] = section_config
            elif section_type.startswith("show:"):
                # show
                _, show_name = section_type.split(": ", 2)
                if not section_config.startswith("#show_version=5"):
                    section_config = "#show_version=5\n" + section_config
                shows[show_name] = section_config

        # load all modes
        if modes and "modes:" not in machine_config:
            machine_config += "\nmodes:\n"
            for mode in modes.keys():
                machine_config += " - " + mode + "\n"

        return machine_config, modes, shows, tests

    def getConfigFile(self):
        return "config.yaml"

    def get_platform(self):
        return "smart_virtual"

    def getMachinePath(self):
        return self.config_dir

    def _delete_tmp_dir(self, config_dir):
        shutil.rmtree(config_dir)

    def test_config_parsing(self):
        line_no = 0
        for line in self.tests:
            line_no += 1
            if not line or line.startswith("#"):
                continue

            parts = line.split(" ")
            command = parts.pop(0)
            method = getattr(self, "command_" + command)
            if not method:
                raise AssertionError("Unknown command {} in line {}".format(command, line_no))
            try:
                method(*parts)
            except AssertionError as e:
                raise AssertionError("Error in line {}".format(line_no), e)

    def command_start_game(self):
        self.start_game()

    def command_stop_game(self, stop_time=1):
        self.stop_game(float(stop_time))

    def command_drain_ball(self):
        self.drain_ball()

    def command_start_mode(self, mode):
        self.machine.modes[mode].start()
        self.machine_run()
        self.assertModeRunning(mode)

    def command_stop_mode(self, mode):
        self.machine.modes[mode].stop()
        self.machine_run()
        self.assertModeNotRunning(mode)

    def command_assert_mode_running(self, mode):
        self.assertModeRunning(mode)

    def command_assert_mode_not_running(self, mode):
        self.assertModeNotRunning(mode)

    def command_post(self, event_name):
        self.post_event(event_name)

    def command_hit_and_release_switch(self, switch_name):
        self.hit_and_release_switch(switch_name)

    def command_hit_switch(self, switch_name):
        self.hit_switch_and_run(switch_name, 0)

    def command_release_switch(self, switch_name):
        self.release_switch_and_run(switch_name, 0)

    def command_advance_time_and_run(self, delta):
        self.advance_time_and_run(float(delta))

    def command_assert_player_variable(self, value, player_var):
        if isinstance(self.machine.game.player[player_var], (int, float)):
            value = float(value)
        self.assertPlayerVarEqual(value, player_var=player_var)

    def command_assert_light_color(self, light, color):
        self.assertLightColor(light, color)

    def command_assert_light_flashing(self, light, color, secs=1, delta=.1):
        self.assertLightFlashing(light, color, float(secs), float(delta))

    def command_mock_event(self, name):
        self.mock_event(name)

    def command_assert_event_called(self, name, times=1):
        self.assertEventCalled(name, times)

    def command_assert_event_not_called(self, name):
        self.assertEventNotCalled(name)

    def command_assert_balls_on_playfield(self, balls):
        self.assertBallsOnPlayfield(int(balls))

    def command_assert_balls_in_play(self, balls):
        self.assertBallsInPlay(int(balls))
<commit_msg>better argument parsing in doc tests<commit_after>import os
import re
import tempfile

import shutil
import shlex

from mpf.tests.MpfFakeGameTestCase import MpfFakeGameTestCase
from mpf.tests.MpfMachineTestCase import MockConfigPlayers

class MpfDocTestCase(MockConfigPlayers, MpfFakeGameTestCase):

    def __init__(self, config_string, methodName='test_config_parsing'):
        super().__init__(methodName)
        machine_config, mode_configs, show_configs, self.tests = self.prepare_config(config_string)
        self.config_dir = tempfile.mkdtemp()

        # create machine config
        os.mkdir(os.path.join(self.config_dir, "config"))
        with open(os.path.join(self.config_dir, "config", "config.yaml"), "w") as f:
            f.write(machine_config)

        # create shows
        os.mkdir(os.path.join(self.config_dir, "shows"))
        for show_name, show_config in show_configs.items():
            with open(os.path.join(self.config_dir, "shows", show_name + ".yaml"), "w") as f:
                f.write(show_config)

        # create modes
        os.mkdir(os.path.join(self.config_dir, "modes"))
        for mode_name, mode_config in mode_configs.items():
            os.mkdir(os.path.join(self.config_dir, "modes", mode_name))
            os.mkdir(os.path.join(self.config_dir, "modes", mode_name, "config"))
            with open(os.path.join(self.config_dir, "modes", mode_name, "config", mode_name + ".yaml"), "w") as f:
                f.write(mode_config)

        # cleanup at the end
        self.addCleanup(self._delete_tmp_dir, self.config_dir)

    def get_enable_plugins(self):
        return True

    def getOptions(self):
        options = super().getOptions()
        # no cache since we are in a tmp folder anyway
        options['no_load_cache'] = True,
        options['create_config_cache'] = False
        return options

    def prepare_config(self, config_string):
        # inline invisible comments from documentation
        config_string = re.sub(r'^#! ([^\n]+)', '\\1', config_string, flags=re.MULTILINE)

        # first find sections
        configs = re.split(r'^##! (config: \w+|test|show: \w+|mode: \w+)\n', config_string, flags=re.MULTILINE)
        machine_config = configs.pop(0)

        # add config_version if missing
        if not machine_config.startswith("#config_version=5"):
            machine_config = "#config_version=5\n" + machine_config

        modes = {}
        shows = {}
        tests = []
        while configs:
            section_type = configs.pop(0)
            section_config = configs.pop(0)
            if section_type.startswith("test"):
                # unit test
                tests = section_config.splitlines()
            elif section_type.startswith("mode:") or section_type.startswith("config:"):
                # normal mode
                _, mode_name = section_type.split(": ", 2)
                if not section_config.startswith("#config_version=5"):
                    section_config = "#config_version=5\n" + section_config
                modes[mode_name] = section_config
            elif section_type.startswith("show:"):
                # show
                _, show_name = section_type.split(": ", 2)
                if not section_config.startswith("#show_version=5"):
                    section_config = "#show_version=5\n" + section_config
                shows[show_name] = section_config

        # load all modes
        if modes and "modes:" not in machine_config:
            machine_config += "\nmodes:\n"
            for mode in modes.keys():
                machine_config += " - " + mode + "\n"

        return machine_config, modes, shows, tests

    def getConfigFile(self):
        return "config.yaml"

    def get_platform(self):
        return "smart_virtual"

    def getMachinePath(self):
        return self.config_dir

    def _delete_tmp_dir(self, config_dir):
        shutil.rmtree(config_dir)

    def test_config_parsing(self):
        line_no = 0
        for line in self.tests:
            line_no += 1
            if not line or line.startswith("#"):
                continue

            parts = shlex.split(line)
            command = parts.pop(0)
            method = getattr(self, "command_" + command)
            if not method:
                raise AssertionError("Unknown command {} in line {}".format(command, line_no))
            try:
                method(*parts)
            except AssertionError as e:
                raise AssertionError("Error in line {}".format(line_no), e)

    def command_start_game(self):
        self.start_game()

    def command_stop_game(self, stop_time=1):
        self.stop_game(float(stop_time))

    def command_drain_ball(self):
        self.drain_ball()

    def command_start_mode(self, mode):
        self.machine.modes[mode].start()
        self.machine_run()
        self.assertModeRunning(mode)

    def command_stop_mode(self, mode):
        self.machine.modes[mode].stop()
        self.machine_run()
        self.assertModeNotRunning(mode)

    def command_assert_mode_running(self, mode):
        self.assertModeRunning(mode)

    def command_assert_mode_not_running(self, mode):
        self.assertModeNotRunning(mode)

    def command_post(self, event_name, *args):
        kwargs = {}
        for arg in args:
            key, value = arg.split("=", 2)
            kwargs[key] = value
        self.post_event_with_params(event_name, **kwargs)

    def command_hit_and_release_switch(self, switch_name):
        self.hit_and_release_switch(switch_name)

    def command_hit_switch(self, switch_name):
        self.hit_switch_and_run(switch_name, 0)

    def command_release_switch(self, switch_name):
        self.release_switch_and_run(switch_name, 0)

    def command_advance_time_and_run(self, delta):
        self.advance_time_and_run(float(delta))

    def command_assert_player_variable(self, value, player_var):
        if isinstance(self.machine.game.player[player_var], (int, float)):
            value = float(value)
        self.assertPlayerVarEqual(value, player_var=player_var)

    def command_assert_machine_variable(self, value, name):
        if name in self.machine.machine_vars and isinstance(self.machine.machine_vars[name]["value"], (int, float)):
            value = float(value)
        self.assertMachineVarEqual(value, name)

    def command_assert_light_color(self, light, color):
        self.assertLightColor(light, color)

    def command_assert_light_flashing(self, light, color, secs=1, delta=.1):
        self.assertLightFlashing(light, color, float(secs), float(delta))

    def command_mock_event(self, name):
        self.mock_event(name)

    def command_assert_event_called(self, name, times=1):
        self.assertEventCalled(name, times)

    def command_assert_event_not_called(self, name):
        self.assertEventNotCalled(name)

    def command_assert_balls_on_playfield(self, balls):
        self.assertBallsOnPlayfield(int(balls))

    def command_assert_balls_in_play(self, balls):
        self.assertBallsInPlay(int(balls))
<|endoftext|>


<commit_before>from django.conf.urls import url
from mrbelvedereci.build import views

urlpatterns = [
    url(r'^$', views.build_list),
    url(r'^/(?P<build_id>\w+)/rerun$', views.build_rerun),
    url(r'^/(?P<build_id>\w+)$', views.build_detail),
]
<commit_msg>fix view name<commit_after>from django.conf.urls import url
from mrbelvedereci.build import views

urlpatterns = [
    url(r'^$', views.build_list),
    url(r'^/(?P<build_id>\w+)/rebuild$', views.build_rebuild),
    url(r'^/(?P<build_id>\w+)$', views.build_detail),
]
<|endoftext|>