From 4e20c4947ddee48e98c7a8ec223bae58cb41cb96 Mon Sep 17 00:00:00 2001 From: unexcellent <> Date: Fri, 8 Nov 2024 16:37:23 +0100 Subject: [PATCH] refactor: add boilerplate using generation script --- generate/__init__.py | 1 + generate/generate_boilerplate.py | 105 +++++++++++++++++ generate/quantities.py | 27 +++++ pyproject.toml | 4 +- quantio/_quantity_base.py | 37 ++---- quantio/quantities.py | 187 ++++++++++++++++++++++++++----- 6 files changed, 309 insertions(+), 52 deletions(-) create mode 100644 generate/__init__.py create mode 100644 generate/generate_boilerplate.py create mode 100644 generate/quantities.py 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..0f7c2cd --- /dev/null +++ b/generate/generate_boilerplate.py @@ -0,0 +1,105 @@ +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}") + + return code + + +def _generate_properties(current_class: str, units: dict[str, str]) -> list[str]: + code = [] + + for unit, factor in units.items(): + code.append("") + 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}") + + return code + + +if __name__ == "__main__": + quantities_with_fields = { + "Area": { + "square_miles": "1609.34**2", + "square_kilometers": "10 ** (3 * 2)", + "square_meters": "10**0", + "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": "10**0", + "feet": "0.3048", + "inches": "0.0254", + "centimeters": "10**-2", + "millimeters": "10**-3", + "micrometers": "10**-6", + }, + "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 716291c..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 @@ -36,6 +36,8 @@ ignore = [ "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 diff --git a/quantio/_quantity_base.py b/quantio/_quantity_base.py index 587810a..f201f43 100644 --- a/quantio/_quantity_base.py +++ b/quantio/_quantity_base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import ABC from .exceptions import CanNotAddTypesError, CanNotSubtractTypesError @@ -11,40 +11,27 @@ class _QuantityBase(ABC): _base_value: float "The base unit of the quantity." - @property - @abstractmethod - def _UNIT_CONVERSION(self) -> dict[str, float]: - """Table used for recording the units with conversion values.""" - - def __init__(self, **kwargs: float) -> None: - """Construct this class with the used units.""" - self._base_value = kwargs.get("_base_value", 0.0) - - for unit_name, factor in self._UNIT_CONVERSION.items(): - self._base_value += kwargs.get(unit_name, 0.0) * factor - - for unit_name, factor in self._UNIT_CONVERSION.items(): - - def make_property(factor: float) -> property: - return property(lambda self: self._base_value / factor) - - setattr(self.__class__, unit_name, make_property(factor)) - def __eq__(self, other: object) -> bool: """Assess if this object is the same as another.""" - if type(self) is not type(other): - return False + if isinstance(other, type(self)): + return self._base_value == other._base_value - return self._base_value == other._base_value # type: ignore + 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__) - return type(self)(_base_value=self._base_value + other._base_value) + + 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__) - return type(self)(_base_value=self._base_value - other._base_value) + + result = type(self)() + result._base_value = self._base_value - other._base_value + return result diff --git a/quantio/quantities.py b/quantio/quantities.py index 7cfc22c..eacdac6 100644 --- a/quantio/quantities.py +++ b/quantio/quantities.py @@ -6,39 +6,174 @@ class Area(_QuantityBase): """The two-dimensional extent of an object.""" - _UNIT_CONVERSION: dict[str, float] = { - "square_miles": 1609.34**2, - "square_kilometers": 10 ** (3 * 2), - "square_meters": 10**0, - "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), - } + # --- 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 * 10**0 + 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 / 10**0 + + @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.""" - _UNIT_CONVERSION: 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, - } + # --- 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 * 10**0 + 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._base_value / 1609.34 + + @property + def kilometers(self) -> float: + """The length in kilometers.""" + return self._base_value / 10**3 + + @property + def meters(self) -> float: + """The length in meters.""" + return self._base_value / 10**0 + + @property + def feet(self) -> float: + """The length in feet.""" + return self._base_value / 0.3048 + + @property + def inches(self) -> float: + """The length in inches.""" + return self._base_value / 0.0254 + + @property + def centimeters(self) -> float: + """The length in centimeters.""" + return self._base_value / 10**-2 + + @property + def millimeters(self) -> float: + """The length in millimeters.""" + return self._base_value / 10**-3 + + @property + def micrometers(self) -> float: + """The length in micrometers.""" + return self._base_value / 10**-6 + + # --- End of auto generated part. --- class Time(_QuantityBase): """The duration of an event.""" - _UNIT_CONVERSION: dict[str, float] = { - "hours": 60 * 60, - "minutes": 60, - "seconds": 1, - "milliseconds": 10**-3, - } + # --- 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. ---