Skip to content

Commit

Permalink
all things passing
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-s-s committed Oct 26, 2020
1 parent 4aabfd4 commit 96da036
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 106 deletions.
14 changes: 10 additions & 4 deletions dicetables/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ def __init__(
ignore_case: bool = False,
checker: AbstractLimitChecker = NoOpLimitChecker(),
):
"""
:param ignore_case: False: Can the parser ignore case on die names and kwargs.
:param checker: `dicetables.tools.limit_checker.NoOpLimitChecker`: How limits will be enforced
for parsing. This defaults to a limit checker that does not check limits.
"""
self.checker = checker
self.ignore_case = ignore_case

Expand Down Expand Up @@ -70,6 +76,10 @@ def with_limits(
max_dice_pools: int = 2,
) -> "Parser":
"""
Creates a parser with a functioning limit checker from `dicetables.tools.limit_checker.LimitChecker`
For explanation of how or why to change `max_dice_pool_combinations_per_dict_size`, and
`max_dice_pool_calls`, see
`Parser <http://dice-tables.readthedocs.io/en/latest/implementation_details/parser.html#limits-and-dicepool-objects>`_
:param ignore_case: False: Can the parser ignore case on die names and kwargs.
:param max_size: 500: The maximum allowed die size when :code:`parse_die_within_limits`
Expand All @@ -78,10 +88,6 @@ def with_limits(
:param max_dice: 6: The maximum number of dice calls when :code:`parse`.
Ex: :code:`StrongDie(Exploding(Die(5), 2), 3)` has 3 dice calls.
:param max_dice_pools: 2: The maximum number of allowed dice_pool calls
For explanation of how or why to change `max_dice_pool_combinations_per_dict_size`, and
`max_dice_pool_calls`, see
`Parser <http://dice-tables.readthedocs.io/en/latest/implementation_details/parser.html#limits-and-dicepool-objects>`_
"""
checker = LimitChecker(
max_dice_pools=max_dice_pools,
Expand Down
48 changes: 36 additions & 12 deletions dicetables/tools/limit_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ def __init__(self, *args):
super(LimitsError, self).__init__(*args)


class LimitType(Enum):
class ArgumentType(Enum):
SIZE = auto()
EXPLOSIONS = auto()
EXPLODES_ON = auto()
INPUT_DIE = auto()
POOL_SIZE = auto()


def get_bound_args(arg_type: LimitType, bound_args: BoundArguments) -> Optional[Any]:
def get_bound_args(arg_type: ArgumentType, bound_args: BoundArguments) -> Optional[Any]:
arg_names = {
LimitType.SIZE: ("die_size", "dictionary_input"),
LimitType.EXPLOSIONS: ("explosions",),
LimitType.EXPLODES_ON: ("explodes_on",),
LimitType.INPUT_DIE: ("input_die",),
LimitType.POOL_SIZE: ("pool_size",),
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():
Expand All @@ -45,27 +45,51 @@ def assert_numbers_of_calls_within_limits(
self, die_classes: Iterable[Type[ProtoDie]]
) -> None:
"""
asserts that the number of `dicetables.ProtoDie` calls and the number of
`dicetables.bestworstmid.DicePool` calls are within limits.
:raises LimitsError:
"""
raise NotImplementedError

@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

@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

@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
Expand Down Expand Up @@ -127,7 +151,7 @@ def assert_numbers_of_calls_within_limits(
raise LimitsError(msg)

def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None:
sized_value = get_bound_args(LimitType.SIZE, bound_args)
sized_value = get_bound_args(ArgumentType.SIZE, bound_args)
if isinstance(sized_value, dict):
size = max(sized_value.keys())
else:
Expand All @@ -139,8 +163,8 @@ def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None:
)

def assert_explosions_within_limits(self, bound_args: BoundArguments) -> None:
explosions = get_bound_args(LimitType.EXPLOSIONS, bound_args)
explodes_on = get_bound_args(LimitType.EXPLODES_ON, bound_args)
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)

Expand All @@ -150,8 +174,8 @@ def assert_explosions_within_limits(self, bound_args: BoundArguments) -> None:
)

def assert_dice_pool_within_limits(self, bound_args: BoundArguments) -> None:
input_die = get_bound_args(LimitType.INPUT_DIE, bound_args)
pool_size = get_bound_args(LimitType.POOL_SIZE, bound_args)
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

Expand Down
139 changes: 49 additions & 90 deletions docs/implementation_details/parser.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,20 @@ True
Limiting Max Values
-------------------

You can make the parser enforce limits with :meth:`Parser.parse_die_within_limits`. This uses the limits
declared in :meth:`Parser.__init__` (and two for DicePool not in the `init`. see: `Limits and DicePool objects`_).
It limits the size, explosions and number of nested dice in a die.
A parser is instantiated with a :class:`dicetables.tools.limit_checker.AbstractLimitChecker`.

.. autoclass:: dicetables.tools.limit_checker.AbstractLimitChecker
:members:
:undoc-members:

The default limit checker is a `NoOpLimitChecker` which does not check limits. If you use the
factory function, :meth:`Parser.with_limits`, you will create a parser that uses a
:class:`dicetables.tools.limit_checker.LimitChecker`.

The size is limited according to the `die_size` parameter or the max value of the `dictionary_input` parameter.
The explosions is limited according to `explosions` parameter and the `len` of the `explodes_on` parameter. The number
of nested dice is limited according to how many times the parser has to make a die while creating the die.
:code:`StrongDie(Exploding(Die(4)), 3)` is a `StrongDie` containing two nested dice.
:code:`StrongDie(Exploding(Die(4)), 3)` has three calls.

.. _`kwargs issue`:

Expand All @@ -189,104 +195,61 @@ will simply parse the die string.

ex:

>>> dt.Parser().parse_die_within_limits('Die(500)')
>>> parser = dt.Parser.with_limits()
>>> parser.parse_die('Die(500)')
Die(500)
>>> dt.Parser().parse_die_within_limits('Die(501)')
>>> parser.parse_die('Die(501)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
>>> stupid = 'StrongDie(' * 20 + 'Die(5)' + ', 2)' * 20
>>> dt.Parser().parse_die_within_limits(stupid)
>>> parser.parse_die(stupid)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max number of nested dice: 5
LimitsError: Limits exceeded. Max dice calls: 6.

>>> class NewDie(dt.Die):
... def __init__(self, funky_new_die_size=6):
... def __init__(self, funky_new_die_size: int = 6):
... super(NewDie, self).__init__(funky_new_die_size)
...
... def __repr__(self):
... return 'NewDie({})'.format(self.get_size())

>>> parser = dt.Parser()
>>> parser.add_class(NewDie, ('int',))
>>> parser = dt.Parser.with_limits()
>>> parser.add_class(NewDie)

>>> parser.parse_die_within_limits('NewDie(5000)')
>>> parser.parse_die('NewDie(5000)')
NewDie(5000)
>>> parser.parse_die_within_limits('Die(5000)')
>>> parser.parse_die('Die(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
LimitsError: Limits exceeded. Max dice calls: 6.

You can add your new and exciting key-words to the parser with :meth:`Parser.add_limits_kwarg`.
If this has a default value, you can add that too.
In order to make sure your new and exciting key-word gets checked, you'll need to subclass the Limit Checker

>>> new_parser = dt.Parser()
>>> new_parser.add_class(NewDie, ('int',))
>>> new_parser.add_limits_kwarg('size', 'funky_new_die_size', default=6)

>>> new_parser.parse_die_within_limits('NewDie(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
>>> new_parser.parse_die_within_limits('Die(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
>>> from dicetables.tools.limit_checker import LimitChecker, LimitsError
>>> from inspect import BoundArguments
>>> def get_new_size_args(bound_args):
... return bound_args.arguments.get("funky_new_die_size")

>>> new_parser.parse_die_within_limits('NewDie()')
NewDie(6)
>>> new_parser.max_size = 5
>>> new_parser.parse_die_within_limits('NewDie()')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 5
>>> class MyChecker(LimitChecker):
... def assert_die_size_within_limits(self, bound_args: BoundArguments) -> None:
... super(MyChecker, self).assert_die_size_within_limits(bound_args)
... new_size_arg = get_new_size_args(bound_args)
... if new_size_arg and new_size_arg > self.max_size:
... raise LimitsError("your funky new die size is too funky fresh for this parser")

The parser only knows how to evaluate size based on a parameter that represents size as an `int` or dictionary of
`{int: int}` where the size is the highest key value. Similarly, the parser assumes that it can count the explosions
by evaluating an `int` or the length of a `list` or `tuple`. If these are not the case, you will need to delve into the
code and over-ride :meth:`Parser._check_die_size`, :meth:`Parser._check_explosions` or :meth:`Parser._check_limits`
>>> new_parser = dt.Parser(checker=MyChecker())
>>> new_parser.add_class(NewDie)

>>> class NewDie(dt.Die):
... def __init__(self, size_int_as_str):
... super(NewDie, self).__init__(int(size_int_as_str))

>>> def make_string(str_node):
... return str_node.s

>>> parser = dt.Parser()
>>> parser.add_param_type('string', make_string)
>>> parser.add_class(NewDie, ('string',))
>>> parser.add_limits_kwarg('size', 'size_int_as_str')

>>> parser.parse_die('NewDie("5")') == NewDie("5")
True
>>> parser.parse_die_within_limits('NewDie("5")')
>>> new_parser.parse_die('NewDie(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: A kwarg declared as a "die size limit" is neither an int nor a dict of ints.

and **a** solution

>>> class NewParser(dt.Parser):
... def _check_die_size(self, die_size_param):
... if isinstance(die_size_param, str):
... die_size_param = int(die_size_param)
... super(NewParser, self)._check_die_size(die_size_param)
...
>>> parser = NewParser()
>>> parser.add_param_type('string', make_string)
>>> parser.add_class(NewDie, ('string',))
>>> parser.add_limits_kwarg('size', 'size_int_as_str')

>>> parser.parse_die_within_limits('NewDie("5000")')
LimitsError: your funky new die size ...
>>> new_parser.parse_die('Die(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
>>> parser.parse_die_within_limits('Die(5000)')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max die_size: 500
LimitsError: Limits exceeded. Max dice calls: 6.

Limits and DicePool Objects
---------------------------
Expand All @@ -299,30 +262,26 @@ Suffice it to say that the limits on any DicePool can be determined by :code:`le
was determined using the extremely scientific approach of "trying different things and seeing how long they took". This
is likely going to be different with whatever computer you will be using. That's why this a public variable.

The other variable is :code:`Parser().max_dice_pool_calls`, currently set to "2". This is separate from
`max_nested_dice`. A dice pool call is still counted against nested dice.
The other variable is :code:`Parser.with_limits().checker.max_dice_pools`, currently set to "2".
This is separate from `max_nested_dice`. A dice pool call is still counted against nested dice.

>>> parser = dt.Parser(max_nested_dice=3)
>>> two_pools_three_nested_dice = 'BestOfDicePool(StrongDie(WorstOfDicePool(Die(2), 3, 2), 2), 3, 2)'
>>> parser.parse_die_within_limits(two_pools_three_nested_dice)
>>> parser = dt.Parser.with_limits(max_dice=4)
>>> two_pools_three_dice = 'BestOfDicePool(StrongDie(WorstOfDicePool(Die(2), 3, 2), 2), 3, 2)'
>>> parser.parse_die(two_pools_three_dice)
BestOfDicePool(StrongDie(WorstOfDicePool(Die(2), 3, 2), 2), 3, 2)
>>> three_pools = 'three_calls = BestOfDicePool(BestOfDicePool(WorstOfDicePool(Die(2), 3, 2), 3, 2), 3, 2)'
>>> parser.parse_die_within_limits(three_pools)
>>> three_pools = 'BestOfDicePool(BestOfDicePool(WorstOfDicePool(Die(2), 3, 2), 3, 2), 3, 2)'
>>> parser.parse_die(three_pools)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max number of DicePool objects: 2
>>> four_nested_dice = 'BestOfDicePool(StrongDie(StrongDie(StrongDie(Die(2), 2), 2), 2), 2, 2)'
>>> parser.parse_die_within_limits(four_nested_dice)
LimitsError: "Limits exceeded. Max dice calls: 4. Max dice pool calls: 2 ...
>>> five_dice = 'BestOfDicePool(StrongDie(StrongDie(StrongDie(Die(2), 2), 2), 2), 2, 2)'
>>> parser.parse_die(five_dice)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
LimitsError: Max number of nested dice: 3
LimitsError: "Limits exceeded. Max dice calls: 4. Max dice pool calls: 2 ...

With the current limits in place,
an implementation of a dice pool could take up to 0.5s. If five calls were allowed, that would be 2.5s to parse a
single die. It is hard to imagine any practical reason to use more than one pool.
:code:`BestOfDicePool(WorstOfDicePool(Die(6), 4, 3), 2, 1)` would mean: "Roll 4D6 and take the worst three. Do that
twice and take the best one". If the current limit of two feels too limiting, change it.

Just as in `kwargs issue`_, the parser looks for the key-word arguments: "input_die" and "pool_size"
to figure things out. If you make a new DicePool that doesn't use these variable names, you'll need to tell the
parser. use the methods: :meth:`Parser.add_limits_kwarg` with the appropriate `existing_key`.

0 comments on commit 96da036

Please sign in to comment.