Source code for dicetables.parser

import ast
from typing import Dict, Iterable, Type, Union
from inspect import signature, _empty, Signature

from dicetables.dicepool_collection import (
    BestOfDicePool,
    WorstOfDicePool,
    UpperMidOfDicePool,
    LowerMidOfDicePool,
)
from dicetables.dicepool import DicePool
from dicetables.dieevents import (
    Die,
    ModDie,
    Modifier,
    ModWeightedDie,
    WeightedDie,
    StrongDie,
    Exploding,
    ExplodingOn,
)
from dicetables.eventsbases.protodie import ProtoDie
from dicetables.tools.limit_checker import (
    AbstractLimitChecker,
    NoOpLimitChecker,
    LimitChecker,
)


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


class ParseError(ValueError):
    pass


[docs]class Parser(object):
[docs] def __init__( self, 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 self._classes = { Die, ModDie, Modifier, ModWeightedDie, WeightedDie, StrongDie, Exploding, ExplodingOn, BestOfDicePool, WorstOfDicePool, UpperMidOfDicePool, LowerMidOfDicePool, DicePool, } self._param_types = { int: make_int, Dict[int, int]: make_int_dict, ProtoDie: self.make_die, Iterable[int]: make_int_tuple, DicePool: self.make_pool, }
[docs] @classmethod def with_limits( cls, ignore_case: bool = False, max_size: int = 500, max_explosions: int = 10, max_dice: int = 6, 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` :param max_explosions: 10: The maximum allowed (explosions + len(explodes_on)) when :code:`parse_die_within_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 """ checker = LimitChecker( max_dice_pools=max_dice_pools, max_dice=max_dice, max_explosions=max_explosions, max_size=max_size, ) return cls(ignore_case=ignore_case, checker=checker)
@property def param_types(self): return self._param_types.copy() @property def classes(self): return self._classes.copy()
[docs] def parse_die(self, die_string): die_string = die_string.strip() ast_call_node = ast.parse(die_string).body[0].value self.checker.assert_numbers_of_calls_within_limits( self.walk_dice_calls(ast_call_node) ) return self.make_die(ast_call_node)
[docs] def make_die(self, call_node: ast.Call): die_class = self._get_call_class(call_node) bound_args = self._get_bound_args(call_node, die_class) self.checker.assert_explosions_within_limits(bound_args) self.checker.assert_die_size_within_limits(bound_args) return die_class(*bound_args.args, **bound_args.kwargs)
[docs] def make_pool(self, call_node: ast.Call): pool_class = self._get_call_class(call_node) bound_args = self._get_bound_args(call_node, pool_class) self.checker.assert_dice_pool_within_limits(bound_args) return pool_class(*bound_args.args, **bound_args.kwargs)
[docs] def walk_dice_calls(self, call_node: ast.AST) -> Iterable[DieOrPool]: return ( self._get_call_class(node) for node in ast.walk(call_node) if isinstance(node, ast.Call) )
def _get_call_class(self, call_node: ast.AST): class_name = call_node.func.id class_name = self._update_search_string(class_name) for die_class in self._classes: test_against = self._update_search_string(die_class.__name__) if class_name == test_against: return die_class raise ParseError("Die class: <{}> not recognized by parser.".format(class_name)) def _update_search_string(self, search_str): if self.ignore_case: return search_str.lower() return search_str def _get_bound_args(self, call_node: ast.Call, call_class: DieOrPool): call_signature = signature(call_class) args = self._get_params(call_node, call_signature) kwargs = self._get_kwargs(call_node, call_signature) try: bound_args = call_signature.bind(*args, **kwargs) bound_args.apply_defaults() except TypeError as e: msg = "{} for class: {}".format(e.args[0], call_node.func.id) raise ParseError(msg) return bound_args def _get_params(self, call_node: ast.Call, call_signature: Signature): signature_params = list(call_signature.parameters.values()) param_nodes = call_node.args if len(param_nodes) > len(signature_params): raise ParseError("Too many parameters for class: {}".format(call_node.func.id)) params = [] for node, param in zip(param_nodes, signature_params): type_hint = param.annotation converter = self._param_types[type_hint] params.append(converter(node)) return params def _raise_error_for_missing_type(self, call_signature: Signature): if any( param.annotation not in self._param_types for param in call_signature.parameters.values() ): raise ParseError( "The signature: {} has one or more un-recognized param types".format( call_signature ) ) def _get_kwargs(self, call_node: ast.Call, call_signature: Signature): kwarg_nodes = call_node.keywords out = {} for kwarg_node in kwarg_nodes: kwarg_name, value = self._get_kwarg_value(call_signature, kwarg_node) out[kwarg_name] = value return out def _get_kwarg_value(self, die_signature, kwarg_node): kwarg_name_to_search_for = self._update_search_string(kwarg_node.arg) value_node = kwarg_node.value class_params = die_signature.parameters param_mapping = { self._update_search_string(key): value for key, value in class_params.items() } try: param = param_mapping[kwarg_name_to_search_for] except KeyError: raise ParseError( "The keyword: {} is not in the die signature: {}".format( kwarg_node.arg, die_signature ) ) converter = self._param_types[param.annotation] return param.name, converter(value_node)
[docs] def add_class(self, class_: object): """ :param class_: the class you are adding """ die_signature = signature(class_) self._raise_error_for_missing_annotation(die_signature) self._raise_error_for_missing_type(die_signature) self._classes.add(class_)
@staticmethod def _raise_error_for_missing_annotation(die_signature: Signature): if any( param.annotation == _empty for param in die_signature.parameters.values() ): raise ParseError( "The signature: {} is missing type annotations".format(die_signature) )
[docs] def add_param_type(self, param_type, creation_method): self._param_types[param_type] = creation_method
def make_int_dict(dict_node): keys = _make_int_list(dict_node.keys) values = _make_int_list(dict_node.values) return dict(zip(keys, values)) def make_int_tuple(tuple_node): return tuple(_make_int_list(tuple_node.elts)) def _make_int_list(num_node_list): return [make_int(num_node) for num_node in num_node_list] def make_int(num_node): if isinstance(num_node, ast.UnaryOp) and isinstance(num_node.op, ast.USub): value = num_node.operand.n * -1 else: value = None for key, val in ast.iter_fields(num_node): if key != "kind": # pragma no branch value = val if not isinstance(value, int): raise ValueError("Expected an integer, but got: {!r}".format(value)) return value