Skip to content

Commit dfb1ca9

Browse files
committed
Added total (t) and rearranged stuff
* Removed parse and evaluate functions * Moved operators back to dice/elements.py * Added total operator (needs testing) * Removed alternative single and parse used in tests * Raise ImportWarnings for all PyParsing modifications
1 parent 4d4c52a commit dfb1ca9

File tree

9 files changed

+121
-143
lines changed

9 files changed

+121
-143
lines changed

dice/__init__.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,19 @@
2424
'expression',
2525
help="the expression to parse and roll")
2626

27-
def parse(string, grammar):
28-
"""Returns an AST parsed from an expression"""
29-
return grammar.parseString(string, parseAll=True)
3027

31-
def evaluate(string, grammar, **options):
32-
"""Parse and then evaluate a string with a grammar"""
33-
return [e.evaluate(**options) for e in parse(string, grammar)]
34-
35-
def roll(string, verbose=False):
28+
def roll(string, single=True, verbose=False):
3629
"""Parses and evaluates an expression"""
37-
return evaluate(string, grammar=dice.grammar.expression, verbose=verbose)
30+
ast = dice.grammar.expression.parseString(string, parseAll=True)
31+
result = [element.evaluate(verbose=verbose) for element in ast]
32+
if single:
33+
return dice.utilities.single(result)
34+
return result
35+
3836

3937
def main(argv=None):
4038
args = parser.parse_args(argv)
4139
result = roll(args.expression, verbose=args.verbose)
42-
result = dice.utilities.single(result)
4340
if args.verbose:
4441
print("Result:", end=" ")
4542
return result

dice/elements.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from __future__ import absolute_import, print_function, unicode_literals
44

55
import random
6+
import operator
67

78
from dice.utilities import classname
89

9-
1010
class Element(object):
1111
def evaluate(self, verbose=False):
1212
"""Evaluate the current object - a no-op by default"""
@@ -100,3 +100,55 @@ def evaluate(self, verbose=False):
100100

101101
def roll(self):
102102
return self.evaluate(verbose=False)
103+
104+
105+
class Operator(Element):
106+
@classmethod
107+
def parse(cls, string, location, tokens):
108+
return cls(operands=tokens)
109+
110+
def __init__(self, operands):
111+
self.operands = operands
112+
113+
def __repr__(self):
114+
return "{0}({1})".format(
115+
classname(self),
116+
', '.join(map(repr, self.operands)))
117+
118+
def evaluate(self):
119+
raise NotImplementedError(
120+
"Operator subclass has no evaluate()")
121+
122+
def evaluate_operands(self):
123+
self.operands = map(self.evaluate_object, self.operands)
124+
return self.operands
125+
126+
127+
class FunctionOperator(Operator):
128+
@property
129+
def function(self):
130+
raise NotImplementedError(
131+
"FunctionOperator subclass has no function")
132+
133+
def evaluate(self, verbose=False):
134+
return self.function(*self.evaluate_operands())
135+
136+
137+
class Div(FunctionOperator):
138+
function = operator.floordiv
139+
140+
141+
class Mul(FunctionOperator):
142+
function = operator.mul
143+
144+
145+
class Sub(FunctionOperator):
146+
function = operator.sub
147+
148+
149+
class Add(FunctionOperator):
150+
function = operator.add
151+
152+
153+
class Total(FunctionOperator):
154+
function = sum

dice/grammar.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88

99
from __future__ import absolute_import, print_function, unicode_literals
1010

11-
from pyparsing import (Forward, Literal, OneOrMore, StringStart, StringEnd,
12-
Suppress, Word, nums, opAssoc)
11+
from pyparsing import (CaselessLiteral, Forward, Literal, OneOrMore,
12+
StringStart, StringEnd, Suppress, Word, nums, opAssoc)
1313

14-
from dice.elements import Integer, Dice
15-
from dice.operators import Mul, Div, Sub, Add
14+
from dice.elements import Integer, Dice, Total, Mul, Div, Sub, Add
1615
from dice.utilities import patch_pyparsing
1716

1817
# Set PyParsing options
@@ -89,5 +88,7 @@ def parse_operator(expr, arity, association, action=None):
8988
(Literal('*').suppress(), 2, opAssoc.LEFT, Mul.parse),
9089
(Literal('-').suppress(), 2, opAssoc.LEFT, Sub.parse),
9190
(Literal('+').suppress(), 2, opAssoc.LEFT, Add.parse),
91+
92+
(CaselessLiteral('t').suppress(), 1, opAssoc.LEFT, Total.parse),
9293
]) + StringEnd()
9394
expression.setName("expression")

dice/operators.py

Lines changed: 0 additions & 55 deletions
This file was deleted.

dice/tests/__init__.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1 @@
11
"""Tests for the dice package"""
2-
3-
from __future__ import absolute_import, print_function
4-
5-
from dice import evaluate
6-
7-
def single(iterable):
8-
return iterable[0] if len(iterable) == 1 else list(iterable)
9-
10-
def parse(grammar, string):
11-
"""Parses a string with a grammar, returning and printing the result"""
12-
return single(evaluate(string, grammar))

dice/tests/test_elements.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
from __future__ import absolute_import
22

3-
import py.test
4-
53
from dice.elements import Integer, Roll, Dice
64

7-
@py.test.fixture
8-
def dice_constructors():
9-
return Dice(2, 6), "2d6", (2, 6)
10-
115
def test_integer():
126
assert isinstance(Integer(1), int)
137

14-
def test_roll():
15-
assert isinstance(Dice(2, 6).roll(), Roll)
16-
178
def test_dice_from_iterable():
189
d = Dice.from_iterable((2, 6))
1910
assert d.amount == 2 and d.sides == 6
@@ -22,21 +13,7 @@ def test_dice_from_string():
2213
d = Dice.from_string("2d6")
2314
assert d.amount == 2 and d.sides == 6
2415

25-
#@py.test.fixture
26-
#def bag():
27-
# return Bag("1d2", "2d4", "4d6", "6d8", "8d10")
28-
#
29-
#def test_dice_from_object(dice_constructors):
30-
# for obj in dice_constructors:
31-
# d = Bag.dice_from_object(obj)
32-
# assert d.amount == 2 and d.sides == 6
33-
#
34-
#def test_dice_from_object_exception():
35-
# with py.test.raises(TypeError):
36-
# Bag.dice_from_object(NotImplemented)
37-
#
38-
#def test_bag_length(dice_constructors):
39-
# assert len(Bag(*dice_constructors)) == len(dice_constructors)
40-
#
41-
#def test_bag_str(bag):
42-
# assert re.match("[d,\d]*", str(bag))
16+
def test_roll():
17+
amount, sides = 6, 6
18+
assert len(Roll.roll(amount, sides)) == amount
19+
assert (1 * sides) <= sum(Roll.roll(amount, sides)) <= (amount * sides)

dice/tests/test_grammar.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,65 @@
11
from __future__ import absolute_import, unicode_literals, division
22

33
from dice.elements import Integer, Roll
4-
from dice.grammar import integer, expression
5-
from dice.tests import parse
4+
from dice import roll
65

76
class TestInteger(object):
8-
def test_parse(self):
9-
assert parse(integer, "1337") == 1337
10-
assert parse(expression, "1337") == 1337
7+
def test_value(self):
8+
assert roll("1337") == 1337
119

1210
def test_type(self):
13-
assert isinstance(parse(integer, "1"), int)
14-
assert isinstance(parse(integer, "1"), Integer)
11+
assert isinstance(roll("1"), int)
12+
assert isinstance(roll("1"), Integer)
1513

1614

1715
class TestDice(object):
18-
def test_dice_type(self):
19-
for dice in ("d6", "1d6"):
20-
assert isinstance(parse(expression, dice), Roll)
21-
2216
def test_dice_value(self):
23-
for dice in ("d6", "1d6"):
24-
assert 0 < int(parse(expression, dice)) <= 6
25-
26-
def test_dice_result(self):
27-
for result in parse(expression, "6d6"):
28-
assert 0 < result <= 6
29-
17+
assert 0 < int(roll('d6')) <= 6
18+
assert 0 < int(roll('1d6')) <= 6
3019

31-
class TestExpression(object):
32-
def test_expression(self):
33-
assert parse(expression, "2d6")
20+
def test_dice_type(self):
21+
assert isinstance(roll('d6'), Roll)
22+
assert isinstance(roll('1d6'), Roll)
3423

35-
def test_sub_expression(self):
36-
assert parse(expression, "(2d6)d(2d6)")
24+
def test_dice_values(self):
25+
for die in roll("6d6"):
26+
assert 0 < die <= 6
3727

3828

39-
class TestExpressionMaths(object):
29+
class TestFunctionOperators(object):
4030
def test_add(self):
41-
assert parse(expression, "2 + 2") == 4
31+
assert roll("2 + 2") == 4
4232

4333
def test_sub(self):
44-
assert parse(expression, "2 - 2") == 0
34+
assert roll("2 - 2") == 0
4535

4636
def test_mul(self):
47-
assert parse(expression, "2 * 2") == 4
37+
assert roll("2 * 2") == 4
4838

4939
def test_div(self):
50-
assert parse(expression, "2 / 2") == 1
40+
assert roll("2 / 2") == 1
5141

42+
def test_total(self):
43+
assert (6 * 1) <= roll("6d6t") <= (6 * 6)
44+
45+
46+
def TestOperatorPrecedence(object):
5247
def test_operator_precedence_1(self):
53-
assert parse(expression, "16 / 8 * 4 + 2 - 1") == 9
48+
assert roll("16 / 8 * 4 + 2 - 1") == 9
5449

5550
def test_operator_precedence_2(self):
56-
assert parse(expression, "16 - 8 + 4 * 2 / 1") == 16
51+
assert roll("16 - 8 + 4 * 2 / 1") == 16
5752

5853
def test_operator_precedence_3(self):
59-
assert parse(expression, "10 - 3 + 2") == 9
54+
assert roll("10 - 3 + 2") == 9
6055

6156
def test_operator_precedence_4(self):
62-
assert parse(expression, "1 + 2 * 3") == 7
57+
assert roll("1 + 2 * 3") == 7
58+
59+
60+
class TestExpression(object):
61+
def test_expression(self):
62+
assert isinstance(roll("2d6"), Roll)
63+
64+
def test_sub_expression(self):
65+
assert isinstance(roll("(2d6)d(2d6)"), Roll)

dice/tests/test_main.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
from __future__ import absolute_import
22

3-
from dice import main
3+
from dice import roll, main
4+
from dice.elements import Integer, Roll
5+
6+
7+
def test_roll():
8+
assert isinstance(roll('2d6'), (Integer, Roll, list))
9+
410

511
def test_main():
6-
assert main(["2d6"]) is not None
12+
assert isinstance(main(['2d6']), (Integer, Roll, list))

dice/utilities.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ def classname(obj):
88
"""Returns the name of an objects class"""
99
return obj.__class__.__name__
1010

11+
1112
def single(iterable):
1213
"""Returns a single item if the iterable has only one item"""
13-
return iterable[0] if len(iterable) == 1 else list(iterable)
14+
return iterable[0] if len(iterable) == 1 else iterable
15+
1416

1517
def patch_pyparsing(packrat=True, arity=True, verbose=True):
1618
"""Applies monkey-patches to pyparsing"""
@@ -23,20 +25,26 @@ def patch_pyparsing(packrat=True, arity=True, verbose=True):
2325
if verbose:
2426
enable_pyparsing_verbose_stacktrace()
2527

28+
2629
def enable_pyparsing_packrat():
2730
"""Enables pyparsing's packrat parsing, which is much faster for the type
2831
of parsing being done in this library"""
32+
warnings.warn("Enabled pyparsing packrat parsing", ImportWarning)
2933
pyparsing.ParserElement.enablePackrat()
3034

35+
3136
def enable_pyparsing_verbose_stacktrace():
3237
"""Enables verbose stacktraces in pyparsing"""
38+
warnings.warn("Enabled pyparsing verbose stacktrace", ImportWarning)
3339
pyparsing.ParserElement.verbose_stacktrace = True
3440

41+
3542
def _trim_arity(func, maxargs=None):
3643
def wrapper(string, location, tokens):
3744
return func(string, location, tokens)
3845
return wrapper
3946

47+
4048
def disable_pyparsing_arity_trimming():
4149
"""When pyparsing encounters a TypeError when calling a parse action, it
4250
will keep trying the call the function with one less argument each time

0 commit comments

Comments
 (0)