I have recently created the Money class with a helper class Currency. What do you think about the following code? I created unit tests for my class using very nice pytest library.
Is it a good idea to put code tests in the if __name__ == '__main__' or should I always use dedicated libraries like unittest and pytest?
Is it okay to make self attribute assignment depend on the another attribute assignment, for example let's suppose we have an assignment self.amount = default_value depending on whether self.precision is defined first, where self.amount is property setter that uses self.precision?
How can I simplify repeating decorators code like I have in the test_money.py?
If I am going to gather exchange rate info from the Internet, should I still use namedtuple or use dataclasses?
Thanks
money.py:
import functools
from decimal import Decimal
from dataclasses import dataclass
@dataclass
class Currency:
name: str
symbol: str
exchange_rate: Decimal
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.name!r}, {self.symbol!r}, {self.exchange_rate!r})"
@functools.total_ordering
class Money:
def __init__(self, amount: Decimal, currency: Currency, precision: int = 2):
self._amount = amount
self.precision = precision
self.currency = currency
@property
def amount(self) -> Decimal:
return self._amount.quantize(Decimal("10") ** -self.precision)
@amount.setter
def amount(self, value: Decimal) -> None:
self._amount = value
def __str__(self) -> str:
return f"{self.amount} {self.currency.symbol}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.amount!r}, {self.currency!r})"
def __add__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot add two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot subtract two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot multiply two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount * other.amount, self.currency, self.precision)
def __truediv__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot divide two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount / other.amount, self.currency, self.precision)
def __iadd__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot add two {self.__class__.__name__} instances: "
"different currency property")
self.amount += other.amount
return self
def __isub__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot subtract two {self.__class__.__name__} instances: "
"different currency property")
self.amount -= other.amount
return self
def __imul__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot multiply two {self.__class__.__name__} instances: "
"different currency property")
self.amount *= other.amount
return self
def __idiv__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot divide two {self.__class__.__name__} instances: "
"different currency property")
self.amount /= other.amount
return self
def __lt__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot compare two {self.__class__.__name__} instances: "
"different currency property")
return self.amount < other.amount
def __eq__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot compare two {self.__class__.__name__} instances: "
"different currency property")
return self.amount == other.amount
test_money.py:
import pytest
from example_shop.shop.money import Currency, Money
from decimal import Decimal
from typing import Optional
def money_euro(amount: str, precision: Optional[int] = None) -> Money:
if precision is None:
return Money(Decimal(amount), Currency("Euro", "EUR", Decimal("4.52")))
return Money(Decimal(amount), Currency("Euro", "EUR", Decimal("4.52")), precision)
def money_usd(amount: str, precision: Optional[int] = None) -> Money:
if precision is None:
return Money(Decimal(amount), Currency("American dollar", "USD", Decimal("4.17")))
return Money(Decimal(amount), Currency("American dollar", "USD", Decimal("4.17")), precision)
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("2.7")),
(money_usd("2.7"), money_usd("1.3"), money_usd("4.0")),
(money_usd("2.700"), money_usd("1.3"), money_usd("4.00000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("3", 4)),
(money_usd("-1.5", 4), money_usd("3", 5), money_usd("1.5", 5))])
def test_money_add_the_same_currency(price1, price2, expected):
assert price1 + price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("0.3")),
(money_usd("2.7"), money_usd("1.3"), money_usd("1.4")),
(money_usd("2.700"), money_usd("1.3"), money_usd("1.40000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("3", 5), money_usd("-1.5", 5))])
def test_money_subtract_the_same_currency(price1, price2, expected):
assert price1 - price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.8")),
(money_usd("2.7"), money_usd("1.3"), money_usd("3.51")),
(money_usd("2.700"), money_usd("1.3"), money_usd("3.51000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-4.5", 5))])
def test_money_multiply_the_same_currency(price1, price2, expected):
assert price1 * price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.25")),
(money_usd("2.7"), money_usd("1.3"), money_usd("2.08")),
(money_usd("2.700"), money_usd("1.3"), money_usd("2.08000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-0.5", 5))])
def test_money_divide_the_same_currency(price1, price2, expected):
assert price1 / price2 == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_add_the_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 + price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_subtract_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 - price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_multiply_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 * price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_divide_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 / price2
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("2.7")),
(money_usd("2.7"), money_usd("1.3"), money_usd("4.0")),
(money_usd("2.700"), money_usd("1.3"), money_usd("4.00000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("3", 4)),
(money_usd("-1.5", 4), money_usd("3", 5), money_usd("1.5", 5))])
def test_money_add_in_place_the_same_currency(price1, price2, expected):
result = price1
result += price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("0.3")),
(money_usd("2.7"), money_usd("1.3"), money_usd("1.4")),
(money_usd("2.700"), money_usd("1.3"), money_usd("1.40000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("3", 5), money_usd("-1.5", 5))])
def test_money_subtract_in_place_the_same_currency(price1, price2, expected):
result = price1
result -= price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.8")),
(money_usd("2.7"), money_usd("1.3"), money_usd("3.51")),
(money_usd("2.700"), money_usd("1.3"), money_usd("3.51000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-4.5", 5))])
def test_money_multiply_in_place_the_same_currency(price1, price2, expected):
result = price1
result *= price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.25")),
(money_usd("2.7"), money_usd("1.3"), money_usd("2.08")),
(money_usd("2.700"), money_usd("1.3"), money_usd("2.08000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-0.5", 5))])
def test_money_divide_in_place_the_same_currency(price1, price2, expected):
result = price1
result /= price2
assert result == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_add_in_place_the_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result += price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_subtract_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result -= price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_multiply_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result *= price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_divide_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result /= price2
assert result
@pytest.mark.parametrize("price1,price2,expected",
[(money_euro("1.23"), money_euro("4.56"), True),
(money_euro("1.5"), money_euro("1"), False),
(money_usd("-2"), money_usd("0"), True),
(money_euro("0"), money_euro("0"), False)])
def test_less_than_the_same_currency(price1, price2, expected):
assert (price1 < price2) == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_euro("1.23"), money_euro("4.56"), False),
(money_euro("1.5"), money_euro("1"), False),
(money_usd("-2"), money_usd("0"), False),
(money_euro("0"), money_euro("0"), True)])
def test_equal_the_same_currency(price1, price2, expected):
assert (price1 == price2) == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.23"), money_euro("4.56")),
(money_euro("1.5"), money_usd("1"))])
def test_less_than_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 < price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.23"), money_euro("4.56")),
(money_euro("1.5"), money_usd("1"))])
def test_equal_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 == price2
MoneyandCurrencya named tuples and define functionconvert_currency? \$\endgroup\$