Skip to content

Commit

Permalink
refactor: add boilerplate using generation script
Browse files Browse the repository at this point in the history
  • Loading branch information
unexcellent committed Nov 8, 2024
1 parent 55682aa commit 4e20c49
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 52 deletions.
1 change: 1 addition & 0 deletions generate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package used for auto generating the boilerplate in the quantities."""
105 changes: 105 additions & 0 deletions generate/generate_boilerplate.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions generate/quantities.py
Original file line number Diff line number Diff line change
@@ -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. ---
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
37 changes: 12 additions & 25 deletions quantio/_quantity_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from abc import ABC

from .exceptions import CanNotAddTypesError, CanNotSubtractTypesError

Expand All @@ -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
Loading

0 comments on commit 4e20c49

Please sign in to comment.