Skip to content
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

More quantitites and boilerplate generation #3

Merged
merged 16 commits into from
Nov 9, 2024
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."""
128 changes: 128 additions & 0 deletions generate/generate_boilerplate.py
Original file line number Diff line number Diff line change
@@ -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)
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. ---
10 changes: 9 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 @@ -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
Expand Down
5 changes: 3 additions & 2 deletions quantio/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
37 changes: 37 additions & 0 deletions quantio/_quantity_base.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions quantio/exceptions.py
Original file line number Diff line number Diff line change
@@ -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}")
Loading
Loading