Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to the library to make the Money, Rate and Number objects copyable with copy.copy / copy.deepcopy #243

Merged
merged 6 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.5.6] - 2023-08-15

* Added so that `Money`, `Number` and `Rate` objects can now be copied using the `copy.copy()` and `copy.deepcopy()` functions.

## [0.5.5] - 2022-12-07

* Additional support to parse input values from third parties, data models, etc.
Expand Down
604 changes: 325 additions & 279 deletions poetry.lock

Large diffs are not rendered by default.

91 changes: 33 additions & 58 deletions stockholm/money.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@ def sum(
)

@classmethod
def _is_unknown_amount_type(cls, amount: Optional[Union[MoneyType, Decimal, int, float, str, object]]) -> bool:
def _is_unknown_amount_type(
cls, amount: Optional[Union[MoneyType, "MoneyModel[Any]", Decimal, int, float, str, object]]
) -> bool:
return not any(map(lambda type_: isinstance(amount, type_), (Money, Decimal, int, bool, float, str)))

@classmethod
def from_sub_units(
cls: Type[MoneyType],
amount: Optional[Union[MoneyType, Decimal, int, float, str, object]],
amount: Optional[Union[MoneyType, "MoneyModel[Any]", Decimal, int, float, str, object]],
currency: Optional[Union[DefaultCurrencyValue, CurrencyValue, str]] = DefaultCurrency,
value: Optional[Union[MoneyType, Decimal, int, float, str]] = None,
value: Optional[Union[MoneyType, "MoneyModel[Any]", Decimal, int, float, str]] = None,
currency_code: Optional[str] = None,
**kwargs: Any,
) -> MoneyType:
Expand Down Expand Up @@ -122,14 +124,22 @@ def from_protobuf(
}
)

@classmethod
def from_proto(
cls: Type[MoneyType],
input_value: Union[str, bytes, object],
proto_class: Type[GenericProtobufMessage] = MoneyProtobufMessage,
) -> MoneyType:
return cast(MoneyType, cls.from_protobuf(input_value, proto_class=proto_class))

def __init__(
self,
amount: Optional[Union[MoneyType, Decimal, Dict, int, float, str, object]] = None,
amount: Optional[Union[MoneyType, "MoneyModel[Any]", Decimal, Dict, int, float, str, object]] = None,
currency: Optional[Union[DefaultCurrencyValue, CurrencyValue, str]] = DefaultCurrency,
from_sub_units: Optional[bool] = None,
units: Optional[int] = None,
nanos: Optional[int] = None,
value: Optional[Union[MoneyType, Decimal, int, float, str]] = None,
value: Optional[Union[MoneyType, "MoneyModel[Any]", Decimal, int, float, str]] = None,
currency_code: Optional[str] = None,
**kwargs: Any,
) -> None:
Expand Down Expand Up @@ -241,6 +251,7 @@ def __init__(
"currency",
"currency_code",
"from_sub_units",
"sub_units",
)
if hasattr(amount, k)
}
Expand Down Expand Up @@ -533,10 +544,10 @@ def sub(self, other: Any, from_sub_units: Optional[bool] = None) -> MoneyType:
def to_integral(self) -> MoneyType:
return self.__round__(0)

def to_currency(self, currency: Optional[Union[CurrencyValue, str]]) -> MoneyType:
def to_currency(self, currency: Optional[Union[CurrencyValue, str]]) -> Union[MoneyType, "MoneyModel[Any]"]:
return cast(MoneyType, self.__class__(self, currency=currency))

def to(self, currency: Optional[Union[CurrencyValue, str]]) -> MoneyType:
def to(self, currency: Optional[Union[CurrencyValue, str]]) -> Union[MoneyType, "MoneyModel[Any]"]:
return self.to_currency(currency)

def to_sub_units(self) -> MoneyType:
Expand Down Expand Up @@ -584,7 +595,7 @@ def amount_as_string(self, min_decimals: Optional[int] = None, max_decimals: Opt
return value

def __repr__(self) -> str:
return f'<stockholm.Money: "{self}">'
return f'<stockholm.{self.__class__.__name__}: "{self}">'

def __str__(self) -> str:
return self.as_string()
Expand Down Expand Up @@ -692,7 +703,7 @@ def __format__(self, format_spec: str) -> str:
return output

def __hash__(self) -> int:
return hash(("stockholm.Money", self._amount, self._currency))
return hash((f"stockholm.{self.__class__.__name__}", str(self._amount), self._currency))

def __bool__(self) -> bool:
return bool(self._amount)
Expand Down Expand Up @@ -897,57 +908,21 @@ def __round__(self, ndigits: int = 0) -> MoneyType:

return cast(MoneyType, self.__class__(amount, currency=self._currency))

def __reduce__(
self,
) -> Tuple[Type[MoneyType], Tuple[str, Optional[Union[CurrencyValue, str]]]]:
return cast(Type[MoneyType], self.__class__), (str(self._amount), self._currency)

class Money(MoneyModel):
@classmethod
def from_sub_units(
cls,
amount: Optional[Union[MoneyType, Decimal, int, float, str, object]],
currency: Optional[Union[DefaultCurrencyValue, CurrencyValue, str]] = DefaultCurrency,
value: Optional[Union[MoneyType, Decimal, int, float, str]] = None,
currency_code: Optional[str] = None,
**kwargs: Any,
) -> "Money":
return cast(
Money,
super().from_sub_units(
amount=amount, currency=currency, value=value, currency_code=currency_code, **kwargs
),
)
def __copy__(self) -> MoneyModel[MoneyType]:
return self

@classmethod
def from_dict(cls, input_dict: Dict) -> "Money":
return cls(**input_dict)
def __deepcopy__(self, memo: Dict) -> MoneyModel[MoneyType]:
return self

@classmethod
def from_json(cls, input_value: Union[str, bytes]) -> "Money":
return cls(**json.loads(input_value))

@classmethod
def from_protobuf(
cls, input_value: Union[str, bytes, object], proto_class: Type[GenericProtobufMessage] = MoneyProtobufMessage
) -> "Money":
if input_value is not None and isinstance(input_value, bytes):
input_value = proto_class.FromString(input_value)
class Money(MoneyModel["Money"]):
def to_currency(self, currency: Optional[Union[CurrencyValue, str]]) -> "Money":
return cast("Money", super().to_currency(currency=currency))

return cls(
**{
k: getattr(input_value, k)
for k in (
"value",
"units",
"nanos",
"amount",
"currency",
"currency_code",
"from_sub_units",
)
if hasattr(input_value, k)
}
)

@classmethod
def from_proto(
cls, input_value: Union[str, bytes, object], proto_class: Type[GenericProtobufMessage] = MoneyProtobufMessage
) -> "Money":
return cls.from_protobuf(input_value, proto_class=proto_class)
def to(self, currency: Optional[Union[CurrencyValue, str]]) -> "Money":
return cast("Money", super().to(currency=currency))
36 changes: 16 additions & 20 deletions stockholm/rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,34 @@
from .compat import CurrencyValue
from .currency import DefaultCurrency, DefaultCurrencyValue
from .exceptions import ConversionError
from .money import Money, MoneyType
from .money import Money, MoneyModel, MoneyType

DEFAULT_MIN_DECIMALS = 0
DEFAULT_MAX_DECIMALS = 9

RoundingContext = decimal.Context(rounding=ROUND_HALF_UP)


class Rate(Money):
class NumericType(MoneyModel[MoneyType]):
_currency: None

@classmethod
def from_sub_units(
cls,
amount: Optional[Union[MoneyType, Decimal, int, float, str, object]],
amount: Optional[Union[MoneyType, MoneyModel[Any], Decimal, int, float, str, object]],
currency: Optional[Union[DefaultCurrencyValue, CurrencyValue, str]] = DefaultCurrency,
value: Optional[Union[MoneyType, Decimal, int, float, str]] = None,
value: Optional[Union[MoneyType, MoneyModel[Any], Decimal, int, float, str]] = None,
currency_code: Optional[str] = None,
**kwargs: Any,
) -> "Rate":
) -> MoneyType:
raise ConversionError("Rates and numbers cannot be created from sub units")

@classmethod
def from_dict(cls, input_dict: Dict) -> "Rate":
return cls(**input_dict)

def __init__(
self,
amount: Optional[Union[MoneyType, Decimal, Dict, int, float, str, object]] = None,
amount: Optional[Union[MoneyType, MoneyModel[Any], Decimal, Dict, int, float, str, object]] = None,
units: Optional[int] = None,
nanos: Optional[int] = None,
value: Optional[Union[MoneyType, Decimal, int, float, str]] = None,
value: Optional[Union[MoneyType, MoneyModel[Any], Decimal, int, float, str]] = None,
**kwargs: Any,
) -> None:
currency = kwargs.get("currency", DefaultCurrency)
Expand Down Expand Up @@ -104,22 +100,22 @@ def amount_as_string(self, min_decimals: Optional[int] = None, max_decimals: Opt
def to_currency(self, currency: Optional[Union[CurrencyValue, str]]) -> Money:
return Money(self, currency=currency)

def to_sub_units(self) -> MoneyType: # type: ignore
def to_sub_units(self) -> MoneyType:
raise ConversionError("Rates and numbers cannot be measured in sub units")

def __repr__(self) -> str:
return f'<stockholm.Rate: "{self}">'
return f'<stockholm.{self.__class__.__name__}: "{self}">'

def __hash__(self) -> int:
return hash(("stockholm.Rate", self._amount))
return hash((f"stockholm.{self.__class__.__name__}", self._amount))


ExchangeRate = Rate
class Rate(NumericType["Rate"]):
pass


class Number(Rate):
def __repr__(self) -> str:
return f'<stockholm.Number: "{self}">'
ExchangeRate = Rate

def __hash__(self) -> int:
return hash(("stockholm.Number", self._amount))

class Number(NumericType["Number"]):
pass
Loading