Source code for dicetables.tools.limit_checker

from abc import ABC, abstractmethod
from enum import Enum
from inspect import BoundArguments
from typing import Iterable, Type, Optional, Any, Union

from dicetables.dicepool import DicePool
from dicetables.eventsbases.protodie import ProtoDie
from dicetables.tools.orderedcombinations import (
    count_unique_combination_keys,
    largest_permitted_pool_size,
)


DieOrPool = Union[Type[ProtoDie], Type[DicePool]]


class LimitsError(ValueError):
    def __init__(self, *args):
        super(LimitsError, self).__init__(*args)


class ArgumentType(Enum):
    SIZE = 1
    EXPLOSIONS = 2
    EXPLODES_ON = 3
    INPUT_DIE = 4
    POOL_SIZE = 5


def get_bound_args(arg_type: ArgumentType, bound_args: BoundArguments) -> Optional[Any]:
    arg_names = {
        ArgumentType.SIZE: ("die_size", "dictionary_input"),
        ArgumentType.EXPLOSIONS: ("explosions",),
        ArgumentType.EXPLODES_ON: ("explodes_on",),
        ArgumentType.INPUT_DIE: ("input_die",),
        ArgumentType.POOL_SIZE: ("pool_size",),
    }
    possible_args = arg_names[arg_type]
    for arg_name, arg_value in bound_args.arguments.items():
        if arg_name in possible_args:
            return arg_value
    return None


[docs]class AbstractLimitChecker(ABC):
[docs] @abstractmethod def assert_numbers_of_calls_within_limits( self, die_classes: Iterable[DieOrPool] ) -> None: """ asserts that the number of `dicetables.ProtoDie` calls and the number of `dicetables.dicepool.DicePool` calls are within limits. :raises LimitsError: """ raise NotImplementedError
[docs] @abstractmethod def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None: """ Checks the bound arguments for the size of the die and asserts they are within limits. This typically uses `dicetables.tools.limit_checker.get_bound_args` to determine what is a `dicetables.tools.limit_checker.ArgumentType.SIZE` argument. :raises LimitsError: """ raise NotImplementedError
[docs] @abstractmethod def assert_explosions_within_limits(self, bound_args: BoundArguments) -> None: """ Asserts that the number of explosions on an `dicetables.Exploding` or an `dicetables.ExplodingOn` has a number of explosions withing limits. This typically uses `dicetables.tools.limit_checker.get_bound_args` to determine what is a `dicetables.tools.limit_checker.ArgumentType.EXPLOSIONS` and a `dicetables.tools.limit_checker.ArgumentType.EXPLODES_ON` argument. :raises LimitsError: """ raise NotImplementedError
[docs] @abstractmethod def assert_dice_pool_within_limits(self, bound_args: BoundArguments) -> None: """ asserts that the size of a dice pool is within limits. Dice pools can be very expensive to calculate. see: `DicePools <http://dice-tables.readthedocs.io/en/latest/the_dice.html#dice-pools>`_ This typically uses `dicetables.tools.limit_checker.get_bound_args` to determine what is a `dicetables.tools.limit_checker.ArgumentType.POOL_SIZE` argument. :raises LimitsError: """ raise NotImplementedError
class NoOpLimitChecker(AbstractLimitChecker): def assert_numbers_of_calls_within_limits( self, die_classes: Iterable[DieOrPool] ) -> None: pass def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None: pass def assert_explosions_within_limits(self, bound_args: BoundArguments) -> None: pass def assert_dice_pool_within_limits(self, bound_args: BoundArguments) -> None: pass class LimitChecker(AbstractLimitChecker): def __init__( self, max_size: int = 500, max_explosions: int = 10, max_dice: int = 5, max_dice_pools: int = 2, ): self.max_size = max_size self.max_explosions = max_explosions self.max_dice_calls = max_dice self.max_dice_pool_calls = max_dice_pools self.max_dice_pool_combinations_per_dict_size = { 2: 600, 3: 8700, 4: 30000, 5: 55000, 6: 70000, 7: 100000, 12: 200000, 30: 250000, } def assert_numbers_of_calls_within_limits( self, call_classes: Iterable[DieOrPool] ) -> None: class_list = list(call_classes) die_classes = len([el for el in class_list if issubclass(el, ProtoDie)]) dice_pools = class_list.count(DicePool) if ( die_classes > self.max_dice_calls or dice_pools > self.max_dice_pool_calls ): msg = ( "Limits exceeded. Max dice calls: {}. ".format(self.max_dice_calls) + "Max dice pool calls: {}. ".format(self.max_dice_pool_calls) + "Calls requested: {}".format(class_list) ) raise LimitsError(msg) def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None: sized_value = get_bound_args(ArgumentType.SIZE, bound_args) if isinstance(sized_value, dict): size = max(sized_value.keys()) else: size = sized_value if size and size > self.max_size: raise LimitsError( "A die of size: {} is greater than the allowed ".format(size) + "max: {}".format(self.max_size) ) def assert_explosions_within_limits(self, bound_args: BoundArguments) -> None: explosions = get_bound_args(ArgumentType.EXPLOSIONS, bound_args) explodes_on = get_bound_args(ArgumentType.EXPLODES_ON, bound_args) if explodes_on: explosions += len(explodes_on) if explosions and explosions > self.max_explosions: raise LimitsError( ( "Explosions: {} + ".format(explosions) + "len({}) is greater than allowed ".format(explodes_on) + "max: {}".format(self.max_explosions) ) ) def assert_dice_pool_within_limits(self, bound_args: BoundArguments) -> None: input_die = get_bound_args(ArgumentType.INPUT_DIE, bound_args) pool_size = get_bound_args(ArgumentType.POOL_SIZE, bound_args) if input_die is None or pool_size is None: return dict_size = len(input_die.get_dict()) pool_limit = 0 for key, limit in sorted(self.max_dice_pool_combinations_per_dict_size.items()): if key > dict_size: break pool_limit = limit score = count_unique_combination_keys(input_die, pool_size) if score > pool_limit: max_pool_size = largest_permitted_pool_size(input_die, pool_limit) msg = "{!r} has a get_dict() of size: {}\n".format(input_die, dict_size) explanation = "For this die, the largest permitted pool_size is {}".format( max_pool_size ) raise LimitsError(msg + explanation)