From 7f3724d6c167c407108d8f19ab7e0ba6d3ef41e4 Mon Sep 17 00:00:00 2001 From: Patryk Zawadzki Date: Tue, 20 Aug 2013 16:40:58 +0200 Subject: [PATCH] 0.4.2 This version adds a FixedDiscount price modifier. We now also have full test coverage. --- .coveragerc | 9 ++ .gitignore | 1 + .travis.yml | 7 +- prices/__init__.py | 42 +++++-- prices/{tests/test_prices.py => tests.py} | 140 ++++++++++++++++++---- setup.py | 3 +- 6 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 .coveragerc rename prices/{tests/test_prices.py => tests.py} (52%) diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ea1c10c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +branch = 1 +omit = */tests.py +source = prices + +[report] +exclude_lines = + pragma: no cover + raise NotImplementedError diff --git a/.gitignore b/.gitignore index d73cafe..01a4857 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .* +!.coveragerc !.gitignore !.travis.* *.py[co] diff --git a/.travis.yml b/.travis.yml index d34c96d..0e34000 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/prices/__init__.py b/prices/__init__.py index f5479d8..258779b 100644 --- a/prices/__init__.py +++ b/prices/__init__.py @@ -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!'), @@ -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 @@ -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): @@ -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) @@ -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): @@ -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), @@ -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) @@ -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): diff --git a/prices/tests/test_prices.py b/prices/tests.py similarity index 52% rename from prices/tests/test_prices.py rename to prices/tests.py index 8a0aa8a..fa189f8 100644 --- a/prices/tests/test_prices.py +++ b/prices/tests.py @@ -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): @@ -28,7 +28,19 @@ 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) @@ -36,7 +48,7 @@ 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) @@ -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): @@ -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) @@ -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): @@ -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 @@ -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() diff --git a/setup.py b/setup.py index 70b54d2..4353bd4 100644 --- a/setup.py +++ b/setup.py @@ -22,9 +22,10 @@ author_email='hello@mirumee.com', 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'])