Skip to content

Commit a8fe123

Browse files
committed
Allow all quantities multiplication by float | Percentage
This is only applying a factor so it should be safe for any Quantity. To be able to use nicer names in overloads, we use positional-only arguments for `__mul__()`, which in practice shouldn't be much of a breaking change as these methods should be used mostly via operators. We also use `match` syntax in the changed methods. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 8a045c2 commit a8fe123

File tree

2 files changed

+115
-47
lines changed

2 files changed

+115
-47
lines changed

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 115 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,19 @@ def __sub__(self, other: Self) -> Self:
334334
difference._base_value = self._base_value - other._base_value
335335
return difference
336336

337-
def __mul__(self, percent: Percentage) -> Self:
337+
@overload
338+
def __mul__(self, scalar: float, /) -> Self:
339+
"""Scale this quantity by a scalar.
340+
341+
Args:
342+
scalar: The scaler by which to scale this quantity.
343+
344+
Returns:
345+
The scaled quantity.
346+
"""
347+
348+
@overload
349+
def __mul__(self, percent: Percentage, /) -> Self:
338350
"""Scale this quantity by a percentage.
339351
340352
Args:
@@ -343,12 +355,23 @@ def __mul__(self, percent: Percentage) -> Self:
343355
Returns:
344356
The scaled quantity.
345357
"""
346-
if not isinstance(percent, Percentage):
347-
return NotImplemented
348358

349-
product = type(self).__new__(type(self))
350-
product._base_value = self._base_value * percent.as_fraction()
351-
return product
359+
def __mul__(self, value: float | Percentage, /) -> Self:
360+
"""Scale this quantity by a scalar or percentage.
361+
362+
Args:
363+
value: The scalar or percentage by which to scale this quantity.
364+
365+
Returns:
366+
The scaled quantity.
367+
"""
368+
match value:
369+
case float():
370+
return type(self)._new(self._base_value * value)
371+
case Percentage():
372+
return type(self)._new(self._base_value * value.as_fraction())
373+
case _:
374+
return NotImplemented
352375

353376
def __gt__(self, other: Self) -> bool:
354377
"""Return whether this quantity is greater than another.
@@ -584,18 +607,29 @@ def as_megawatts(self) -> float:
584607
return self._base_value / 1e6
585608

586609
@overload # type: ignore
587-
def __mul__(self, other: Percentage) -> Self:
610+
def __mul__(self, scalar: float, /) -> Self:
611+
"""Scale this power by a scalar.
612+
613+
Args:
614+
scalar: The scaler by which to scale this power.
615+
616+
Returns:
617+
The scaled quantity.
618+
"""
619+
620+
@overload
621+
def __mul__(self, percent: Percentage, /) -> Self:
588622
"""Scale this power by a percentage.
589623
590624
Args:
591-
other: The percentage by which to scale this power.
625+
percent: The percentage by which to scale this power.
592626
593627
Returns:
594628
The scaled power.
595629
"""
596630

597631
@overload
598-
def __mul__(self, other: timedelta) -> Energy:
632+
def __mul__(self, other: timedelta, /) -> Energy:
599633
"""Return an energy from multiplying this power by the given duration.
600634
601635
Args:
@@ -605,23 +639,22 @@ def __mul__(self, other: timedelta) -> Energy:
605639
The calculated energy.
606640
"""
607641

608-
def __mul__(self, other: Percentage | timedelta) -> Self | Energy:
642+
def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy:
609643
"""Return a power or energy from multiplying this power by the given value.
610644
611645
Args:
612-
other: The percentage or duration to multiply by.
646+
other: The scalar, percentage or duration to multiply by.
613647
614648
Returns:
615649
A power or energy.
616650
"""
617-
if isinstance(other, Percentage):
618-
return super().__mul__(other)
619-
if isinstance(other, timedelta):
620-
return Energy.from_watt_hours(
621-
self._base_value * other.total_seconds() / 3600.0
622-
)
623-
624-
return NotImplemented
651+
match other:
652+
case float() | Percentage():
653+
return super().__mul__(other)
654+
case timedelta():
655+
return Energy._new(self._base_value * other.total_seconds() / 3600.0)
656+
case _:
657+
return NotImplemented
625658

626659
@overload
627660
def __truediv__(self, other: Current) -> Voltage:
@@ -726,18 +759,29 @@ def as_milliamperes(self) -> float:
726759
return self._base_value * 1e3
727760

728761
@overload # type: ignore
729-
def __mul__(self, other: Percentage) -> Self:
762+
def __mul__(self, scalar: float, /) -> Self:
763+
"""Scale this current by a scalar.
764+
765+
Args:
766+
scalar: The scaler by which to scale this current.
767+
768+
Returns:
769+
The scaled quantity.
770+
"""
771+
772+
@overload
773+
def __mul__(self, percent: Percentage, /) -> Self:
730774
"""Scale this current by a percentage.
731775
732776
Args:
733-
other: The percentage by which to scale this current.
777+
percent: The percentage by which to scale this current.
734778
735779
Returns:
736780
The scaled current.
737781
"""
738782

739783
@overload
740-
def __mul__(self, other: Voltage) -> Power:
784+
def __mul__(self, other: Voltage, /) -> Power:
741785
"""Multiply the current by a voltage to get a power.
742786
743787
Args:
@@ -747,21 +791,22 @@ def __mul__(self, other: Voltage) -> Power:
747791
The calculated power.
748792
"""
749793

750-
def __mul__(self, other: Percentage | Voltage) -> Self | Power:
794+
def __mul__(self, other: float | Percentage | Voltage, /) -> Self | Power:
751795
"""Return a current or power from multiplying this current by the given value.
752796
753797
Args:
754-
other: The percentage or voltage to multiply by.
798+
other: The scalar, percentage or voltage to multiply by.
755799
756800
Returns:
757801
A current or power.
758802
"""
759-
if isinstance(other, Percentage):
760-
return super().__mul__(other)
761-
if isinstance(other, Voltage):
762-
return Power.from_watts(self._base_value * other._base_value)
763-
764-
return NotImplemented
803+
match other:
804+
case float() | Percentage():
805+
return super().__mul__(other)
806+
case Voltage():
807+
return Power._new(self._base_value * other._base_value)
808+
case _:
809+
return NotImplemented
765810

766811

767812
class Voltage(
@@ -841,18 +886,29 @@ def as_kilovolts(self) -> float:
841886
return self._base_value / 1e3
842887

843888
@overload # type: ignore
844-
def __mul__(self, other: Percentage) -> Self:
889+
def __mul__(self, scalar: float, /) -> Self:
890+
"""Scale this voltage by a scalar.
891+
892+
Args:
893+
scalar: The scaler by which to scale this voltage.
894+
895+
Returns:
896+
The scaled quantity.
897+
"""
898+
899+
@overload
900+
def __mul__(self, percent: Percentage, /) -> Self:
845901
"""Scale this voltage by a percentage.
846902
847903
Args:
848-
other: The percentage by which to scale this voltage.
904+
percent: The percentage by which to scale this voltage.
849905
850906
Returns:
851907
The scaled voltage.
852908
"""
853909

854910
@overload
855-
def __mul__(self, other: Current) -> Power:
911+
def __mul__(self, other: Current, /) -> Power:
856912
"""Multiply the voltage by the current to get the power.
857913
858914
Args:
@@ -862,21 +918,22 @@ def __mul__(self, other: Current) -> Power:
862918
The calculated power.
863919
"""
864920

865-
def __mul__(self, other: Percentage | Current) -> Self | Power:
921+
def __mul__(self, other: float | Percentage | Current, /) -> Self | Power:
866922
"""Return a voltage or power from multiplying this voltage by the given value.
867923
868924
Args:
869-
other: The percentage or current to multiply by.
925+
other: The scalar, percentage or current to multiply by.
870926
871927
Returns:
872928
The calculated voltage or power.
873929
"""
874-
if isinstance(other, Percentage):
875-
return super().__mul__(other)
876-
if isinstance(other, Current):
877-
return Power.from_watts(self._base_value * other._base_value)
878-
879-
return NotImplemented
930+
match other:
931+
case float() | Percentage():
932+
return super().__mul__(other)
933+
case Current():
934+
return Power._new(self._base_value * other._base_value)
935+
case _:
936+
return NotImplemented
880937

881938

882939
class Energy(
@@ -959,6 +1016,23 @@ def as_megawatt_hours(self) -> float:
9591016
"""
9601017
return self._base_value / 1e6
9611018

1019+
def __mul__(self, other: float | Percentage) -> Self:
1020+
"""Scale this energy by a percentage.
1021+
1022+
Args:
1023+
other: The percentage by which to scale this energy.
1024+
1025+
Returns:
1026+
The scaled energy.
1027+
"""
1028+
match other:
1029+
case float():
1030+
return self._new(self._base_value * other)
1031+
case Percentage():
1032+
return self._new(self._base_value * other.as_fraction())
1033+
case _:
1034+
return NotImplemented
1035+
9621036
@overload
9631037
def __truediv__(self, other: timedelta) -> Power:
9641038
"""Return a power from dividing this energy by the given duration.

tests/timeseries/test_quantities.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -592,12 +592,6 @@ def test_invalid_multiplications() -> None:
592592
with pytest.raises(TypeError):
593593
energy *= quantity # type: ignore
594594

595-
for quantity in [power, voltage, current, energy, Percentage.from_percent(50)]:
596-
with pytest.raises(TypeError):
597-
_ = quantity * 200.0 # type: ignore
598-
with pytest.raises(TypeError):
599-
quantity *= 200.0 # type: ignore
600-
601595

602596
# We can't use _QUANTITY_TYPES here, because it will break the tests, as hypothesis
603597
# will generate more values, some of which are unsupported by the quantities. See the

0 commit comments

Comments
 (0)