From 818701e680d4edd3847e88c216e492e6536a7eda Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Wed, 17 Jun 2026 11:51:50 -0700 Subject: [PATCH] fix(data): Ensure that __slots__ is used up the inheritance tree I must have forgotten to do this because I didn't fully understand how __slots__ worked when I first started working on this library. This may be responsible for the weirdness here: https://github.com/ladybug-tools/ladybug/issues/656#event-26867959032 --- ladybug/_datacollectionbase.py | 45 ++++++++++--------------- ladybug/datacollection.py | 54 +++++++++++++++++++++++------- ladybug/datacollectionimmutable.py | 31 +++++++++++++++++ ladybug/epw.py | 4 +-- 4 files changed, 92 insertions(+), 42 deletions(-) diff --git a/ladybug/_datacollectionbase.py b/ladybug/_datacollectionbase.py index 5b96fe19..fa7abf16 100644 --- a/ladybug/_datacollectionbase.py +++ b/ladybug/_datacollectionbase.py @@ -33,7 +33,6 @@ class BaseCollection(object): __slots__ = ('_header', '_values', '_datetimes', '_validated_a_period') _collection_type = None _mutable = True - _enumeration = None def __init__(self, header, values, datetimes): """Initialize base collection. @@ -104,6 +103,16 @@ def validated_a_period(self): """ return self._validated_a_period + @property + def mutable_class(self): + """Get the mutable class for this type of data collection.""" + return self.__class__ + + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + return self.__class__ + @property def datetime_strings(self): """Get a list of datetime strings for this collection. @@ -212,14 +221,12 @@ def is_in_data_type_range(self, raise_exception=True): def to_mutable(self): """Get a mutable version of this collection.""" - return self.duplicate() + + return self.mutable_class(self.header, self.values, self.datetimes) def to_immutable(self): """Get an immutable version of this collection.""" - if self._enumeration is None: - self._get_mutable_enumeration() - col_obj = self._enumeration['immutable'][self._collection_type] - new_obj = col_obj(self.header, self.values, self.datetimes) + new_obj = self.immutable_class(self.header, self.values, self.datetimes) new_obj._validated_a_period = self._validated_a_period return new_obj @@ -398,9 +405,7 @@ def filter_by_conditional_statement(self, statement): A new Data Collection containing only the filtered data. """ _filt_values, _filt_datetimes = self._filter_by_statement(statement) - if self._enumeration is None: - self._get_mutable_enumeration() - col_obj = self._enumeration['mutable'][self._collection_type] + col_obj = self.mutable_class try: collection = col_obj(self.header.duplicate(), _filt_values, _filt_datetimes) except AssertionError as e: @@ -427,9 +432,7 @@ def filter_by_range(self, greater_than=float('-inf'), less_than=float('inf')): A new Data Collection with filtered data. """ _filt_values, _filt_datetimes = self._filter_by_range(greater_than, less_than) - if self._enumeration is None: - self._get_mutable_enumeration() - col_obj = self._enumeration['mutable'][self._collection_type] + col_obj = self.mutable_class try: collection = col_obj(self.header.duplicate(), _filt_values, _filt_datetimes) except AssertionError as e: @@ -451,9 +454,7 @@ def filter_by_pattern(self, pattern): A new Data Collection with filtered data. """ _filt_values, _filt_datetimes = self._filter_by_pattern(pattern) - if self._enumeration is None: - self._get_mutable_enumeration() - col_obj = self._enumeration['mutable'][self._collection_type] + col_obj = self.mutable_class collection = col_obj(self.header.duplicate(), _filt_values, _filt_datetimes) collection._validated_a_period = self._validated_a_period return collection @@ -509,12 +510,10 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None, mutable=Non if mutable is None: collection = self.__class__(header, values, self.datetimes) else: - if self._enumeration is None: - self._get_mutable_enumeration() if not mutable: - col_obj = self._enumeration['immutable'][self._collection_type] + col_obj = self.immutable_class else: - col_obj = self._enumeration['mutable'][self._collection_type] + col_obj = self.mutable_class collection = col_obj(header, values, self.datetimes) collection._validated_a_period = self._validated_a_period return collection @@ -1122,14 +1121,6 @@ def percentile_function(vals): return self._percentile(vals, percentile) return percentile_function - def _get_mutable_enumeration(self): - self._enumeration = {'mutable': {}, 'immutable': {}} - for clss in self._all_subclasses(BaseCollection): - if clss._mutable: - self._enumeration['mutable'][clss._collection_type] = clss - else: - self._enumeration['immutable'][clss._collection_type] = clss - def _all_subclasses(self, clss): return set(clss.__subclasses__()).union( [s for c in clss.__subclasses__() for s in self._all_subclasses(c)]) diff --git a/ladybug/datacollection.py b/ladybug/datacollection.py index 5a220244..615a2162 100644 --- a/ladybug/datacollection.py +++ b/ladybug/datacollection.py @@ -76,7 +76,7 @@ class HourlyDiscontinuousCollection(BaseCollection): * validated_a_period * values """ - + __slots__ = () _collection_type = 'HourlyDiscontinuous' def __init__(self, header, values, datetimes): @@ -141,6 +141,12 @@ def moys_dict(self): moy_dict[dt.moy] = val return moy_dict + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + from .datacollectionimmutable import HourlyDiscontinuousCollectionImmutable + return HourlyDiscontinuousCollectionImmutable + def filter_by_analysis_period(self, analysis_period): """Filter a Data Collection based on an analysis period. @@ -595,7 +601,7 @@ class HourlyContinuousCollection(HourlyDiscontinuousCollection): * validated_a_period * values """ - + __slots__ = () _collection_type = 'HourlyContinuous' def __init__(self, header, values): @@ -644,6 +650,12 @@ def datetimes(self): self._datetimes = self.header.analysis_period.datetimes return self._datetimes + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + from .datacollectionimmutable import HourlyContinuousCollectionImmutable + return HourlyContinuousCollectionImmutable + def interpolate_holes(self): """All continuous collections do not have holes in the data set. @@ -900,12 +912,13 @@ def group_by_month(self): indx += interval return hourly_data_by_month + def to_mutable(self): + """Get an mutable version of this collection.""" + return self.mutable_class(self.header, self.values) + def to_immutable(self): """Get an immutable version of this collection.""" - if self._enumeration is None: - self._get_mutable_enumeration() - col_obj = self._enumeration['immutable'][self._collection_type] - return col_obj(self.header, self.values) + return self.immutable_class(self.header, self.values) def get_aligned_collection(self, value=0, data_type=None, unit=None, mutable=None): """Return a Collection aligned with this one composed of one repeated value. @@ -937,12 +950,10 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None, mutable=Non if mutable is None: collection = self.__class__(header, values) else: - if self._enumeration is None: - self._get_mutable_enumeration() if not mutable: - col_obj = self._enumeration['immutable'][self._collection_type] + col_obj = self.immutable_class else: - col_obj = self._enumeration['mutable'][self._collection_type] + col_obj = self.mutable_class collection = col_obj(header, values) return collection @@ -1093,6 +1104,7 @@ class DailyCollection(BaseCollection): * validated_a_period * values """ + __slots__ = () _collection_type = 'Daily' def __init__(self, header, values, datetimes): @@ -1118,6 +1130,12 @@ def datetime_strings(self): lp_yr = self.header.analysis_period.is_leap_year return [str(Date.from_doy(d, lp_yr)) for d in self._datetimes] + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + from .datacollectionimmutable import DailyCollectionImmutable + return DailyCollectionImmutable + def filter_by_analysis_period(self, analysis_period): """Filter the Data Collection based on an analysis period. @@ -1341,7 +1359,7 @@ class MonthlyCollection(BaseCollection): * validated_a_period * values """ - + __slots__ = () _collection_type = 'Monthly' def __init__(self, header, values, datetimes): @@ -1366,6 +1384,12 @@ def datetime_strings(self): """ return [AnalysisPeriod.MONTHNAMES[int(d)] for d in self._datetimes] + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + from .datacollectionimmutable import MonthlyCollectionImmutable + return MonthlyCollectionImmutable + def filter_by_analysis_period(self, analysis_period): """Filter the Data Collection based on an analysis period. @@ -1492,7 +1516,7 @@ class MonthlyPerHourCollection(BaseCollection): * validated_a_period * values """ - + __slots__ = () _collection_type = 'MonthlyPerHour' def __init__(self, header, values, datetimes): @@ -1520,6 +1544,12 @@ def datetime_strings(self): for d in self._datetimes ] + @property + def immutable_class(self): + """Get the immutable class for this type of data collection.""" + from .datacollectionimmutable import MonthlyPerHourCollectionImmutable + return MonthlyPerHourCollectionImmutable + def filter_by_analysis_period(self, analysis_period): """Filter the Data Collection based on an analysis period. diff --git a/ladybug/datacollectionimmutable.py b/ladybug/datacollectionimmutable.py index c89a0747..d0f094f6 100644 --- a/ladybug/datacollectionimmutable.py +++ b/ladybug/datacollectionimmutable.py @@ -25,6 +25,7 @@ class _ImmutableCollectionBase(object): """Base class for all immutable Data Collections.""" + __slots__ = () _mutable = False @property @@ -62,6 +63,12 @@ def __setitem__(self, key, value): class HourlyDiscontinuousCollectionImmutable( _ImmutableCollectionBase, HourlyDiscontinuousCollection): """Immutable Discontinuous Data Collection at hourly or sub-hourly intervals.""" + __slots__ = () + + @property + def mutable_class(self): + """Get the immutable class for this type of data collection.""" + return HourlyDiscontinuousCollection def convert_to_culled_timestep(self, timestep=1): """This method is not available for immutable collections.""" @@ -77,6 +84,12 @@ def to_mutable(self): class HourlyContinuousCollectionImmutable( _ImmutableCollectionBase, HourlyContinuousCollection): """Immutable Continuous Data Collection at hourly or sub-hourly intervals.""" + __slots__ = () + + @property + def mutable_class(self): + """Get the immutable class for this type of data collection.""" + return DailyCollection def convert_to_culled_timestep(self, timestep=1): """This method is not available for immutable collections.""" @@ -96,6 +109,12 @@ def duplicate(self): class DailyCollectionImmutable( _ImmutableCollectionBase, DailyCollection): """Immutable Daily Data Collection.""" + __slots__ = () + + @property + def mutable_class(self): + """Get the immutable class for this type of data collection.""" + return DailyCollection def to_mutable(self): """Get a mutable version of this collection.""" @@ -107,6 +126,12 @@ def to_mutable(self): class MonthlyCollectionImmutable( _ImmutableCollectionBase, MonthlyCollection): """Immutable Monthly Data Collection.""" + __slots__ = () + + @property + def mutable_class(self): + """Get the immutable class for this type of data collection.""" + return MonthlyCollection def to_mutable(self): """Get a mutable version of this collection.""" @@ -118,6 +143,12 @@ def to_mutable(self): class MonthlyPerHourCollectionImmutable( _ImmutableCollectionBase, MonthlyPerHourCollection): """Immutable Monthly Per Hour Data Collection.""" + __slots__ = () + + @property + def mutable_class(self): + """Get the immutable class for this type of data collection.""" + return MonthlyPerHourCollection def to_mutable(self): """Get a mutable version of this collection.""" diff --git a/ladybug/epw.py b/ladybug/epw.py index b6d92ff6..b906bd44 100644 --- a/ladybug/epw.py +++ b/ladybug/epw.py @@ -1928,9 +1928,7 @@ def save(self, file_path): Args: file_path: Text for the full path to where the file will be written. """ - file_data = self.to_file_string() - write_to_file(file_path, file_data, True) - return file_data + return self.write(file_path) def ToString(self): """Overwrite .NET ToString."""