3 Commits

Author SHA1 Message Date
Markus Heiser
898fbe7bc2 [mod] ci: add cp3.14 (part 2)
Use stable version (remove `-dev` suffix).
Stop CI on error [1] / Don't ignore when 3.14 ends with ERROR.

[1] https://github.com/searxng/searxng/pull/5217

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-08 13:47:22 +02:00
Markus Heiser
d16283d93a [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>
2025-10-08 13:47:22 +02:00
Markus Heiser
8fdc59a760 [fix:py3.14] Struct fields aren't discovered in Python 3.14
This is a temporary workaround for #5284 [1].  However, the problem must be
solved at a different level, in msgspec [2].

[1] https://github.com/searxng/searxng/issues/5284
[2] https://github.com/jcrist/msgspec/issues/847

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
2025-10-08 13:47:22 +02:00
11 changed files with 70 additions and 29 deletions

View File

@@ -25,14 +25,13 @@ jobs:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14-dev"
- "3.14"
steps:
- name: Setup Python
@@ -56,7 +55,6 @@ jobs:
run: make V=1 install
- name: Run tests
continue-on-error: ${{ matrix.python-version == '3.14-dev' }}
run: make V=1 ci.test
theme:

View File

@@ -5,6 +5,10 @@
----
"""
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["ExpireCacheCfg", "ExpireCacheStats", "ExpireCache", "ExpireCacheSQLite"]
import abc

View File

@@ -17,6 +17,10 @@
"""
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
import typing as t
import os

View File

@@ -1,6 +1,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
import pathlib
import msgspec

View File

@@ -1,6 +1,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations for a favicon proxy"""
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
from typing import Callable

View File

@@ -16,6 +16,10 @@
:members:
"""
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["Result"]
import typing as t

View File

@@ -28,6 +28,9 @@ template.
"""
# pylint: disable=too-few-public-methods
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["AnswerSet", "Answer", "Translations", "WeatherAnswer"]

View File

@@ -14,6 +14,8 @@ template. For highlighting the code passages, Pygments_ is used.
"""
# pylint: disable=too-few-public-methods, disable=invalid-name
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["Code"]

View File

@@ -13,6 +13,9 @@ template.
"""
# pylint: disable=too-few-public-methods
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["KeyValue"]

View File

@@ -21,6 +21,8 @@ Related topics:
"""
# pylint: disable=too-few-public-methods, disable=invalid-name
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = ["Paper"]

View File

@@ -2,6 +2,10 @@
"""Implementations used for weather conditions and forecast."""
# pylint: disable=too-few-public-methods
# Struct fields aren't discovered in Python 3.14
# - https://github.com/searxng/searxng/issues/5284
from __future__ import annotations
__all__ = [
"symbol_url",
"Temperature",
@@ -251,7 +255,8 @@ class DateTime(msgspec.Struct):
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):
@@ -259,19 +264,19 @@ class Temperature(msgspec.Struct, kw_only=True):
measured values."""
val: float
unit: TemperatureUnits
unit: TemperatureUnit
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):
if self.unit not in self.units:
if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}")
def __str__(self):
return self.l10n()
def value(self, unit: TemperatureUnits) -> float:
def value(self, unit: TemperatureUnit) -> float:
if unit == self.unit:
return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@@ -279,7 +284,7 @@ class Temperature(msgspec.Struct, kw_only=True):
def l10n(
self,
unit: TemperatureUnits | None = None,
unit: TemperatureUnit | None = None,
locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}",
num_pattern: str = "#,##0",
@@ -318,7 +323,8 @@ class Temperature(msgspec.Struct, kw_only=True):
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):
@@ -326,19 +332,19 @@ class Pressure(msgspec.Struct, kw_only=True):
measured values."""
val: float
unit: PressureUnits
unit: PressureUnit
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):
if self.unit not in self.units:
if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}")
def __str__(self):
return self.l10n()
def value(self, unit: PressureUnits) -> float:
def value(self, unit: PressureUnit) -> float:
if unit == self.unit:
return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@@ -346,7 +352,7 @@ class Pressure(msgspec.Struct, kw_only=True):
def l10n(
self,
unit: PressureUnits | None = None,
unit: PressureUnit | None = None,
locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}",
num_pattern: str = "#,##0",
@@ -363,7 +369,8 @@ class Pressure(msgspec.Struct, kw_only=True):
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):
@@ -378,19 +385,19 @@ class WindSpeed(msgspec.Struct, kw_only=True):
"""
val: float
unit: WindSpeedUnits
unit: WindSpeedUnit
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):
if self.unit not in self.units:
if self.unit not in self.UNITS:
raise ValueError(f"invalid unit: {self.unit}")
def __str__(self):
return self.l10n()
def value(self, unit: WindSpeedUnits) -> float:
def value(self, unit: WindSpeedUnit) -> float:
if unit == self.unit:
return self.val
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
@@ -398,7 +405,7 @@ class WindSpeed(msgspec.Struct, kw_only=True):
def l10n(
self,
unit: WindSpeedUnits | None = None,
unit: WindSpeedUnit | None = None,
locale: babel.Locale | GeoLocation | None = None,
template: str = "{value} {unit}",
num_pattern: str = "#,##0",
@@ -415,7 +422,8 @@ class WindSpeed(msgspec.Struct, kw_only=True):
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):
@@ -424,8 +432,12 @@ class RelativeHumidity(msgspec.Struct):
val: float
# there exists only one unit (%) --> set "%" as the final value (constant)
unit: t.ClassVar["t.Final[RelativeHumidityUnits]"] = "%"
units: t.ClassVar[list[str]] = list(t.get_args(RelativeHumidityUnits))
unit: t.ClassVar[RelativeHumidityUnit] = "%"
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):
return self.l10n()
@@ -453,20 +465,23 @@ CompassPoint: t.TypeAlias = t.Literal[
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
]
"""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 for converting compass points and azimuth values (360°)"""
val: "float | int | CompassPoint"
unit: CompassUnits = "°"
unit: CompassUnit = "°"
UNITS: t.ClassVar[tuple[CompassUnit]] = COMPASS_UNITS
TURN: t.ClassVar[float] = 360.0
"""Full turn (360°)"""
POINTS: t.ClassVar[list[CompassPoint]] = list(t.get_args(CompassPoint))
POINTS: t.ClassVar[tuple[CompassPoint]] = COMPASS_POINTS
"""Compass points."""
RANGE: t.ClassVar[float] = TURN / len(POINTS)
@@ -484,7 +499,7 @@ class Compass(msgspec.Struct):
def __str__(self):
return self.l10n()
def value(self, unit: CompassUnits):
def value(self, unit: CompassUnit):
if unit == "Point" and isinstance(self.val, float):
return self.point(self.val)
if unit == "°":
@@ -503,7 +518,7 @@ class Compass(msgspec.Struct):
def l10n(
self,
unit: CompassUnits = "Point",
unit: CompassUnit = "Point",
locale: babel.Locale | GeoLocation | None = None,
template: str = "{value}{unit}",
num_pattern: str = "#,##0",