From 050880f052cc99578866bbb56a90c763dd682e14 Mon Sep 17 00:00:00 2001 From: daryaguettler Date: Wed, 11 Feb 2026 13:20:58 -0500 Subject: [PATCH] add ascii sanitization and update safe name calls --- epinterface/sbem/common.py | 20 +++++++++++++++++--- epinterface/sbem/components/envelope.py | 10 ++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/epinterface/sbem/common.py b/epinterface/sbem/common.py index d3429fb1..022af143 100644 --- a/epinterface/sbem/common.py +++ b/epinterface/sbem/common.py @@ -2,7 +2,7 @@ from typing import Annotated -from pydantic import BaseModel, BeforeValidator, Field +from pydantic import BaseModel, BeforeValidator, Field, field_validator from epinterface.sbem.annotations import ( nan_to_none_or_str, @@ -11,16 +11,30 @@ ) +def _ascii_safe(s: str) -> str: + """Return an ascii-only string suitable for idf names.""" + return s.encode("ascii", "ignore").decode("ascii") + + class NamedObject(BaseModel): """A Named object (with a name field).""" Name: str = Field(..., title="Name of the object used in referencing.") + @field_validator("Name", mode="before") + @classmethod + def sanitize_name(cls, v: str) -> str: + """Sanitize name to ascii-only, replacing spaces and commas with underscores.""" + if not isinstance(v, str): + return v + sanitized = _ascii_safe(v).replace(" ", "_").replace(",", "_") + return sanitized + # TODO: this is potentially a confusing footgun, but it's probably necessary. @property def safe_name(self) -> str: - """Get the safe name of the object.""" - return self.Name.replace(" ", "_").replace(",", "_") + """Get the safe name of the object (same as Name after validation).""" + return self.Name NanStr = Annotated[str | None, BeforeValidator(nan_to_none_or_str)] diff --git a/epinterface/sbem/components/envelope.py b/epinterface/sbem/components/envelope.py index be2853c1..31b4fe7e 100644 --- a/epinterface/sbem/components/envelope.py +++ b/epinterface/sbem/components/envelope.py @@ -50,14 +50,14 @@ def add_to_idf(self, idf: IDF) -> IDF: IDF: The updated IDF object. """ glazing_mat = SimpleGlazingMaterial( - Name=self.Name, + Name=self.safe_name, UFactor=self.UValue, Solar_Heat_Gain_Coefficient=self.SHGF, Visible_Transmittance=self.TVis, ) construction = Construction( - name=self.Name, + name=self.safe_name, layers=[glazing_mat], ) @@ -127,11 +127,9 @@ def add_infiltration_to_idf_zone( return idf infiltration_schedule_name = ( - f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION_Schedule" - ) - infiltration_name = ( - f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION" + f"{target_zone_or_zone_list_name}_{self.Name}_INFILTRATION_Schedule" ) + infiltration_name = f"{target_zone_or_zone_list_name}_{self.Name}_INFILTRATION" schedule = Schedule.constant_schedule( value=1, Name=infiltration_schedule_name, Type="Fraction" )