Skip to content

Commit

Permalink
0.4.2
Browse files Browse the repository at this point in the history
This version adds a FixedDiscount price modifier.
We now also have full test coverage.
  • Loading branch information
patrys committed Aug 20, 2013
1 parent 960ce62 commit 7f3724d
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 37 deletions.
9 changes: 9 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[run]
branch = 1
omit = */tests.py
source = prices

[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.*
!.coveragerc
!.gitignore
!.travis.*
*.py[co]
Expand Down
7 changes: 5 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ python:
- 2.6
- 2.7
- 3.2
script: nosetests
install:
- pip install nose
- python setup.py install
- pip install coverage
script: coverage run setup.py test
after_success:
- pip install coveralls
- coveralls
42 changes: 32 additions & 10 deletions prices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Price(object):
def __init__(self, net, gross=None, currency=None, previous=None,
modifier=None, operation=None):
if isinstance(net, float) or isinstance(gross, float):
# pragma: nocover
warnings.warn(
RuntimeWarning(
'You should never use floats when dealing with prices!'),
Expand All @@ -40,7 +41,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
return NotImplemented # pragma: nocover

def __le__(self, other):
return self < other or self == other
Expand Down Expand Up @@ -85,7 +86,7 @@ def __sub__(self, other):
return Price(net=price_net, gross=price_gross,
currency=self.currency, previous=self, modifier=other,
operation=operator.__sub__)
return NotImplemented
return NotImplemented # pragma: nocover

@property
def tax(self):
Expand Down Expand Up @@ -155,14 +156,14 @@ def __add__(self, other):
min_price = self.min_price + other.min_price
max_price = self.max_price + other.max_price
return PriceRange(min_price=min_price, max_price=max_price)
return NotImplemented
return NotImplemented # pragma: nocover

def __sub__(self, other):
if isinstance(other, Price):
if other.currency != self.min_price.currency:
raise ValueError("Cannot subtract price in %r from pricerange"
" in %r" %
(other.min_price.currency, self.currency))
(other.currency, self.min_price.currency))
min_price = self.min_price - other
max_price = self.max_price - other
return PriceRange(min_price=min_price, max_price=max_price)
Expand All @@ -174,7 +175,7 @@ def __sub__(self, other):
min_price = self.min_price - other.min_price
max_price = self.max_price - other.max_price
return PriceRange(min_price=min_price, max_price=max_price)
return NotImplemented
return NotImplemented # pragma: nocover

def __eq__(self, other):
if isinstance(other, PriceRange):
Expand Down Expand Up @@ -208,15 +209,13 @@ class PriceModifier(object):
name = None

def apply(self, price):
raise NotImplementedError()
raise NotImplementedError() # pragma: nocover


class Tax(PriceModifier):
'''
A generic tax class, provided so all taxers have a common base.
'''
name = None

def apply(self, price_obj):
return Price(net=price_obj.net,
gross=price_obj.gross + self.calculate_tax(price_obj),
Expand All @@ -226,12 +225,12 @@ def apply(self, price_obj):
operation=operator.__add__)

def calculate_tax(self, price_obj):
raise NotImplementedError()
raise NotImplementedError() # pragma: nocover


class LinearTax(Tax):
'''
A linear tax, modifies .
Adds a certain fraction on top of the price.
'''
def __init__(self, multiplier, name=None):
self.multiplier = Decimal(multiplier)
Expand All @@ -258,6 +257,29 @@ def calculate_tax(self, price_obj):
return price_obj.gross * self.multiplier


class FixedDiscount(PriceModifier):
'''
Adds a fixed amount to the price.
'''
def __init__(self, amount, name=None):
self.amount = amount
self.name = name or self.name

def __repr__(self):
return 'FixedDiscount(%r, name=%r)' % (self.amount, self.name)

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))
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__)


def inspect_price(price_obj):
def format_inspect(data):
if isinstance(data, tuple):
Expand Down
140 changes: 116 additions & 24 deletions prices/tests/test_prices.py → prices/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import decimal
import unittest

from prices import Price, PriceRange, LinearTax, inspect_price
from prices import Price, PriceRange, LinearTax, FixedDiscount, inspect_price


class PriceTest(unittest.TestCase):
Expand All @@ -28,15 +28,27 @@ def test_multiplication(self):
p2 = self.ten_btc * 5
self.assertEqual(p1, p2)

def test_valid_comparison(self):
def test_equality(self):
p1 = Price(net='10', gross='20', currency='USD')
p2 = Price(net='10', gross='20', currency='USD')
p3 = Price(net='20', gross='20', currency='USD')
p4 = Price(net='10', gross='10', currency='USD')
p5 = Price(net='10', gross='20', currency='AUD')
self.assertEqual(p1, p2)
self.assertNotEqual(p1, p3)
self.assertNotEqual(p1, p4)
self.assertNotEqual(p1, p5)
self.assertNotEqual(p1, 10)

def test_comparison(self):
self.assertLess(self.ten_btc, self.twenty_btc)
self.assertGreater(self.twenty_btc, self.ten_btc)

def test_invalid_comparison(self):
self.assertRaises(ValueError,
lambda: self.ten_btc < self.thirty_dollars)

def test_valid_addition(self):
def test_addition(self):
p = self.ten_btc + self.twenty_btc
self.assertEqual(p.net, 30)
self.assertEqual(p.gross, 30)
Expand All @@ -46,25 +58,24 @@ def test_invalid_addition(self):
lambda: self.ten_btc + self.thirty_dollars)

def test_tax(self):
tax = LinearTax(1, name='2x Tax')
p = self.ten_btc + tax
self.assertEqual(p.net, self.ten_btc.net)
self.assertEqual(p.gross, self.ten_btc.gross * 2)
self.assertEqual(p.currency, self.ten_btc.currency)
p = Price(net='20', gross='30', currency='BTC')
self.assertEqual(p.tax, 10)

def test_inspect(self):
tax = LinearTax('1.2345678', name='Silly Tax')
p = ((self.ten_btc + self.twenty_btc) * 5 + tax).quantize('0.01')
p = ((self.ten_btc + self.twenty_btc) * 5).quantize('0.01')
self.assertEqual(
inspect_price(p),
"((Price('10', currency='BTC') + Price('20', currency='BTC')) * 5 + LinearTax('1.2345678', name='Silly Tax')).quantize('0.01')")
"((Price('10', currency='BTC') + Price('20', currency='BTC')) * 5).quantize('0.01')")

def test_elements(self):
tax = LinearTax('1.2345678', name='Silly Tax')
p = ((self.ten_btc + self.twenty_btc) * 5 + tax).quantize('0.01')
p = ((self.ten_btc + self.twenty_btc) * 5).quantize('0.01')
self.assertEqual(
p.elements(),
[self.ten_btc, self.twenty_btc, 5, tax, decimal.Decimal('0.01')])
[self.ten_btc, self.twenty_btc, 5, decimal.Decimal('0.01')])

def test_repr(self):
p = Price(net='10', gross='20', currency='GBP')
self.assertEqual(repr(p), "Price(net='10', gross='20', currency='GBP')")


class PriceRangeTest(unittest.TestCase):
Expand All @@ -81,7 +92,7 @@ def test_basics(self):
self.assertEqual(self.range_ten_twenty.min_price, self.ten_btc)
self.assertEqual(self.range_ten_twenty.max_price, self.twenty_btc)

def test_valid_addition(self):
def test_addition(self):
pr1 = self.range_ten_twenty + self.range_thirty_forty
self.assertEqual(pr1.min_price, self.ten_btc + self.thirty_btc)
self.assertEqual(pr1.max_price, self.twenty_btc + self.forty_btc)
Expand All @@ -94,21 +105,33 @@ def test_invalid_addition(self):

def test_subtraction(self):
pr1 = self.range_thirty_forty - self.range_ten_twenty
pr2 = self.range_thirty_forty - self.ten_btc
self.assertEqual(pr1.min_price, self.thirty_btc - self.ten_btc)
self.assertEqual(pr1.max_price, self.forty_btc - self.twenty_btc)

def test_valid_membership(self):
'''
Prices can fit in a pricerange.
'''
self.assertEqual(pr2.min_price, self.thirty_btc - self.ten_btc)
self.assertEqual(pr2.max_price, self.forty_btc - self.ten_btc)

def test_invalid_subtraction(self):
pr = self.range_thirty_forty
p = Price(10, currency='USD')
self.assertRaises(ValueError, lambda: pr - p)

def test_equality(self):
pr1 = PriceRange(self.ten_btc, self.twenty_btc)
pr2 = PriceRange(self.ten_btc, self.twenty_btc)
pr3 = PriceRange(self.ten_btc, self.ten_btc)
pr4 = PriceRange(self.twenty_btc, self.twenty_btc)
self.assertEqual(pr1, pr2)
self.assertNotEqual(pr1, pr3)
self.assertNotEqual(pr1, pr4)
self.assertNotEqual(pr1, self.ten_btc)

def test_membership(self):
self.assertTrue(self.ten_btc in self.range_ten_twenty)
self.assertTrue(self.twenty_btc in self.range_ten_twenty)
self.assertFalse(self.thirty_btc in self.range_ten_twenty)

def test_invalid_membership(self):
'''
Non-prices can't fit in a pricerange.
'''
self.assertRaises(TypeError, lambda: 15 in self.range_ten_twenty)

def test_replacement(self):
Expand All @@ -119,7 +142,32 @@ def test_replacement(self):
self.assertEqual(pr2.min_price, self.twenty_btc)
self.assertEqual(pr2.max_price, self.forty_btc)

def test_tax(self):
def test_repr(self):
pr1 = self.range_thirty_forty
pr2 = PriceRange(self.ten_btc, self.ten_btc)
self.assertEqual(
repr(pr1),
"PriceRange(Price('30', currency='BTC'), Price('40', currency='BTC'))")
self.assertEqual(
repr(pr2),
"PriceRange(Price('10', currency='BTC'))")


class LinearTaxTest(unittest.TestCase):

def setUp(self):
self.ten_btc = Price(10, currency='BTC')
self.twenty_btc = Price(20, currency='BTC')
self.range_ten_twenty = PriceRange(self.ten_btc, self.twenty_btc)

def test_price(self):
tax = LinearTax(1, name='2x Tax')
p = self.ten_btc + tax
self.assertEqual(p.net, self.ten_btc.net)
self.assertEqual(p.gross, self.ten_btc.gross * 2)
self.assertEqual(p.currency, self.ten_btc.currency)

def test_pricerange(self):
tax_name = '2x Tax'
tax = LinearTax(1, name=tax_name)
pr = self.range_ten_twenty + tax
Expand All @@ -130,6 +178,50 @@ def test_tax(self):
self.assertEqual(pr.max_price.gross, self.twenty_btc.gross * 2)
self.assertEqual(pr.max_price.currency, self.twenty_btc.currency)

def test_comparison(self):
tax1 = LinearTax(1)
tax2 = LinearTax(2)
self.assertLess(tax1, tax2)
self.assertGreater(tax2, tax1)
self.assertRaises(TypeError, lambda: tax1 < 10)

def test_equality(self):
tax1 = LinearTax(1)
tax2 = LinearTax(1)
tax3 = LinearTax(2)
self.assertEqual(tax1, tax2)
self.assertNotEqual(tax1, tax3)
self.assertNotEqual(tax1, 1)

def test_repr(self):
tax = LinearTax(decimal.Decimal('1.23'), name='VAT')
self.assertEqual(repr(tax), "LinearTax('1.23', name='VAT')")


class FixedDiscountTest(unittest.TestCase):

def setUp(self):
self.ten_btc = Price(10, currency='BTC')
self.ten_usd = Price(10, currency='USD')
self.thirty_btc = Price(30, currency='BTC')

def test_discount(self):
discount = FixedDiscount(self.ten_btc, name='Ten off')
p = self.thirty_btc + discount
self.assertEqual(p.net, 20)
self.assertEqual(p.gross, 20)
self.assertEqual(p.currency, 'BTC')

def test_currency_mismatch(self):
discount = FixedDiscount(self.ten_usd)
self.assertRaises(ValueError, lambda: self.ten_btc + discount)

def test_repr(self):
discount = FixedDiscount(self.ten_usd, name='Ten off')
self.assertEqual(
repr(discount),
"FixedDiscount(Price('10', currency='USD'), name='Ten off')")


if __name__ == '__main__':
unittest.main()
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
author_email='[email protected]',
description='Python price handling for humans',
license='BSD',
version='0.4.1',
version='0.4.2',
url='http://satchless.com/',
packages=['prices'],
test_suite='prices.tests',
include_package_data=True,
classifiers=CLASSIFIERS,
platforms=['any'])

0 comments on commit 7f3724d

Please sign in to comment.