diff --git a/generate/__init__.py b/generate/__init__.py new file mode 100644 index 0000000..3584e8b --- /dev/null +++ b/generate/__init__.py @@ -0,0 +1 @@ +"""Package used for auto generating the boilerplate in the quantities.""" diff --git a/generate/generate_boilerplate.py b/generate/generate_boilerplate.py new file mode 100644 index 0000000..55080e0 --- /dev/null +++ b/generate/generate_boilerplate.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from pathlib import Path + +TARGET_FILE_PATH = Path(__file__).parent.parent / "quantio" / "quantities.py" + + +def generate_boilerplate(quantities_with_fields: dict[str, dict[str, str]]) -> None: + """Generate the boilerplate parts of the quantity classes.""" + with TARGET_FILE_PATH.open() as target_file: + target_content = target_file.readlines() + + target_content_with_boilerplate = [] + current_class: str | None = None + + current_line_is_part_of_autogenerated_code = False + for line in target_content: + if line.startswith("class "): + current_class = line.split("(")[0][6:] + + if "# --- This part is auto generated. Do not change manually. ---" in line: + current_line_is_part_of_autogenerated_code = True + target_content_with_boilerplate.append(line) + target_content_with_boilerplate.extend( + _generate_init(quantities_with_fields[current_class]) + ) + target_content_with_boilerplate.extend( + _generate_properties(current_class, quantities_with_fields[current_class]) + ) + + if "# --- End of auto generated part. ---" in line: + current_line_is_part_of_autogenerated_code = False + + if current_line_is_part_of_autogenerated_code: + continue + + target_content_with_boilerplate.append(line) + + for line_number, line in enumerate(target_content_with_boilerplate): + if not line.endswith("\n"): + target_content_with_boilerplate[line_number] += "\n" + + with TARGET_FILE_PATH.open("w") as target_file: + target_file.writelines(target_content_with_boilerplate) + + +def _generate_init(units: dict[str, str]) -> list[str]: + code = ["", " " * 4 + "def __init__(", " " * 8 + "self,"] + + for unit in units: + code.append(" " * 8 + f"{unit}: float = 0.0,") + + code.append(" " * 4 + ") -> None:") + code.append(" " * 8 + "self._base_value = 0.0") + + for unit, factor in units.items(): + code.append(" " * 8 + f"self._base_value += {unit} * {factor}") + + code.append("") + return code + + +def _generate_properties(current_class: str, units: dict[str, str]) -> list[str]: + code = [] + + for unit, factor in units.items(): + code.append(" " * 4 + "@property") + code.append(" " * 4 + f"def {unit}(self) -> float:") + code.append(" " * 8 + f'"""The {current_class.lower()} in {unit.replace("_", " ")}."""') + code.append(" " * 8 + f"return self._base_value / {factor}") + code.append("") + + return code + + +if __name__ == "__main__": + quantities_with_fields = { + "Acceleration": { + "meters_per_square_second": "1", + "g_force": "(1 / 9.8)", + }, + "Angle": { + "degrees": "(3.141592653589793 / 180)", + "radians": "1", + }, + "Area": { + "square_miles": "1609.34**2", + "square_kilometers": "10 ** (3 * 2)", + "square_meters": "1", + "square_feet": "0.3048**2", + "square_inches": "0.0254**2", + "square_centimeters": "10 ** (-2 * 2)", + "square_millimeters": "10 ** (-3 * 2)", + "square_micrometers": "10 ** (-6 * 2)", + }, + "Length": { + "miles": "1609.34", + "kilometers": "10**3", + "meters": "1", + "feet": "0.3048", + "inches": "0.0254", + "centimeters": "10**-2", + "millimeters": "10**-3", + "micrometers": "10**-6", + }, + "Mass": { + "tonnes": "10**3", + "kilograms": "1", + "pounds": "(1 / 2.20462)", + "ounces": "(1 / 35.27396)", + "grams": "10**-3", + "milligrams": "10**-6", + "micrograms": "10**-9", + }, + "Velocity": { + "meters_per_second": "1", + "kilometers_per_hour": "(1 / 3.6)", + "miles_per_hour": "(1 / 2.23694)", + }, + "Time": { + "hours": "60 * 60", + "minutes": "60", + "seconds": "1", + "milliseconds": "10**-3", + }, + } + + generate_boilerplate(quantities_with_fields) diff --git a/generate/quantities.py b/generate/quantities.py new file mode 100644 index 0000000..43a9ab5 --- /dev/null +++ b/generate/quantities.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from ._quantity_base import _QuantityBase + + +class Area(_QuantityBase): + """The two-dimensional extent of an object.""" + + # --- This part is auto generated. Do not change manually. --- + + # --- End of auto generated part. --- + + +class Length(_QuantityBase): + """The one-dimensional extent of an object or the distance between two points.""" + + # --- This part is auto generated. Do not change manually. --- + + # --- End of auto generated part. --- + + +class Time(_QuantityBase): + """The duration of an event.""" + + # --- This part is auto generated. Do not change manually. --- + + # --- End of auto generated part. --- diff --git a/pyproject.toml b/pyproject.toml index 36996b5..45c74a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ pytest = "<9.0" line-length = 101 [tool.ruff.lint] -exclude = ["test/*"] +exclude = ["test/*", "generate/*"] select = ["ALL"] ignore = [ "COM812", # conflicts with ruff formatter @@ -32,6 +32,14 @@ ignore = [ "ISC001", # conflicts with ruff formatter + "N802", # interferes with setting constant abstract properties + + "PGH003", # necessary for _QuantityBase.__eq__() + + "PLR0913", # many arguments are necessary for the quantities to enable enough different units + + "RUF012", # does not work with constants + "TCH001", # adds hard to understand compexity without providing a benefit for smaller projects "TCH002", # same as TCH001 "TCH003", # same as TCH001 diff --git a/quantio/__init__.py b/quantio/__init__.py index 5419807..f73c24a 100644 --- a/quantio/__init__.py +++ b/quantio/__init__.py @@ -1,5 +1,6 @@ """The main quantio package.""" -from .quantities import Length +from .exceptions import CanNotAddTypesError, CanNotSubtractTypesError +from .quantities import Length, Time -__all__ = ["Length"] +__all__ = ["Length", "Time", "CanNotAddTypesError", "CanNotSubtractTypesError"] diff --git a/quantio/_quantity_base.py b/quantio/_quantity_base.py new file mode 100644 index 0000000..f201f43 --- /dev/null +++ b/quantio/_quantity_base.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from abc import ABC + +from .exceptions import CanNotAddTypesError, CanNotSubtractTypesError + + +class _QuantityBase(ABC): + """Parent class to all quantities.""" + + _base_value: float + "The base unit of the quantity." + + def __eq__(self, other: object) -> bool: + """Assess if this object is the same as another.""" + if isinstance(other, type(self)): + return self._base_value == other._base_value + + return False + + def __add__(self, other: _QuantityBase) -> _QuantityBase: + """Add two quantities of the same type.""" + if type(self) is not type(other): + raise CanNotAddTypesError(self.__class__.__name__, other.__class__.__name__) + + result = type(self)() + result._base_value = self._base_value + other._base_value + return result + + def __sub__(self, other: _QuantityBase) -> _QuantityBase: + """Subtract two quantities of the same type.""" + if type(self) is not type(other): + raise CanNotSubtractTypesError(self.__class__.__name__, other.__class__.__name__) + + result = type(self)() + result._base_value = self._base_value - other._base_value + return result diff --git a/quantio/exceptions.py b/quantio/exceptions.py new file mode 100644 index 0000000..cae7dd4 --- /dev/null +++ b/quantio/exceptions.py @@ -0,0 +1,12 @@ +class CanNotAddTypesError(TypeError): + """Raised when two uncompatible quantities are added to one another.""" + + def __init__(self, self_type_descriptor: str, other_type_descriptor: str) -> None: + super().__init__(f"Can not add {other_type_descriptor} to {self_type_descriptor}") + + +class CanNotSubtractTypesError(TypeError): + """Raised when two uncompatible quantities are subtracted from one another.""" + + def __init__(self, self_type_descriptor: str, other_type_descriptor: str) -> None: + super().__init__(f"Can not subtract {other_type_descriptor} from {self_type_descriptor}") diff --git a/quantio/quantities.py b/quantio/quantities.py index ed09fd0..916ca88 100644 --- a/quantio/quantities.py +++ b/quantio/quantities.py @@ -1,72 +1,332 @@ from __future__ import annotations -from typing import ClassVar +from ._quantity_base import _QuantityBase -class Length: +class Acceleration(_QuantityBase): + """Rate of change of velocity.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + meters_per_square_second: float = 0.0, + g_force: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += meters_per_square_second * 1 + self._base_value += g_force * (1 / 9.8) + + @property + def meters_per_square_second(self) -> float: + """The acceleration in meters per square second.""" + return self._base_value / 1 + + @property + def g_force(self) -> float: + """The acceleration in g force.""" + return self._base_value / (1 / 9.8) + + # --- End of auto generated part. --- + + +class Angle(_QuantityBase): + """The figure formed by two rays.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + degrees: float = 0.0, + radians: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += degrees * (3.141592653589793 / 180) + self._base_value += radians * 1 + + @property + def degrees(self) -> float: + """The angle in degrees.""" + return self._base_value / (3.141592653589793 / 180) + + @property + def radians(self) -> float: + """The angle in radians.""" + return self._base_value / 1 + + # --- End of auto generated part. --- + + +class Area(_QuantityBase): + """The two-dimensional extent of an object.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + square_miles: float = 0.0, + square_kilometers: float = 0.0, + square_meters: float = 0.0, + square_feet: float = 0.0, + square_inches: float = 0.0, + square_centimeters: float = 0.0, + square_millimeters: float = 0.0, + square_micrometers: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += square_miles * 1609.34**2 + self._base_value += square_kilometers * 10 ** (3 * 2) + self._base_value += square_meters * 1 + self._base_value += square_feet * 0.3048**2 + self._base_value += square_inches * 0.0254**2 + self._base_value += square_centimeters * 10 ** (-2 * 2) + self._base_value += square_millimeters * 10 ** (-3 * 2) + self._base_value += square_micrometers * 10 ** (-6 * 2) + + @property + def square_miles(self) -> float: + """The area in square miles.""" + return self._base_value / 1609.34**2 + + @property + def square_kilometers(self) -> float: + """The area in square kilometers.""" + return self._base_value / 10 ** (3 * 2) + + @property + def square_meters(self) -> float: + """The area in square meters.""" + return self._base_value / 1 + + @property + def square_feet(self) -> float: + """The area in square feet.""" + return self._base_value / 0.3048**2 + + @property + def square_inches(self) -> float: + """The area in square inches.""" + return self._base_value / 0.0254**2 + + @property + def square_centimeters(self) -> float: + """The area in square centimeters.""" + return self._base_value / 10 ** (-2 * 2) + + @property + def square_millimeters(self) -> float: + """The area in square millimeters.""" + return self._base_value / 10 ** (-3 * 2) + + @property + def square_micrometers(self) -> float: + """The area in square micrometers.""" + return self._base_value / 10 ** (-6 * 2) + + # --- End of auto generated part. --- + + +class Length(_QuantityBase): """The one-dimensional extent of an object or the distance between two points.""" - _meters: float - UNIT_CONVERSION: ClassVar[dict[str, float]] = { - "miles": 1609.34, - "kilometers": 10**3, - "meters": 10**0, - "feet": 0.3048, - "inches": 0.0254, - "centimeters": 10**-2, - "millimeters": 10**-3, - "micrometers": 10**-6, - } - - def __init__(self, **kwargs: float) -> None: - """Construct this class with the used units.""" - self._meters = ( - kwargs.get("miles", 0.0) * self.UNIT_CONVERSION["miles"] - + kwargs.get("kilometers", 0.0) * self.UNIT_CONVERSION["kilometers"] - + kwargs.get("meters", 0.0) * self.UNIT_CONVERSION["meters"] - + kwargs.get("feet", 0.0) * self.UNIT_CONVERSION["feet"] - + kwargs.get("inches", 0.0) * self.UNIT_CONVERSION["inches"] - + kwargs.get("centimeters", 0.0) * self.UNIT_CONVERSION["centimeters"] - + kwargs.get("millimeters", 0.0) * self.UNIT_CONVERSION["millimeters"] - + kwargs.get("micrometers", 0.0) * self.UNIT_CONVERSION["micrometers"] - ) + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + miles: float = 0.0, + kilometers: float = 0.0, + meters: float = 0.0, + feet: float = 0.0, + inches: float = 0.0, + centimeters: float = 0.0, + millimeters: float = 0.0, + micrometers: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += miles * 1609.34 + self._base_value += kilometers * 10**3 + self._base_value += meters * 1 + self._base_value += feet * 0.3048 + self._base_value += inches * 0.0254 + self._base_value += centimeters * 10**-2 + self._base_value += millimeters * 10**-3 + self._base_value += micrometers * 10**-6 @property def miles(self) -> float: """The length in miles.""" - return self._meters / self.UNIT_CONVERSION["miles"] + return self._base_value / 1609.34 @property def kilometers(self) -> float: """The length in kilometers.""" - return self._meters / self.UNIT_CONVERSION["kilometers"] + return self._base_value / 10**3 @property def meters(self) -> float: """The length in meters.""" - return self._meters / self.UNIT_CONVERSION["meters"] + return self._base_value / 1 @property def feet(self) -> float: """The length in feet.""" - return self._meters / self.UNIT_CONVERSION["feet"] + return self._base_value / 0.3048 @property def inches(self) -> float: """The length in inches.""" - return self._meters / self.UNIT_CONVERSION["inches"] + return self._base_value / 0.0254 @property def centimeters(self) -> float: """The length in centimeters.""" - return self._meters / self.UNIT_CONVERSION["centimeters"] + return self._base_value / 10**-2 @property def millimeters(self) -> float: """The length in millimeters.""" - return self._meters / self.UNIT_CONVERSION["millimeters"] + return self._base_value / 10**-3 @property def micrometers(self) -> float: """The length in micrometers.""" - return self._meters / self.UNIT_CONVERSION["micrometers"] + return self._base_value / 10**-6 + + # --- End of auto generated part. --- + + +class Mass(_QuantityBase): + """A measure of resistance to acceleration.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + tonnes: float = 0.0, + kilograms: float = 0.0, + pounds: float = 0.0, + ounces: float = 0.0, + grams: float = 0.0, + milligrams: float = 0.0, + micrograms: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += tonnes * 10**3 + self._base_value += kilograms * 1 + self._base_value += pounds * (1 / 2.20462) + self._base_value += ounces * (1 / 35.27396) + self._base_value += grams * 10**-3 + self._base_value += milligrams * 10**-6 + self._base_value += micrograms * 10**-9 + + @property + def tonnes(self) -> float: + """The mass in tonnes.""" + return self._base_value / 10**3 + + @property + def kilograms(self) -> float: + """The mass in kilograms.""" + return self._base_value / 1 + + @property + def pounds(self) -> float: + """The mass in pounds.""" + return self._base_value / (1 / 2.20462) + + @property + def ounces(self) -> float: + """The mass in ounces.""" + return self._base_value / (1 / 35.27396) + + @property + def grams(self) -> float: + """The mass in grams.""" + return self._base_value / 10**-3 + + @property + def milligrams(self) -> float: + """The mass in milligrams.""" + return self._base_value / 10**-6 + + @property + def micrograms(self) -> float: + """The mass in micrograms.""" + return self._base_value / 10**-9 + + # --- End of auto generated part. --- + + +class Velocity(_QuantityBase): + """Distance per time.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + meters_per_second: float = 0.0, + kilometers_per_hour: float = 0.0, + miles_per_hour: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += meters_per_second * 1 + self._base_value += kilometers_per_hour * (1 / 3.6) + self._base_value += miles_per_hour * (1 / 2.23694) + + @property + def meters_per_second(self) -> float: + """The velocity in meters per second.""" + return self._base_value / 1 + + @property + def kilometers_per_hour(self) -> float: + """The velocity in kilometers per hour.""" + return self._base_value / (1 / 3.6) + + @property + def miles_per_hour(self) -> float: + """The velocity in miles per hour.""" + return self._base_value / (1 / 2.23694) + + # --- End of auto generated part. --- + + +class Time(_QuantityBase): + """The duration of an event.""" + + # --- This part is auto generated. Do not change manually. --- + + def __init__( + self, + hours: float = 0.0, + minutes: float = 0.0, + seconds: float = 0.0, + milliseconds: float = 0.0, + ) -> None: + self._base_value = 0.0 + self._base_value += hours * 60 * 60 + self._base_value += minutes * 60 + self._base_value += seconds * 1 + self._base_value += milliseconds * 10**-3 + + @property + def hours(self) -> float: + """The time in hours.""" + return self._base_value / 60 * 60 + + @property + def minutes(self) -> float: + """The time in minutes.""" + return self._base_value / 60 + + @property + def seconds(self) -> float: + """The time in seconds.""" + return self._base_value / 1 + + @property + def milliseconds(self) -> float: + """The time in milliseconds.""" + return self._base_value / 10**-3 + + # --- End of auto generated part. --- diff --git a/test/test_length.py b/test/test_length.py deleted file mode 100644 index 209af60..0000000 --- a/test/test_length.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest - -from quantio import Length - - -def test_construction__multiple_units(): - actual = Length(meters=5, kilometers=1, centimeters=7) - assert actual.meters == 1005.07 - - -def test_miles(): - actual = Length(meters=1) - assert actual.miles == 1 / 1609.34 - - -def test_kilometers(): - actual = Length(kilometers=1) - assert actual.kilometers == 1 - - -def test_meters(): - actual = Length(meters=1) - assert actual.meters == 1 - - -def test_feet(): - actual = Length(feet=1) - assert actual.feet == 1 - - -def test_inches(): - actual = Length(inches=1) - assert actual.inches == 1 - - -def test_centimeters(): - actual = Length(centimeters=1) - assert actual.centimeters == 1 - - -def test_millimeters(): - actual = Length(millimeters=1) - assert actual.millimeters == 1 - - -def test_micrometers(): - actual = Length(micrometers=1) - assert actual.micrometers == 1 - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/test/test_quantities_base.py b/test/test_quantities_base.py new file mode 100644 index 0000000..05c4c67 --- /dev/null +++ b/test/test_quantities_base.py @@ -0,0 +1,42 @@ +import pytest + +from quantio import Length, CanNotAddTypesError, CanNotSubtractTypesError + + +def test_construction__multiple_units(): + actual = Length(meters=5, kilometers=1, centimeters=7) + assert actual.meters == 1005.07 + + +def test_add__success(): + length1 = Length(meters=1) + length2 = Length(meters=2) + + actual = length1 + length2 + assert actual == Length(meters=3) + + +def test_add__false_class(): + length = Length(meters=1) + + with pytest.raises(CanNotAddTypesError): + length += 1 + + +def test_sub__success(): + length1 = Length(meters=1) + length2 = Length(meters=2) + + actual = length2 - length1 + assert actual == Length(meters=1) + + +def test_sub__false_class(): + length = Length(meters=1) + + with pytest.raises(CanNotSubtractTypesError): + length -= 1 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])