Source code for encomp._polars_dtype
"""Pint-light registration core for encomp's Polars unit extension dtype.
This module may be imported by :mod:`encomp.coolprop` before any unit computation.
Keep Pint and :mod:`encomp.units` imports inside the methods that actually need them:
registering the dtype itself must be cheap and must happen before Polars reads a file.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import polars as pl
if TYPE_CHECKING:
from .units import Unit
EXTENSION_NAME = "encomp.unit"
"""Public, stable Arrow extension name for a unit-carrying column."""
_CANONICAL_UNIT_FORMAT = "~P"
"""Fixed Pint format spec for the unit string stored in dtype metadata."""
def canonical_unit_string(unit: str | Unit[Any]) -> str:
"""Return the fixed canonical rendering stored in extension metadata.
The rendering is independent of encomp's process-wide display format because it
is an on-disk, cross-process contract. It normalizes spelling (``"m^3"`` and
``"m³"``), not physical equivalence (``"Pa"`` and ``"N/m²"`` remain distinct).
"""
from .units import Unit
parsed = Unit(unit) if isinstance(unit, str) else unit
return format(parsed, _CANONICAL_UNIT_FORMAT)
def _validate_storage(storage: pl.DataType) -> None:
if not (storage.is_float() or storage.is_integer()):
raise TypeError(
f"unit dtype storage must be a float or integer dtype, got {storage!r}. "
"Boolean and non-numeric columns cannot carry a unit, and a column that "
"already has a unit dtype cannot be re-wrapped."
)
[docs]
class UnitDType(pl.BaseExtension):
"""Polars extension dtype carrying a physical unit as column metadata.
A dtype instance consists of the stable extension name ``"encomp.unit"``, a
numeric storage dtype, and a canonical unit string. Polars refuses value-producing
arithmetic on extension columns; validated :class:`encomp.polars.QuantityFrame`
descriptors enter Quantity unit algebra and ``QuantityFrame.derive`` returns
results to a frame.
"""
def __init__(self, unit: str | Unit[Any], storage: pl.DataType | None = None) -> None:
if storage is None:
storage = pl.Float64()
_validate_storage(storage)
super().__init__(EXTENSION_NAME, storage, canonical_unit_string(unit))
@property
def unit(self) -> Unit[Any]:
"""The column unit parsed from the extension metadata."""
from .units import Unit
metadata = self.ext_metadata()
if metadata is None:
raise ValueError(f"{EXTENSION_NAME} dtype without a unit string in its metadata: {self!r}")
return Unit(metadata)
def _string_repr(self) -> str:
return f"unit[{self.ext_metadata()}]"
# This import side effect is the purpose of the module. Both encomp.polars and every
# encomp entry point that can feed extension-typed data to the native plugin import it
# before reading data, so Polars never strips the unit metadata first.
pl.register_extension_type(EXTENSION_NAME, UnitDType)