[fix:py3.14] using a non-empty mutable collection as default is unsafe

Starting with Python 3.14 msgspec reports::

    File "/share/searxng/searx/weather.py", line 261, in <module>
        class Temperature(msgspec.Struct, kw_only=True):
        ...<60 lines>...
                return template.format(value=val_str, unit=unit)
    TypeError: Using a non-empty mutable collection (['°C', '°F', 'K']) \
               as a default value is unsafe.\
               Instead configure a `default_factory` for this field.

The problem is solved by the fact that there are now global constants for the
units (BTW singular/plural names of the type definitions are fixed):

- TEMPERATURE_UNITS
- PRESSURE_UNITS
- WIND_SPEED_UNITS
- RELATIVE_HUMIDITY_UNITS
- COMPASS_POINTS
- COMPASS_UNITS

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Markus Heiser 2025-10-07 15:47:03 +02:00 committed by Markus Heiser
parent 8fdc59a760
commit d16283d93a

View File

@ -255,7 +255,8 @@ class DateTime(msgspec.Struct):
return babel.dates.format_date(self.datetime, format=fmt, locale=locale) return babel.dates.format_date(self.datetime, format=fmt, locale=locale)
TemperatureUnits: t.TypeAlias = t.Literal["°C", "°F", "K"] TemperatureUnit: t.TypeAlias = t.Literal["°C", "°F", "K"]
TEMPERATURE_UNITS: t.Final[tuple[TemperatureUnit]] = t.get_args(TemperatureUnit)
class Temperature(msgspec.Struct, kw_only=True): class Temperature(msgspec.Struct, kw_only=True):
@ -263,19 +264,19 @@ class Temperature(msgspec.Struct, kw_only=True):
measured values.""" measured values."""
val: float val: float
unit: TemperatureUnits unit: TemperatureUnit
si_name: t.ClassVar[str] = "Q11579" si_name: t.ClassVar[str] = "Q11579"
units: t.ClassVar[list[str]] = list(t.get_args(TemperatureUnits)) UNITS: t.ClassVar[tuple[TemperatureUnit]] = TEMPERATURE_UNITS
def __post_init__(self): def __post_init__(self):
if self.unit not in self.units: if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}") raise ValueError(f"invalid unit: {self.unit}")
def __str__(self): def __str__(self):
return self.l10n() return self.l10n()
def value(self, unit: TemperatureUnits) -> float: def value(self, unit: TemperatureUnit) -> float:
if unit == self.unit: if unit == self.unit:
return self.val return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@ -283,7 +284,7 @@ class Temperature(msgspec.Struct, kw_only=True):
def l10n( def l10n(
self, self,
unit: TemperatureUnits | None = None, unit: TemperatureUnit | None = None,
locale: babel.Locale | GeoLocation | None = None, locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}", template: str = "{value} {unit}",
num_pattern: str = "#,##0", num_pattern: str = "#,##0",
@ -322,7 +323,8 @@ class Temperature(msgspec.Struct, kw_only=True):
return template.format(value=val_str, unit=unit) return template.format(value=val_str, unit=unit)
PressureUnits: t.TypeAlias = t.Literal["Pa", "hPa", "cm Hg", "bar"] PressureUnit: t.TypeAlias = t.Literal["Pa", "hPa", "cm Hg", "bar"]
PRESSURE_UNITS: t.Final[tuple[PressureUnit]] = t.get_args(PressureUnit)
class Pressure(msgspec.Struct, kw_only=True): class Pressure(msgspec.Struct, kw_only=True):
@ -330,19 +332,19 @@ class Pressure(msgspec.Struct, kw_only=True):
measured values.""" measured values."""
val: float val: float
unit: PressureUnits unit: PressureUnit
si_name: t.ClassVar[str] = "Q44395" si_name: t.ClassVar[str] = "Q44395"
units: t.ClassVar[list[str]] = list(t.get_args(PressureUnits)) UNITS: t.ClassVar[tuple[PressureUnit]] = PRESSURE_UNITS
def __post_init__(self): def __post_init__(self):
if self.unit not in self.units: if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}") raise ValueError(f"invalid unit: {self.unit}")
def __str__(self): def __str__(self):
return self.l10n() return self.l10n()
def value(self, unit: PressureUnits) -> float: def value(self, unit: PressureUnit) -> float:
if unit == self.unit: if unit == self.unit:
return self.val return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@ -350,7 +352,7 @@ class Pressure(msgspec.Struct, kw_only=True):
def l10n( def l10n(
self, self,
unit: PressureUnits | None = None, unit: PressureUnit | None = None,
locale: babel.Locale | GeoLocation | None = None, locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}", template: str = "{value} {unit}",
num_pattern: str = "#,##0", num_pattern: str = "#,##0",
@ -367,7 +369,8 @@ class Pressure(msgspec.Struct, kw_only=True):
return template.format(value=val_str, unit=unit) return template.format(value=val_str, unit=unit)
WindSpeedUnits: t.TypeAlias = t.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"] WindSpeedUnit: t.TypeAlias = t.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"]
WIND_SPEED_UNITS: t.Final[tuple[WindSpeedUnit]] = t.get_args(WindSpeedUnit)
class WindSpeed(msgspec.Struct, kw_only=True): class WindSpeed(msgspec.Struct, kw_only=True):
@ -382,19 +385,19 @@ class WindSpeed(msgspec.Struct, kw_only=True):
""" """
val: float val: float
unit: WindSpeedUnits unit: WindSpeedUnit
si_name: t.ClassVar[str] = "Q182429" si_name: t.ClassVar[str] = "Q182429"
units: t.ClassVar[list[str]] = list(t.get_args(WindSpeedUnits)) UNITS: t.ClassVar[tuple[WindSpeedUnit]] = WIND_SPEED_UNITS
def __post_init__(self): def __post_init__(self):
if self.unit not in self.units: if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}") raise ValueError(f"invalid unit: {self.unit}")
def __str__(self): def __str__(self):
return self.l10n() return self.l10n()
def value(self, unit: WindSpeedUnits) -> float: def value(self, unit: WindSpeedUnit) -> float:
if unit == self.unit: if unit == self.unit:
return self.val return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@ -402,7 +405,7 @@ class WindSpeed(msgspec.Struct, kw_only=True):
def l10n( def l10n(
self, self,
unit: WindSpeedUnits | None = None, unit: WindSpeedUnit | None = None,
locale: babel.Locale | GeoLocation | None = None, locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}", template: str = "{value} {unit}",
num_pattern: str = "#,##0", num_pattern: str = "#,##0",
@ -419,7 +422,8 @@ class WindSpeed(msgspec.Struct, kw_only=True):
return template.format(value=val_str, unit=unit) return template.format(value=val_str, unit=unit)
RelativeHumidityUnits: t.TypeAlias = t.Literal["%"] RelativeHumidityUnit: t.TypeAlias = t.Literal["%"]
RELATIVE_HUMIDITY_UNITS: t.Final[tuple[RelativeHumidityUnit]] = t.get_args(RelativeHumidityUnit)
class RelativeHumidity(msgspec.Struct): class RelativeHumidity(msgspec.Struct):
@ -428,8 +432,12 @@ class RelativeHumidity(msgspec.Struct):
val: float val: float
# there exists only one unit (%) --> set "%" as the final value (constant) # there exists only one unit (%) --> set "%" as the final value (constant)
unit: t.ClassVar["t.Final[RelativeHumidityUnits]"] = "%" unit: t.ClassVar[RelativeHumidityUnit] = "%"
units: t.ClassVar[list[str]] = list(t.get_args(RelativeHumidityUnits)) UNITS: t.ClassVar[tuple[RelativeHumidityUnit]] = RELATIVE_HUMIDITY_UNITS
def __post_init__(self):
if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}")
def __str__(self): def __str__(self):
return self.l10n() return self.l10n()
@ -457,20 +465,23 @@ CompassPoint: t.TypeAlias = t.Literal[
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
] ]
"""Compass point type definition""" """Compass point type definition"""
COMPASS_POINTS: t.Final[tuple[CompassPoint]] = t.get_args(CompassPoint)
CompassUnits: t.TypeAlias = t.Literal["°", "Point"] CompassUnit: t.TypeAlias = t.Literal["°", "Point"]
COMPASS_UNITS: t.Final[tuple[CompassUnit]] = t.get_args(CompassUnit)
class Compass(msgspec.Struct): class Compass(msgspec.Struct):
"""Class for converting compass points and azimuth values (360°)""" """Class for converting compass points and azimuth values (360°)"""
val: "float | int | CompassPoint" val: "float | int | CompassPoint"
unit: CompassUnits = "°" unit: CompassUnit = "°"
UNITS: t.ClassVar[tuple[CompassUnit]] = COMPASS_UNITS
TURN: t.ClassVar[float] = 360.0 TURN: t.ClassVar[float] = 360.0
"""Full turn (360°)""" """Full turn (360°)"""
POINTS: t.ClassVar[list[CompassPoint]] = list(t.get_args(CompassPoint)) POINTS: t.ClassVar[tuple[CompassPoint]] = COMPASS_POINTS
"""Compass points.""" """Compass points."""
RANGE: t.ClassVar[float] = TURN / len(POINTS) RANGE: t.ClassVar[float] = TURN / len(POINTS)
@ -488,7 +499,7 @@ class Compass(msgspec.Struct):
def __str__(self): def __str__(self):
return self.l10n() return self.l10n()
def value(self, unit: CompassUnits): def value(self, unit: CompassUnit):
if unit == "Point" and isinstance(self.val, float): if unit == "Point" and isinstance(self.val, float):
return self.point(self.val) return self.point(self.val)
if unit == "°": if unit == "°":
@ -507,7 +518,7 @@ class Compass(msgspec.Struct):
def l10n( def l10n(
self, self,
unit: CompassUnits = "Point", unit: CompassUnit = "Point",
locale: babel.Locale | GeoLocation | None = None, locale: babel.Locale | GeoLocation | None = None,
template: str = "{value}{unit}", template: str = "{value}{unit}",
num_pattern: str = "#,##0", num_pattern: str = "#,##0",