-
Couldn't load subscription status.
- Fork 20
Allow multiplying Quantity by float
#875
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
Changes from 5 commits
f78393f
7c754a6
a4e879a
7eaee36
5118985
9d45a46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -334,7 +334,19 @@ def __sub__(self, other: Self) -> Self: | |
| difference._base_value = self._base_value - other._base_value | ||
| return difference | ||
|
|
||
| def __mul__(self, percent: Percentage) -> Self: | ||
| @overload | ||
| def __mul__(self, scalar: float, /) -> Self: | ||
| """Scale this quantity by a scalar. | ||
|
|
||
| Args: | ||
| scalar: The scaler by which to scale this quantity. | ||
|
|
||
| Returns: | ||
| The scaled quantity. | ||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, percent: Percentage, /) -> Self: | ||
| """Scale this quantity by a percentage. | ||
|
|
||
| Args: | ||
|
|
@@ -343,12 +355,23 @@ def __mul__(self, percent: Percentage) -> Self: | |
| Returns: | ||
| The scaled quantity. | ||
| """ | ||
| if not isinstance(percent, Percentage): | ||
| return NotImplemented | ||
|
|
||
| product = type(self).__new__(type(self)) | ||
| product._base_value = self._base_value * percent.as_fraction() | ||
| return product | ||
| def __mul__(self, value: float | Percentage, /) -> Self: | ||
| """Scale this quantity by a scalar or percentage. | ||
|
|
||
| Args: | ||
| value: The scalar or percentage by which to scale this quantity. | ||
|
|
||
| Returns: | ||
| The scaled quantity. | ||
| """ | ||
| match value: | ||
| case float(): | ||
| return type(self)._new(self._base_value * value) | ||
| case Percentage(): | ||
| return type(self)._new(self._base_value * value.as_fraction()) | ||
| case _: | ||
| return NotImplemented | ||
|
|
||
| def __gt__(self, other: Self) -> bool: | ||
| """Return whether this quantity is greater than another. | ||
|
|
@@ -583,19 +606,38 @@ def as_megawatts(self) -> float: | |
| """ | ||
| return self._base_value / 1e6 | ||
|
|
||
| @overload # type: ignore | ||
| def __mul__(self, other: Percentage) -> Self: | ||
| # We need the ignore here because otherwise mypy will give this error: | ||
| # > Overloaded operator methods can't have wider argument types in overrides | ||
| # The problem seems to be when the other type implements an **incompatible** | ||
| # __rmul__ method, which is not the case here, so we should be safe. | ||
| # Please see this example: | ||
| # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 | ||
| # And a discussion in a mypy issue here: | ||
| # https://github.com/python/mypy/issues/4985#issuecomment-389692396 | ||
| @overload # type: ignore[override] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to add some more context, when Marenz was adding these, we had no idea what the issue was, because without these, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I didn't get a crash (thankfully!) but I still had to do my research, because the error message is not very clear why is there a problem with doing that. |
||
| def __mul__(self, scalar: float, /) -> Self: | ||
| """Scale this power by a scalar. | ||
|
|
||
| Args: | ||
| scalar: The scaler by which to scale this power. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Returns: | ||
| The scaled quantity. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, percent: Percentage, /) -> Self: | ||
| """Scale this power by a percentage. | ||
|
|
||
| Args: | ||
| other: The percentage by which to scale this power. | ||
| percent: The percentage by which to scale this power. | ||
|
|
||
| Returns: | ||
| The scaled power. | ||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, other: timedelta) -> Energy: | ||
| def __mul__(self, other: timedelta, /) -> Energy: | ||
| """Return an energy from multiplying this power by the given duration. | ||
|
|
||
| Args: | ||
|
|
@@ -605,23 +647,22 @@ def __mul__(self, other: timedelta) -> Energy: | |
| The calculated energy. | ||
| """ | ||
|
|
||
| def __mul__(self, other: Percentage | timedelta) -> Self | Energy: | ||
| def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: | ||
| """Return a power or energy from multiplying this power by the given value. | ||
|
|
||
| Args: | ||
| other: The percentage or duration to multiply by. | ||
| other: The scalar, percentage or duration to multiply by. | ||
|
|
||
| Returns: | ||
| A power or energy. | ||
| """ | ||
| if isinstance(other, Percentage): | ||
| return super().__mul__(other) | ||
| if isinstance(other, timedelta): | ||
| return Energy.from_watt_hours( | ||
| self._base_value * other.total_seconds() / 3600.0 | ||
| ) | ||
|
|
||
| return NotImplemented | ||
| match other: | ||
| case float() | Percentage(): | ||
| return super().__mul__(other) | ||
| case timedelta(): | ||
| return Energy._new(self._base_value * other.total_seconds() / 3600.0) | ||
| case _: | ||
| return NotImplemented | ||
|
|
||
| @overload | ||
| def __truediv__(self, other: Current) -> Voltage: | ||
|
|
@@ -725,19 +766,31 @@ def as_milliamperes(self) -> float: | |
| """ | ||
| return self._base_value * 1e3 | ||
|
|
||
| @overload # type: ignore | ||
| def __mul__(self, other: Percentage) -> Self: | ||
| # See comment for Power.__mul__ for why we need the ignore here. | ||
| @overload # type: ignore[override] | ||
| def __mul__(self, scalar: float, /) -> Self: | ||
| """Scale this current by a scalar. | ||
|
|
||
| Args: | ||
| scalar: The scaler by which to scale this current. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Returns: | ||
| The scaled quantity. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, percent: Percentage, /) -> Self: | ||
| """Scale this current by a percentage. | ||
|
|
||
| Args: | ||
| other: The percentage by which to scale this current. | ||
| percent: The percentage by which to scale this current. | ||
|
|
||
| Returns: | ||
| The scaled current. | ||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, other: Voltage) -> Power: | ||
| def __mul__(self, other: Voltage, /) -> Power: | ||
| """Multiply the current by a voltage to get a power. | ||
|
|
||
| Args: | ||
|
|
@@ -747,21 +800,22 @@ def __mul__(self, other: Voltage) -> Power: | |
| The calculated power. | ||
| """ | ||
|
|
||
| def __mul__(self, other: Percentage | Voltage) -> Self | Power: | ||
| def __mul__(self, other: float | Percentage | Voltage, /) -> Self | Power: | ||
| """Return a current or power from multiplying this current by the given value. | ||
|
|
||
| Args: | ||
| other: The percentage or voltage to multiply by. | ||
| other: The scalar, percentage or voltage to multiply by. | ||
|
|
||
| Returns: | ||
| A current or power. | ||
| """ | ||
| if isinstance(other, Percentage): | ||
| return super().__mul__(other) | ||
| if isinstance(other, Voltage): | ||
| return Power.from_watts(self._base_value * other._base_value) | ||
|
|
||
| return NotImplemented | ||
| match other: | ||
| case float() | Percentage(): | ||
| return super().__mul__(other) | ||
| case Voltage(): | ||
| return Power._new(self._base_value * other._base_value) | ||
| case _: | ||
| return NotImplemented | ||
|
|
||
|
|
||
| class Voltage( | ||
|
|
@@ -840,19 +894,31 @@ def as_kilovolts(self) -> float: | |
| """ | ||
| return self._base_value / 1e3 | ||
|
|
||
| @overload # type: ignore | ||
| def __mul__(self, other: Percentage) -> Self: | ||
| # See comment for Power.__mul__ for why we need the ignore here. | ||
| @overload # type: ignore[override] | ||
| def __mul__(self, scalar: float, /) -> Self: | ||
| """Scale this voltage by a scalar. | ||
|
|
||
| Args: | ||
| scalar: The scaler by which to scale this voltage. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Returns: | ||
| The scaled quantity. | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, percent: Percentage, /) -> Self: | ||
| """Scale this voltage by a percentage. | ||
|
|
||
| Args: | ||
| other: The percentage by which to scale this voltage. | ||
| percent: The percentage by which to scale this voltage. | ||
|
|
||
| Returns: | ||
| The scaled voltage. | ||
| """ | ||
|
|
||
| @overload | ||
| def __mul__(self, other: Current) -> Power: | ||
| def __mul__(self, other: Current, /) -> Power: | ||
| """Multiply the voltage by the current to get the power. | ||
|
|
||
| Args: | ||
|
|
@@ -862,21 +928,22 @@ def __mul__(self, other: Current) -> Power: | |
| The calculated power. | ||
| """ | ||
|
|
||
| def __mul__(self, other: Percentage | Current) -> Self | Power: | ||
| def __mul__(self, other: float | Percentage | Current, /) -> Self | Power: | ||
| """Return a voltage or power from multiplying this voltage by the given value. | ||
|
|
||
| Args: | ||
| other: The percentage or current to multiply by. | ||
| other: The scalar, percentage or current to multiply by. | ||
|
|
||
| Returns: | ||
| The calculated voltage or power. | ||
| """ | ||
| if isinstance(other, Percentage): | ||
| return super().__mul__(other) | ||
| if isinstance(other, Current): | ||
| return Power.from_watts(self._base_value * other._base_value) | ||
|
|
||
| return NotImplemented | ||
| match other: | ||
| case float() | Percentage(): | ||
| return super().__mul__(other) | ||
| case Current(): | ||
| return Power._new(self._base_value * other._base_value) | ||
| case _: | ||
| return NotImplemented | ||
|
|
||
|
|
||
| class Energy( | ||
|
|
@@ -959,6 +1026,23 @@ def as_megawatt_hours(self) -> float: | |
| """ | ||
| return self._base_value / 1e6 | ||
|
|
||
| def __mul__(self, other: float | Percentage) -> Self: | ||
| """Scale this energy by a percentage. | ||
|
|
||
| Args: | ||
| other: The percentage by which to scale this energy. | ||
|
|
||
| Returns: | ||
| The scaled energy. | ||
| """ | ||
| match other: | ||
| case float(): | ||
| return self._new(self._base_value * other) | ||
| case Percentage(): | ||
| return self._new(self._base_value * other.as_fraction()) | ||
| case _: | ||
| return NotImplemented | ||
|
|
||
| @overload | ||
| def __truediv__(self, other: timedelta) -> Power: | ||
| """Return a power from dividing this energy by the given duration. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -544,31 +544,69 @@ def test_abs() -> None: | |
| assert abs(-pct) == Percentage.from_fraction(30) | ||
|
|
||
|
|
||
| def test_quantity_multiplied_with_precentage() -> None: | ||
| @pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) | ||
| # Use a small amount to avoid long running tests, we have too many combinations | ||
| @hypothesis.settings(max_examples=10) | ||
| @hypothesis.given( | ||
| quantity_value=st.floats( | ||
| allow_infinity=False, | ||
| allow_nan=False, | ||
| allow_subnormal=False, | ||
| # We need to set this because otherwise constructors with big exponents will | ||
| # cause the value to be too big for the float type, and the test will fail. | ||
| max_value=1e298, | ||
| min_value=-1e298, | ||
| ), | ||
| percent=st.floats(allow_infinity=False, allow_nan=False, allow_subnormal=False), | ||
| ) | ||
| def test_quantity_multiplied_with_precentage( | ||
| quantity_ctor: type[Quantity], quantity_value: float, percent: float | ||
| ) -> None: | ||
| """Test the multiplication of all quantities with percentage.""" | ||
| percentage = Percentage.from_percent(50) | ||
| power = Power.from_watts(1000.0) | ||
| voltage = Voltage.from_volts(230.0) | ||
| current = Current.from_amperes(2) | ||
| energy = Energy.from_kilowatt_hours(12) | ||
| percentage_ = Percentage.from_percent(50) | ||
|
|
||
| assert power * percentage == Power.from_watts(500.0) | ||
| assert voltage * percentage == Voltage.from_volts(115.0) | ||
| assert current * percentage == Current.from_amperes(1) | ||
| assert energy * percentage == Energy.from_kilowatt_hours(6) | ||
| assert percentage_ * percentage == Percentage.from_percent(25) | ||
|
|
||
| power *= percentage | ||
| assert power == Power.from_watts(500.0) | ||
| voltage *= percentage | ||
| assert voltage == Voltage.from_volts(115.0) | ||
| current *= percentage | ||
| assert current == Current.from_amperes(1) | ||
| energy *= percentage | ||
| assert energy == Energy.from_kilowatt_hours(6) | ||
| percentage_ *= percentage | ||
| assert percentage_ == Percentage.from_percent(25) | ||
| percentage = Percentage.from_percent(percent) | ||
| quantity = quantity_ctor(quantity_value) | ||
| expected_value = quantity.base_value * (percent / 100.0) | ||
| print(f"{quantity=}, {percentage=}, {expected_value=}") | ||
|
|
||
| product = quantity * percentage | ||
| print(f"{product=}") | ||
| assert product.base_value == expected_value | ||
|
|
||
| quantity *= percentage | ||
| print(f"*{quantity=}") | ||
| assert quantity.base_value == expected_value | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to retain the print statements? don't they pollute the test output when trying to debug something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are useful when something fails. I guess if you are focusing on some other test you can always run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well there's always: assert quantity.base_value == expected_value, f"{quantity=} * {percentage=} != {expected_value}! 🙀 " |
||
|
|
||
|
|
||
| @pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) | ||
| # Use a small amount to avoid long running tests, we have too many combinations | ||
| @hypothesis.settings(max_examples=10) | ||
| @hypothesis.given( | ||
| quantity_value=st.floats( | ||
| allow_infinity=False, | ||
| allow_nan=False, | ||
| allow_subnormal=False, | ||
| # We need to set this because otherwise constructors with big exponents will | ||
| # cause the value to be too big for the float type, and the test will fail. | ||
| max_value=1e298, | ||
| min_value=-1e298, | ||
| ), | ||
| scalar=st.floats(allow_infinity=False, allow_nan=False, allow_subnormal=False), | ||
| ) | ||
| def test_quantity_multiplied_with_float( | ||
| quantity_ctor: type[Quantity], quantity_value: float, scalar: float | ||
| ) -> None: | ||
| """Test the multiplication of all quantities with percentage.""" | ||
llucax marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| quantity = quantity_ctor(quantity_value) | ||
| expected_value = quantity.base_value * scalar | ||
| print(f"{quantity=}, {expected_value=}") | ||
|
|
||
| product = quantity * scalar | ||
| print(f"{product=}") | ||
| assert product.base_value == expected_value | ||
|
|
||
| quantity *= scalar | ||
| print(f"*{quantity=}") | ||
| assert quantity.base_value == expected_value | ||
|
|
||
|
|
||
| def test_invalid_multiplications() -> None: | ||
|
|
@@ -602,12 +640,6 @@ def test_invalid_multiplications() -> None: | |
| with pytest.raises(TypeError): | ||
| energy *= quantity # type: ignore | ||
|
|
||
| for quantity in [power, voltage, current, energy, Percentage.from_percent(50)]: | ||
| with pytest.raises(TypeError): | ||
| _ = quantity * 200.0 # type: ignore | ||
| with pytest.raises(TypeError): | ||
| quantity *= 200.0 # type: ignore | ||
|
|
||
|
|
||
| # We can't use _QUANTITY_TYPES here, because it will break the tests, as hypothesis | ||
| # will generate more values, some of which are unsupported by the quantities. See the | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.