4
\$\begingroup\$

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
\$\endgroup\$
10
  • \$\begingroup\$ Your code is very concise and I like it. You clearly made effort to reduce repetition. But you still haven't eliminated repetition completely. You have currency type check that raises exceptions in each of the dunder methods that does something, the message is almost identical, except for some words related to method name. You have to extract it as a separated helper method and have the other methods call it to do the checking, with the method type as an argument. \$\endgroup\$ Commented Jun 3, 2023 at 17:07
  • \$\begingroup\$ And then this doesn't make sense at all, money of different currency systems can be compared, added and subtracted. That is why we have currency exchange rate, just convert one currency to the other using the exchange rate and do it as normal. \$\endgroup\$ Commented Jun 3, 2023 at 17:09
  • 1
    \$\begingroup\$ But the ability to multiply (and therefore divide) money is pure nonsense, it doesn't make sense at all. Two dollars times two is four dollars. What is two dollars times two dollars? Four dollars squared? The other number needs to be unitless. \$\endgroup\$ Commented Jun 3, 2023 at 17:12
  • \$\begingroup\$ What do you think about make Money and Currency a named tuples and define function convert_currency? \$\endgroup\$ Commented Jun 3, 2023 at 17:12
  • 1
    \$\begingroup\$ I can't offer anymore advices on code, I only know how to make code more concise and efficient, efficiency isn't a problem here, and your code is very concise except for the one function you failed to extract. But you do have two major logical failures that need to be fixed. \$\endgroup\$ Commented Jun 3, 2023 at 17:16

3 Answers 3

3
\$\begingroup\$

The Currency class is simple and nice.

Thank you for the unit tests! They are helpful.


    exchange_rate: Decimal

I have no idea what that means, and the comments, docstrings, narrative do not enlighten me.

Is that a rate to convert the given currency to Yen, to Dinar, to something else? That is, are we measuring a US Dollar -> Yen rate? A Euro -> Yen rate?

Are you sure this attribute is really needed here? Consider removing it. Currency conversion can be a complex topic, for example the time and place of conversion make a difference. It probably belongs in a separate module.


I am uncomfortable with rounding in the Money class. Also, I wouldn't mind seeing both

  • Money(Decimal(1/8), usd) and
  • Money('0.125', usd)

behave identically, for the sake of the caller's convenience. That is, I think of a "money" as a "decimal" quantity, so asking me to use it in a different way doesn't quite feel right. Perhaps Currency should always offer a helper that constructs a new Money ?

I appreciate that we consistently round to precision on the way out, e.g. with expressions like invoice.amount or str(invoice). But I'm not sure how an app developer is supposed to reason about a quantity like this:

invoice = Money(Decimal('.12'), usd)
invoice += Money(Decimal('.135'), usd)

At this point str(invoice) will reveal pennies, despite having precision to mils on the inside.

(BTW, fun fact, '.125' --> .12 while '.135' --> .14, which is correct round-to-even behavior.)

I think my objection boils down to discomfort with tracking two distinct quanitities:

  • public invoice.precision == -2 above, yet
  • invoice._amount.as_tuple().exponent == -3

I don't see a motivating use case for this in the narrative nor in the test suite. I think I recommend abandoning the redundant invoice.precision attribute entirely.

I'm not happy that an app developer can silently misuse the ctor and misuse the setter. There's no checking, no duck typed operation, which immediately blows up when e.g. a str is passed in rather than a Decimal. Simplest way to fix? I recommend that you assign Decimal(amount) and assign Decimal(value), silently "fixing" it on the way in. Consider raising an error if someone foolishly passes in a float, e.g. Money(1/8, usd) or Money(0.10, usd) probably won't do what developer intended, and he will thank you for immediately pointing that out.


I like the "currencies must match!" rule for addition. Let the app developer do an approximate conversion rate calculation if he really wants to add one dollar to one euro.

It's odd that __sub__ doesn't just __add__ with flipped sign. That would let you code the "must match!" test just once.

What's going on with mul & div ?!? That's just crazy. Unconditionally raise an error and be done with it. You export .amount and that suffices.

Nice use of total_ordering !


In the money_euro test helper, special treatment for precision of None is a bit odd. Consider defining a kw dict which is either empty or contains a numeric value, and finish the Money call with **kw)

Consider defining eur and usd in the test suite or perhaps in the money module.


Tests are the one place where "copy-n-paste is OK". But I found that the repeated pasting of four money pairs distracted me from easily reading what mattered, which is the differences between the various tests. Assign the list to a variable so you can concisely refer to it.

Consider writing a hypothesis test for this module.

\$\endgroup\$
8
  • \$\begingroup\$ Should I compare in __eq__ and __lt__ only if self.amount is equal/less than other.amount and raise an error if objects have a different currencies or do conversions to common currency, for instance USD and compare accordingly objects? \$\endgroup\$ Commented Jun 3, 2023 at 18:28
  • \$\begingroup\$ Tell the app developer that comparing apple to orange, or dollar to euro, is raise TypeError("not supported") . Otherwise we need to worry about buy-side vs sell-side conversion rate, documentation, and lots of fractional digits. There's no motivating use case, so just bail, don't even go there. The app developer can access invoice.amount to convert currencies to his heart's content. \$\endgroup\$
    – J_H
    Commented Jun 3, 2023 at 18:32
  • \$\begingroup\$ Do you think that I should in the constructor assign to self.amount = Decimal(amount)? I did a comparison if not isinstance(amount, str): raise TypeError(...), and made type annotation amount: str. Is it a good way? \$\endgroup\$ Commented Jun 3, 2023 at 18:36
  • \$\begingroup\$ The concern I gave voice to was that ctor and setter might assign self._amount = ".125" if an app developer mistakenly passed that in. You document that caller must supply amount: Decimal, and I recommend you enforce that self._amount shall always be a Decimal. We call this a Class Invariant. Documenting something more permissive, like amount: str | Decimal, would be fine. I also mentioned that we should probably raise if app developer mistakenly passes in a float. I'm just trying to anticipate how folks would misuse the module, and how hard the bugs are to find. \$\endgroup\$
    – J_H
    Commented Jun 3, 2023 at 18:41
  • \$\begingroup\$ I have answered my own question. Is the mine proposed code better? \$\endgroup\$ Commented Jun 3, 2023 at 18:47
2
\$\begingroup\$

Other answers (and comments that should be answers) have covered most of my concerns.

This does not follow how most locales would prefer to format their currency:

f"{self.amount} {self.currency.symbol}"

Instead use locale support, which (among other things) will give you your international currency name, proper formatting, grouping etc. Also, you should either freeze your currency dataclass or use NamedTuple instead. As a demonstration,

import locale
from contextlib import contextmanager
from decimal import Decimal
from functools import lru_cache
from numbers import Number
from pprint import pprint
from typing import NamedTuple, Iterator, Any


class Currency(NamedTuple):
    value_eur: Decimal
    display_locale: str

    @contextmanager
    def use_locale(self) -> Iterator:
        """This is not thread-safe"""
        old_locale = locale.getlocale(category=locale.LC_MONETARY)
        try:
            locale.setlocale(category=locale.LC_MONETARY, locale=self.display_locale)
            yield
        finally:
            locale.setlocale(category=locale.LC_MONETARY, locale=old_locale)

    @property
    @lru_cache(maxsize=1)
    def localeconv(self) -> dict[str, Any]:
        """A dictionary of int_curr_symbol, currency_symbol, etc. For details see
        https://docs.python.org/3/library/locale.html#locale.localeconv
        """
        with self.use_locale():
            return locale.localeconv()

    def __str__(self) -> str:
        return self.localeconv['int_curr_symbol']

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.display_locale!r}, {self.value_eur!r})"

    def format(
        self, val: Number,
        symbol: bool = True, grouping: bool = True, international: bool = False,
    ) -> str:
        with self.use_locale():
            return locale.currency(val=val, symbol=symbol, grouping=grouping,
                                   international=international)

    def __truediv__(self, other: 'Currency') -> Decimal:
        return self.value_eur / other.value_eur


cad = Currency(display_locale='en_CA', value_eur=Decimal(0.69680235))
inr = Currency(display_locale='ta_IN', value_eur=Decimal(0.011331307))
pprint(inr.localeconv)
print(cad.format(23_400_000))
print(inr.format(23_400_000))
print(
    cad.format(1, international=True), 'is worth',
    inr.format(cad/inr, international=True),
)
{'currency_symbol': '₹',
 'decimal_point': '.',
 'frac_digits': 2,
 'grouping': [],
 'int_curr_symbol': 'INR',
 'int_frac_digits': 2,
 'mon_decimal_point': '.',
 'mon_grouping': [3, 2, 0],
 'mon_thousands_sep': ',',
 'n_cs_precedes': 1,
 'n_sep_by_space': 1,
 'n_sign_posn': 4,
 'negative_sign': '-',
 'p_cs_precedes': 1,
 'p_sep_by_space': 1,
 'p_sign_posn': 4,
 'positive_sign': '',
 'thousands_sep': ''}
$23,400,000.00
₹ 2,34,00,000.00
CAD1.00 is worth INR61.49
\$\endgroup\$
1
\$\begingroup\$

Edit made to code:

money.py:

import functools
from decimal import Decimal
from typing import NamedTuple


class Currency(NamedTuple):
    name: str
    symbol: str

    def __str__(self) -> str:
        return self.name

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"


@functools.total_ordering
class Money:
    def __init__(self, amount: Decimal, currency: Currency):
        if isinstance(amount, float):
            raise TypeError("amount should not be float")
        self.amount = Decimal(amount)
        self.currency = currency

    def __str__(self) -> str:
        return f"{self.amount} {self.currency.symbol}"

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(amount={self.amount!r}, currency={self.currency!r})"

    def __lt__(self, other):
        if self.currency != other.currency:
            raise ValueError("comparison between different currencies is not supported")
        return self.amount < other.amount

    def __eq__(self, other):
        if self.currency != other.currency:
            raise ValueError("comparison between different currencies is not supported")
        return self.amount == other.amount


EUR = Currency("Euro", "EUR")
USD = Currency("American dollar", "USD")

test_money.py:

import pytest
from example_shop.shop.money import Money, EUR, USD
from decimal import Decimal


@pytest.mark.parametrize("price1,price2,expected",
                         [(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), EUR), True),
                          (Money(Decimal("1.5"), EUR), Money(Decimal("1"), EUR), False),
                          (Money(Decimal("-2"), USD), Money(Decimal("0"), USD), True),
                          (Money(Decimal("0"), EUR), Money(Decimal("0"), EUR), False)])
def test_less_than_the_same_currency(price1, price2, expected):
    assert (price1 < price2) == expected


@pytest.mark.parametrize("price1,price2,expected",
                         [(Money(Decimal("1.23"), USD), Money(Decimal("4.56"), USD), False),
                          (Money(Decimal("1.5"), EUR), Money(Decimal("1"), EUR), False),
                          (Money(Decimal("-2"), EUR), Money(Decimal("0"), EUR), False),
                          (Money(Decimal("0"), USD), Money(Decimal("0"), USD), True)])
def test_equal_the_same_currency(price1, price2, expected):
    assert (price1 == price2) == expected


@pytest.mark.parametrize("price1,price2",
                         [(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), USD)),
                          (Money(Decimal("1.5"), USD), Money(Decimal("1"), EUR))])
def test_less_than_different_currency(price1, price2):
    with pytest.raises(ValueError):
        assert price1 < price2


@pytest.mark.parametrize("price1,price2",
                         [(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), USD)),
                          (Money(Decimal("1.5"), USD), Money(Decimal("1"), EUR))])
def test_equal_different_currency(price1, price2):
    with pytest.raises(ValueError):
        assert price1 == price2
\$\endgroup\$
6
  • \$\begingroup\$ Better you edit your question, instead of posting this as answer :) \$\endgroup\$ Commented Jun 3, 2023 at 19:01
  • \$\begingroup\$ I'll remember next time \$\endgroup\$ Commented Jun 3, 2023 at 19:12
  • 4
    \$\begingroup\$ @BillalBegueradj, I know you're trying to be helpful, and I thank you for that. On SO sometimes appending "I found it!" to a question is most helpful, and sometimes a self-answer works best. But CodeReview is slightly different. I direct your attention to item 6: "After an answer is posted, you must not edit your question to invalidate any advice." If OP revises code in the question, such edits will soon be reverted. The idea is so folks reading a year later will see the original text + suggestions. \$\endgroup\$
    – J_H
    Commented Jun 3, 2023 at 19:50
  • \$\begingroup\$ @J_H thank you for the information \$\endgroup\$ Commented Jun 4, 2023 at 9:09
  • \$\begingroup\$ Thanks for posting this code. It's a good idea to summarise which changes you made, and why - a self-answer ought to review the code, just like any other answer. \$\endgroup\$ Commented Jun 4, 2023 at 18:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.