diff --git a/mkdocs.yml b/mkdocs.yml index 163d1fa..de77690 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,8 +112,10 @@ plugins: show_source: true show_symbol_type_toc: true signature_crossrefs: true + relative_crossrefs: true inventories: - https://docs.python.org/3/objects.inv + - https://marshmallow.readthedocs.io/en/stable/objects.inv - https://typing-extensions.readthedocs.io/en/stable/objects.inv # Note this plugin must be loaded after mkdocstrings to be able to use macros # inside docstrings. diff --git a/pyproject.toml b/pyproject.toml index 421ed2f..94c6e99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,6 +127,9 @@ check-yield-types = false arg-type-hints-in-docstring = false arg-type-hints-in-signature = true allow-init-docstring = true +check-class-attributes = true +check-style-mismatch = true +require-inline-class-var-docs = true [tool.pylint.similarities] ignore-comments = ['yes'] diff --git a/src/frequenz/quantities/__init__.py b/src/frequenz/quantities/__init__.py index 1afd324..2a9ad4f 100644 --- a/src/frequenz/quantities/__init__.py +++ b/src/frequenz/quantities/__init__.py @@ -3,7 +3,7 @@ """Types for holding quantities with units. -This library provide types for holding quantities with units. The main goal is to avoid +This library provides types for holding quantities with units. The main goal is to avoid mistakes while working with different types of quantities, for example avoiding adding a length to a time. @@ -13,7 +13,7 @@ Quantities store the value in a base unit, and then provide methods to get that quantity as a particular unit. They can only be constructed using special constructors with the form `Quantity.from_`, for example -[`Power.from_watts(10.0)`][frequenz.quantities.Power.from_watts]. +[`Power.from_watts(10.0)`][.Power.from_watts]. Internally quantities store values as `float`s, so regular [float issues and limitations apply](https://docs.python.org/3/tutorial/floatingpoint.html), although some of them are @@ -24,19 +24,17 @@ This library provides the following types: -- [ApparentPower][frequenz.quantities.ApparentPower]: A quantity representing apparent - power. -- [Current][frequenz.quantities.Current]: A quantity representing an electric current. -- [Energy][frequenz.quantities.Energy]: A quantity representing energy. -- [Frequency][frequenz.quantities.Frequency]: A quantity representing frequency. -- [Percentage][frequenz.quantities.Percentage]: A quantity representing a percentage. -- [Power][frequenz.quantities.Power]: A quantity representing power. -- [ReactivePower][frequenz.quantities.ReactivePower]: A quantity representing reactive - power. -- [Temperature][frequenz.quantities.Temperature]: A quantity representing temperature. -- [Voltage][frequenz.quantities.Voltage]: A quantity representing electric voltage. - -There is also the unitless [Quantity][frequenz.quantities.Quantity] class. All +- [`ApparentPower`][.ApparentPower]: A quantity representing apparent power. +- [`Current`][.Current]: A quantity representing an electric current. +- [`Energy`][.Energy]: A quantity representing energy. +- [`Frequency`][.Frequency]: A quantity representing frequency. +- [`Percentage`][.Percentage]: A quantity representing a percentage. +- [`Power`][.Power]: A quantity representing power. +- [`ReactivePower`][.ReactivePower]: A quantity representing reactive power. +- [`Temperature`][.Temperature]: A quantity representing temperature. +- [`Voltage`][.Voltage]: A quantity representing electric voltage. + +There is also the unitless [`Quantity`][.Quantity] class. All quantities are subclasses of this class and it can be used as a base to create new quantities. Using the `Quantity` class directly is discouraged, as it doesn't provide any unit conversion methods. @@ -80,7 +78,7 @@ ``` This library also provides an [**experimental** module with marshmallow fields and -a base schema][frequenz.quantities.experimental.marshmallow] to serialize and +a base schema][.experimental.marshmallow] to serialize and deserialize quantities using the marshmallow library. To use it, you need to make sure to install this package with the `marshmallow` optional dependencies (e.g. `pip install frequenz-quantities[marshmallow]`). diff --git a/src/frequenz/quantities/_apparent_power.py b/src/frequenz/quantities/_apparent_power.py index e5df26a..d2fa730 100644 --- a/src/frequenz/quantities/_apparent_power.py +++ b/src/frequenz/quantities/_apparent_power.py @@ -25,7 +25,7 @@ class ApparentPower( 6: "MVA", }, ): - """A apparent power quantity. + """An apparent power quantity. Objects of this type are wrappers around `float` values and are immutable. @@ -118,34 +118,34 @@ def as_mega_volt_amperes(self) -> float: @overload def __mul__(self, scalar: float, /) -> Self: - """Scale this power by a scalar. + """Scale this apparent power by a scalar. Args: - scalar: The scalar by which to scale this power. + scalar: The scalar by which to scale this apparent power. Returns: - The scaled power. + The scaled apparent power. """ @overload def __mul__(self, percent: Percentage, /) -> Self: - """Scale this power by a percentage. + """Scale this apparent power by a percentage. Args: - percent: The percentage by which to scale this power. + percent: The percentage by which to scale this apparent power. Returns: - The scaled power. + The scaled apparent power. """ def __mul__(self, other: float | Percentage, /) -> Self: - """Return a power or energy from multiplying this power by the given value. + """Scale this apparent power by a scalar or percentage. Args: - other: The scalar, percentage or duration to multiply by. + other: The scalar or percentage by which to scale this apparent power. Returns: - A power or energy. + The scaled apparent power. """ from ._percentage import Percentage # pylint: disable=import-outside-toplevel @@ -165,58 +165,58 @@ def __mul__(self, other: float | Percentage, /) -> Self: # https://github.com/python/mypy/issues/4985#issuecomment-389692396 @overload # type: ignore[override] def __truediv__(self, other: float, /) -> Self: - """Divide this power by a scalar. + """Divide this apparent power by a scalar. Args: - other: The scalar to divide this power by. + other: The scalar to divide this apparent power by. Returns: - The divided power. + The divided apparent power. """ @overload def __truediv__(self, other: Self, /) -> float: - """Return the ratio of this power to another. + """Return the ratio of this apparent power to another. Args: - other: The other power. + other: The other apparent power. Returns: - The ratio of this power to another. + The ratio of this apparent power to another. """ @overload def __truediv__(self, current: Current, /) -> Voltage: - """Return a voltage from dividing this power by the given current. + """Return a voltage from dividing this apparent power by the given current. Args: current: The current to divide by. Returns: - A voltage from dividing this power by the a current. + A voltage from dividing this apparent power by a current. """ @overload def __truediv__(self, voltage: Voltage, /) -> Current: - """Return a current from dividing this power by the given voltage. + """Return a current from dividing this apparent power by the given voltage. Args: voltage: The voltage to divide by. Returns: - A current from dividing this power by a voltage. + A current from dividing this apparent power by a voltage. """ def __truediv__( self, other: float | Self | Current | Voltage, / ) -> Self | float | Voltage | Current: - """Return a current or voltage from dividing this power by the given value. + """Return a scaled apparent power, ratio, voltage, or current. Args: - other: The scalar, power, current or voltage to divide by. + other: The scalar, apparent power, current or voltage to divide by. Returns: - A current or voltage from dividing this power by the given value. + A scaled apparent power, a ratio, a voltage, or a current. """ from ._current import Current # pylint: disable=import-outside-toplevel from ._voltage import Voltage # pylint: disable=import-outside-toplevel diff --git a/src/frequenz/quantities/_energy.py b/src/frequenz/quantities/_energy.py index 57d03f3..6c416e7 100644 --- a/src/frequenz/quantities/_energy.py +++ b/src/frequenz/quantities/_energy.py @@ -96,10 +96,10 @@ def as_megawatt_hours(self) -> float: return self._base_value / 1e6 def __mul__(self, other: float | Percentage) -> Self: - """Scale this energy by a percentage. + """Scale this energy by a scalar or percentage. Args: - other: The percentage by which to scale this energy. + other: The scalar or percentage by which to scale this energy. Returns: The scaled energy. @@ -162,13 +162,13 @@ def __truediv__(self, power: Power, /) -> timedelta: def __truediv__( self, other: float | Self | timedelta | Power, / ) -> Self | float | Power | timedelta: - """Return a power or duration from dividing this energy by the given value. + """Return a scaled energy, ratio, power, or duration. Args: other: The scalar, energy, power or duration to divide by. Returns: - A power or duration from dividing this energy by the given value. + A scaled energy, a ratio, a power, or a duration. """ from ._power import Power # pylint: disable=import-outside-toplevel diff --git a/src/frequenz/quantities/_power.py b/src/frequenz/quantities/_power.py index 2feb2da..94b2fb1 100644 --- a/src/frequenz/quantities/_power.py +++ b/src/frequenz/quantities/_power.py @@ -202,7 +202,7 @@ def __truediv__(self, current: Current, /) -> Voltage: current: The current to divide by. Returns: - A voltage from dividing this power by the a current. + A voltage from dividing this power by a current. """ @overload diff --git a/src/frequenz/quantities/_quantity.py b/src/frequenz/quantities/_quantity.py index db89c4a..3982a21 100644 --- a/src/frequenz/quantities/_quantity.py +++ b/src/frequenz/quantities/_quantity.py @@ -104,7 +104,6 @@ def from_string(cls, string: str) -> Self: Raises: ValueError: If the string does not match the expected format. - """ split_string = string.split(" ") @@ -130,11 +129,7 @@ def from_string(cls, string: str) -> Self: @property def base_value(self) -> float: - """Return the value of this quantity in the base unit. - - Returns: - The value of this quantity in the base unit. - """ + """The value of this quantity in the base unit.""" return self._base_value def __round__(self, ndigits: int | None = None) -> Self: @@ -169,13 +164,7 @@ def __mod__(self, other: Self) -> Self: @property def base_unit(self) -> str | None: - """Return the base unit of this quantity. - - None if this quantity has no unit. - - Returns: - The base unit of this quantity. - """ + """The base unit of this quantity, or `None` if this quantity has no unit.""" if not self._exponent_unit_map: return None return self._exponent_unit_map[0] @@ -396,7 +385,7 @@ def __truediv__(self, other: float, /) -> Self: """Divide this quantity by a scalar. Args: - other: The scalar or percentage to divide this quantity by. + other: The scalar to divide this quantity by. Returns: The divided quantity. @@ -527,8 +516,8 @@ def __call__(cls, *_args: Any, **_kwargs: Any) -> NoReturn: """Raise a TypeError when the default constructor is called. Args: - *_args: ignored positional arguments. - **_kwargs: ignored keyword arguments. + *_args: Ignored positional arguments. + **_kwargs: Ignored keyword arguments. Raises: TypeError: Always. diff --git a/src/frequenz/quantities/_reactive_power.py b/src/frequenz/quantities/_reactive_power.py index a38acd8..24898ca 100644 --- a/src/frequenz/quantities/_reactive_power.py +++ b/src/frequenz/quantities/_reactive_power.py @@ -118,34 +118,34 @@ def as_mega_volt_amperes_reactive(self) -> float: @overload def __mul__(self, scalar: float, /) -> Self: - """Scale this power by a scalar. + """Scale this reactive power by a scalar. Args: - scalar: The scalar by which to scale this power. + scalar: The scalar by which to scale this reactive power. Returns: - The scaled power. + The scaled reactive power. """ @overload def __mul__(self, percent: Percentage, /) -> Self: - """Scale this power by a percentage. + """Scale this reactive power by a percentage. Args: - percent: The percentage by which to scale this power. + percent: The percentage by which to scale this reactive power. Returns: - The scaled power. + The scaled reactive power. """ def __mul__(self, other: float | Percentage, /) -> Self: - """Return a power or energy from multiplying this power by the given value. + """Scale this reactive power by a scalar or percentage. Args: - other: The scalar, percentage or duration to multiply by. + other: The scalar or percentage by which to scale this reactive power. Returns: - A power or energy. + The scaled reactive power. """ from ._percentage import Percentage # pylint: disable=import-outside-toplevel @@ -165,58 +165,58 @@ def __mul__(self, other: float | Percentage, /) -> Self: # https://github.com/python/mypy/issues/4985#issuecomment-389692396 @overload # type: ignore[override] def __truediv__(self, other: float, /) -> Self: - """Divide this power by a scalar. + """Divide this reactive power by a scalar. Args: - other: The scalar to divide this power by. + other: The scalar to divide this reactive power by. Returns: - The divided power. + The divided reactive power. """ @overload def __truediv__(self, other: Self, /) -> float: - """Return the ratio of this power to another. + """Return the ratio of this reactive power to another. Args: - other: The other power. + other: The other reactive power. Returns: - The ratio of this power to another. + The ratio of this reactive power to another. """ @overload def __truediv__(self, current: Current, /) -> Voltage: - """Return a voltage from dividing this power by the given current. + """Return a voltage from dividing this reactive power by the given current. Args: current: The current to divide by. Returns: - A voltage from dividing this power by the a current. + A voltage from dividing this reactive power by a current. """ @overload def __truediv__(self, voltage: Voltage, /) -> Current: - """Return a current from dividing this power by the given voltage. + """Return a current from dividing this reactive power by the given voltage. Args: voltage: The voltage to divide by. Returns: - A current from dividing this power by a voltage. + A current from dividing this reactive power by a voltage. """ def __truediv__( self, other: float | Self | Current | Voltage, / ) -> Self | float | Voltage | Current: - """Return a current or voltage from dividing this power by the given value. + """Return a scaled reactive power, ratio, voltage, or current. Args: - other: The scalar, power, current or voltage to divide by. + other: The scalar, reactive power, current or voltage to divide by. Returns: - A current or voltage from dividing this power by the given value. + A scaled reactive power, a ratio, a voltage, or a current. """ from ._current import Current # pylint: disable=import-outside-toplevel from ._voltage import Voltage # pylint: disable=import-outside-toplevel diff --git a/src/frequenz/quantities/experimental/marshmallow.py b/src/frequenz/quantities/experimental/marshmallow.py index 9bb28e7..9e4bf21 100644 --- a/src/frequenz/quantities/experimental/marshmallow.py +++ b/src/frequenz/quantities/experimental/marshmallow.py @@ -4,7 +4,7 @@ """Custom marshmallow fields and schema. This module provides custom marshmallow fields for quantities and -a [QuantitySchema][frequenz.quantities.experimental.marshmallow.QuantitySchema] class to +a [`QuantitySchema`][.QuantitySchema] class to be used as base schema for dataclasses containing quantities. Danger: @@ -34,42 +34,47 @@ serialize_as_string_default: ContextVar[bool] = ContextVar( "serialize_as_string_default", default=False ) -"""Context variable to control the default serialization format for quantities. +"""The context variable controlling the default serialization format for quantities. -If True, quantities are serialized as strings with units. -If False, quantities are serialized as floats. - -This can be overridden on a per-field basis using the `serialize_as_string` -metadata attribute. +If `True`, quantities are serialized as strings with units; if `False`, as floats. +This can be overridden on a per-field basis using the `serialize_as_string` metadata +attribute. """ class _QuantityField(Field[Quantity]): - """Custom field for Quantity objects supporting per-field serialization configuration. + """A custom field for [`Quantity`][....Quantity] objects. + + Supports per-field serialization configuration. - This class handles serialization and deserialization of ALL Quantity - subclasses. - The specific Quantity subclass is determined by the field_type attribute. + This class handles serialization and deserialization of ALL + [`Quantity`][....Quantity] subclasses. + The specific [`Quantity`][....Quantity] subclass is determined by the + [`.field_type`][.field_type] attribute. * Deserialization auto-detects the type of deserialization (float or string) based on the input type. * Serialization uses either the schema's default or the per-field configuration found in the metadata. - We need distinct QuantityField subclasses for each Quantity subclass, so - they can be used in the TYPE_MAPPING in the `QuantitySchema`. - Which means this class is not intended to be used directly. + We need distinct `_QuantityField` subclasses for each + [`Quantity`][....Quantity] subclass, so + they can be used in the [`TYPE_MAPPING`][..QuantitySchema.TYPE_MAPPING] in + [`QuantitySchema`][..QuantitySchema]. + This class is not intended to be used directly. - Instead, we use the specific QuantityField subclasses for each Quantity. - Each field subclass simply sets the field_type attribute to the corresponding - Quantity subclass. + Instead, we use the specific `_QuantityField` subclasses for each + [`Quantity`][....Quantity]. + Each field subclass simply sets the [`.field_type`][.field_type] + attribute to the corresponding [`Quantity`][....Quantity] subclass. - Those subclasses are generated and stored in the QUANTITY_FIELD_CLASSES - mapping and are used for the TYPE_MAPPING in the `QuantitySchema`. + Those subclasses are stored in [`QUANTITY_FIELD_CLASSES`][..QUANTITY_FIELD_CLASSES] + and are used for the [`TYPE_MAPPING`][..QuantitySchema.TYPE_MAPPING] in + [`QuantitySchema`][..QuantitySchema]. """ field_type: Type[Quantity] | None = None - """The specific Quantity subclass.""" + """The specific [`Quantity`][.....Quantity] subclass.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the field.""" @@ -79,7 +84,24 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def _serialize( self, value: Quantity | None, attr: str | None, obj: Any, **kwargs: Any ) -> Any: - """Serialize the Quantity object based on per-field configuration.""" + """Serialize a [`Quantity`][.....Quantity] based on per-field configuration. + + Args: + value: The quantity to serialize, or `None`. + attr: The attribute name being serialized. + obj: The object the value was taken from. + **kwargs: Additional keyword arguments passed to the parent field. + + Returns: + The string representation with unit if serializing as string, or + the raw base float value otherwise. `None` if `value` is `None`. + + Raises: + TypeError: If [`..field_type`][..field_type] is not set to a + [`Quantity`][.....Quantity] subclass, or if + `value` is not a [`Quantity`][.....Quantity] + instance. + """ if self.field_type is None or not issubclass(self.field_type, Quantity): raise TypeError( "field_type must be set to a Quantity subclass in the subclass." @@ -110,7 +132,23 @@ def _serialize( def _deserialize( self, value: Any, attr: str | None, data: Any, **kwargs: Any ) -> Quantity: - """Deserialize the Quantity object from float or string.""" + """Deserialize a [`Quantity`][.....Quantity] from a float, int, or string. + + Args: + value: The raw value to deserialize (float, int, or string). + attr: The attribute name being deserialized. + data: The raw input data (the full object). + **kwargs: Additional keyword arguments passed to the parent field. + + Returns: + The deserialized quantity instance. + + Raises: + TypeError: If [`..field_type`][..field_type] is not set to a + [`Quantity`][.....Quantity] subclass. + ValidationError: If the input type is invalid or parsing fails + (see [`marshmallow.ValidationError`][marshmallow.ValidationError]). + """ if self.field_type is None or not issubclass(self.field_type, Quantity): raise TypeError( "field_type must be set to a Quantity subclass in the subclass." @@ -148,55 +186,55 @@ def _deserialize( class ApparentPowerField(_QuantityField): - """Custom field for ApparentPower objects.""" + """A custom field for [`ApparentPower`][....ApparentPower] objects.""" field_type = ApparentPower class CurrentField(_QuantityField): - """Custom field for Current objects.""" + """A custom field for [`Current`][....Current] objects.""" field_type = Current class EnergyField(_QuantityField): - """Custom field for Energy objects.""" + """A custom field for [`Energy`][....Energy] objects.""" field_type = Energy class FrequencyField(_QuantityField): - """Custom field for Frequency objects.""" + """A custom field for [`Frequency`][....Frequency] objects.""" field_type = Frequency class PercentageField(_QuantityField): - """Custom field for Percentage objects.""" + """A custom field for [`Percentage`][....Percentage] objects.""" field_type = Percentage class PowerField(_QuantityField): - """Custom field for Power objects.""" + """A custom field for [`Power`][....Power] objects.""" field_type = Power class ReactivePowerField(_QuantityField): - """Custom field for ReactivePower objects.""" + """A custom field for [`ReactivePower`][....ReactivePower] objects.""" field_type = ReactivePower class TemperatureField(_QuantityField): - """Custom field for Temperature objects.""" + """A custom field for [`Temperature`][....Temperature] objects.""" field_type = Temperature class VoltageField(_QuantityField): - """Custom field for Voltage objects.""" + """A custom field for [`Voltage`][....Voltage] objects.""" field_type = Voltage @@ -212,21 +250,18 @@ class VoltageField(_QuantityField): Temperature: TemperatureField, Voltage: VoltageField, } -"""Mapping of Quantity subclasses to their corresponding QuantityField subclasses. +"""The mapping from [`Quantity`][....Quantity] subclasses to their corresponding field subclasses. -This mapping is used in the `QuantitySchema` to determine the correct field -class for each Quantity subclass. - -The keys are Quantity subclasses (e.g., Percentage, Energy) and the values are -the corresponding QuantityField subclasses. +This mapping is used in [`QuantitySchema.TYPE_MAPPING`][..QuantitySchema.TYPE_MAPPING] to +determine the correct field class for each [`Quantity`][....Quantity] +subclass. """ class QuantitySchema(Schema): """A schema for quantities. - Example usage: - + Example: ```python from dataclasses import dataclass, field from marshmallow_dataclass import class_schema @@ -292,3 +327,4 @@ class Config: """ TYPE_MAPPING: dict[type, type[Field[Any]]] = QUANTITY_FIELD_CLASSES + """The field class to use for each [`Quantity`][.....Quantity] subclass."""