Skip to content

Commit

Permalink
Merge pull request #7 from mirumee/use-namedtuples
Browse files Browse the repository at this point in the history
Make Price and PriceRange namedtuples
  • Loading branch information
patrys committed Oct 21, 2013
2 parents 62c4e7b + 40ff6f5 commit c078a64
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 71 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ source = prices
exclude_lines =
pragma: no cover
raise NotImplementedError
return NotImplemented
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ python:
- 2.6
- 2.7
- 3.2
- 3.3
install:
- python setup.py install
- pip install coverage
Expand Down
119 changes: 54 additions & 65 deletions prices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
from collections import namedtuple
from decimal import Decimal, ROUND_HALF_UP
import operator
import warnings


class Price(object):
class History(namedtuple('History', 'left operator right')):

gross = Decimal('NaN')
gross_base = Decimal('NaN')
net = Decimal('NaN')
net_base = Decimal('NaN')
currency = None

def __init__(self, net, gross=None, currency=None, previous=None,
modifier=None, operation=None):
def __repr__(self):
left = self.left.history if isinstance(self.left, Price) else None
left = left or self.left
right = self.right.history if isinstance(self.right, Price) else None
right = right or self.right
if self.operator is operator.__mul__:
return '(%r * %r)' % (left, right)
elif self.operator == Price.quantize:
return '(%r).quantize(%r)' % (left, right)
elif self.operator is operator.__sub__:
return '(%r - %r)' % (left, right)
return '(%r + %r)' % (left, right)


class Price(namedtuple('Price', 'net gross currency history')):

def __new__(cls, net, gross=None, currency=None, history=None):
if isinstance(net, float) or isinstance(gross, float):
warnings.warn( # pragma: no cover
RuntimeWarning(
'You should never use floats when dealing with prices!'),
stacklevel=2)
self.net = Decimal(net)
net = Decimal(net)
if gross is not None:
self.gross = Decimal(gross)
gross = Decimal(gross)
else:
self.gross = self.net
self.currency = currency
self.previous = previous
self.modifier = modifier
self.operation = operation
gross = net
return super(Price, cls).__new__(cls, net, gross, currency, history)

def __repr__(self):
if self.net == self.gross:
Expand All @@ -40,7 +47,7 @@ def __lt__(self, other):
raise ValueError('Cannot compare prices in %r and %r' %
(self.currency, other.currency))
return self.gross < other.gross
return NotImplemented # pragma: no cover
return NotImplemented

def __le__(self, other):
return self < other or self == other
Expand All @@ -58,8 +65,9 @@ def __ne__(self, other):
def __mul__(self, other):
price_net = self.net * other
price_gross = self.gross * other
history = History(self, operator.__mul__, other)
return Price(net=price_net, gross=price_gross, currency=self.currency,
previous=self, modifier=other, operation=operator.__mul__)
history=history)

def __add__(self, other):
if isinstance(other, PriceModifier):
Expand All @@ -70,9 +78,9 @@ def __add__(self, other):
(self.currency, other.currency))
price_net = self.net + other.net
price_gross = self.gross + other.gross
history = History(self, operator.__add__, other)
return Price(net=price_net, gross=price_gross,
currency=self.currency, previous=self, modifier=other,
operation=operator.__add__)
currency=self.currency, history=history)
return NotImplemented

def __sub__(self, other):
Expand All @@ -82,44 +90,41 @@ def __sub__(self, other):
(other.currency, self.currency))
price_net = self.net - other.net
price_gross = self.gross - other.gross
history = History(self, operator.__sub__, other)
return Price(net=price_net, gross=price_gross,
currency=self.currency, previous=self, modifier=other,
operation=operator.__sub__)
return NotImplemented # pragma: no cover
currency=self.currency, history=history)
return NotImplemented

@property
def tax(self):
return self.gross - self.net

def quantize(self, exp, rounding=ROUND_HALF_UP):
exp = Decimal(exp)
history = History(self, Price.quantize, exp)
return Price(net=self.net.quantize(exp, rounding=rounding),
gross=self.gross.quantize(exp, rounding=rounding),
currency=self.currency, previous=self, modifier=exp,
operation=Price.quantize)

def inspect(self):
if self.previous:
return (self.previous.inspect(), self.operation, self.modifier)
return self
currency=self.currency, history=history)

def elements(self):
if not self.previous:
return [self]
if hasattr(self.modifier, 'elements'):
modifiers = self.modifier.elements()
if not self.history:
yield self
else:
modifiers = [self.modifier]
return self.previous.elements() + modifiers

if hasattr(self.history.left, 'elements'):
for el in self.history.left.elements():
yield el
else:
yield self.history.left
if hasattr(self.history.right, 'elements'):
for el in self.history.right.elements():
yield el
else:
yield self.history.right

class PriceRange(object):

min_price = None
max_price = None
class PriceRange(namedtuple('PriceRange', 'min_price max_price')):

def __init__(self, min_price, max_price=None):
self.min_price = min_price
def __new__(cls, min_price, max_price=None):
if max_price is None:
max_price = min_price
if min_price.currency != max_price.currency:
Expand All @@ -128,13 +133,12 @@ def __init__(self, min_price, max_price=None):
if min_price > max_price:
raise ValueError('Cannot create a pricerange from %r to %r' %
(min_price, max_price))
self.max_price = max_price
return super(PriceRange, cls).__new__(cls, min_price, max_price)

def __repr__(self):
if self.max_price == self.min_price:
return 'PriceRange(%r)' % (self.min_price,)
return ('PriceRange(%r, %r)' %
(self.min_price, self.max_price))
return 'PriceRange(%r, %r)' % (self.min_price, self.max_price)

def __add__(self, other):
if isinstance(other, PriceModifier):
Expand Down Expand Up @@ -216,12 +220,10 @@ class Tax(PriceModifier):
A generic tax class, provided so all taxers have a common base.
'''
def apply(self, price_obj):
history = History(price_obj, operator.__add__, self)
return Price(net=price_obj.net,
gross=price_obj.gross + self.calculate_tax(price_obj),
currency=price_obj.currency,
previous=price_obj,
modifier=self,
operation=operator.__add__)
currency=price_obj.currency, history=history)

def calculate_tax(self, price_obj):
raise NotImplementedError()
Expand Down Expand Up @@ -271,24 +273,11 @@ def apply(self, price_obj):
if price_obj.currency != self.amount.currency:
raise ValueError('Cannot apply a discount in %r to a price in %r' %
(self.amount.currency, price_obj.currency))
history = History(price_obj, operator.__add__, self)
return Price(net=price_obj.net - self.amount.net,
gross=price_obj.gross - self.amount.gross,
currency=price_obj.currency,
previous=price_obj,
modifier=self,
operation=operator.__add__)
currency=price_obj.currency, history=history)


def inspect_price(price_obj):
def format_inspect(data):
if isinstance(data, tuple):
op1, op, op2 = data
if op is operator.__mul__:
return '(%s) * %r' % (format_inspect(op1), op2)
elif op == Price.quantize:
return '(%s).quantize(%r)' % (format_inspect(op1), str(op2))
elif op is operator.__sub__:
return '%s - %s' % (format_inspect(op1), format_inspect(op2))
return '%s + %s' % (format_inspect(op1), format_inspect(op2))
return repr(data)
return format_inspect(price_obj.inspect())
return repr(price_obj.history or price_obj)
12 changes: 8 additions & 4 deletions prices/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import decimal
import operator
import unittest

from prices import Price, PriceRange, LinearTax, FixedDiscount, inspect_price
from prices import (FixedDiscount, History, LinearTax, Price, PriceRange,
inspect_price)


class PriceTest(unittest.TestCase):
Expand Down Expand Up @@ -70,13 +72,15 @@ def test_inspect(self):
p = ((self.ten_btc + self.twenty_btc) * 5 - self.ten_btc).quantize('0.01')
self.assertEqual(
inspect_price(p),
"((Price('10', currency='BTC') + Price('20', currency='BTC')) * 5 - Price('10', currency='BTC')).quantize('0.01')")
"((((Price('10', currency='BTC') + Price('20', currency='BTC')) * 5) - Price('10', currency='BTC'))).quantize(Decimal('0.01'))")

def test_elements(self):
p = ((self.ten_btc + self.twenty_btc) * 5).quantize('0.01')
p1 = ((self.ten_btc + self.twenty_btc) * 5).quantize('0.01')
self.assertEqual(
p.elements(),
list(p1.elements()),
[self.ten_btc, self.twenty_btc, 5, decimal.Decimal('0.01')])
p2 = Price(3, history=History(1, operator.__add__, 2))
self.assertEqual(list(p2.elements()), [1, 2])

def test_repr(self):
p = Price(net='10', gross='20', currency='GBP')
Expand Down
3 changes: 1 addition & 2 deletions setup.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from setuptools import setup

CLASSIFIERS = [
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
Expand All @@ -22,7 +21,7 @@
author_email='[email protected]',
description='Python price handling for humans',
license='BSD',
version='0.4.2',
version='0.5',
url='http://satchless.com/',
packages=['prices'],
test_suite='prices.tests',
Expand Down

0 comments on commit c078a64

Please sign in to comment.