Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Allow handling warnings when writing deprecated units
The `"ogip"` and `"vounit"` formats have deprecated units. Converting
such units to strings has so far caused an unavoidable warning. A new
parameter of the `UnitBase.to_string()` and unit formatter `to_string()`
methods allows silencing that warning or raising an error instead. This
is analogous to the `parse_strict` parameter of unit parsing methods.

The `StrEnum` that controls the new behavior is defined in a new module
so that it could be imported without having to worry about import loops.
  • Loading branch information
eerovaher committed Sep 16, 2025
commit 32d26b13e635efd8e309ddd1110a74b6b1d8df78
12 changes: 9 additions & 3 deletions astropy/units/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ def powers(self) -> list[UnitPower]:
return [1]

def to_string(
self, format: type["astropy.units.format.Base"] | str | None = None, **kwargs
self,
format: type["astropy.units.format.Base"] | str | None = None,
*,
deprecations: Literal["silent", "warn", "raise"] = "warn",
**kwargs,
) -> str:
r"""Output the unit in the given format as a string.

Expand All @@ -152,7 +156,9 @@ def to_string(
format : `astropy.units.format.Base` subclass or str or None
The name of a format or a formatter class. If not
provided (or `None`), defaults to the generic format.

deprecations : {"warn", "silent", "raise"}, optional, keyword-only
Whether deprecated units should emit a warning, be handled
silently or raise an error.
**kwargs
Further options forwarded to the formatter. Currently
recognized is ``fraction``, which can take the following values:
Expand Down Expand Up @@ -196,7 +202,7 @@ def to_string(

err.add_note(known_formats())
raise err
return formatter.to_string(self, **kwargs)
return formatter.to_string(self, deprecations=deprecations, **kwargs)

def __format__(self, format_spec: str) -> str:
try:
Expand Down
17 changes: 17 additions & 0 deletions astropy/units/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

from enum import StrEnum, auto
from typing import Self


class DeprecatedUnitAction(StrEnum):
SILENT = auto()
WARN = auto()
RAISE = auto()

@classmethod
def _missing_(cls, value) -> Self | None:
raise ValueError(
f"invalid deprecation handling option: {value!r}. Valid options are "
f"{', '.join(repr(opt.value) for opt in cls)}."
)
45 changes: 36 additions & 9 deletions astropy/units/format/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import warnings
from collections.abc import Iterable
from typing import ClassVar, Literal
from typing import ClassVar, Literal, assert_never

import numpy as np

Expand All @@ -14,7 +14,8 @@
UnitBase,
get_current_unit_registry,
)
from astropy.units.errors import UnitsWarning
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitsError, UnitsWarning
from astropy.units.typing import UnitPower, UnitScale
from astropy.units.utils import maybe_simple_fraction
from astropy.utils.misc import did_you_mean
Expand Down Expand Up @@ -135,7 +136,11 @@ def _format_multiline_fraction(

@classmethod
def to_string(
cls, unit: UnitBase, *, fraction: bool | Literal["inline", "multiline"] = True
cls,
unit: UnitBase,
*,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
fraction: bool | Literal["inline", "multiline"] = True,
) -> str:
"""Convert a unit to its string representation.

Expand All @@ -145,6 +150,9 @@ def to_string(
----------
unit : |Unit|
The unit to convert.
deprecations : {"warn", "silent", "raise"}, optional, keyword-only
Whether deprecated units should emit a warning, be handled
silently or raise an error.
fraction : {False|True|'inline'|'multiline'}, optional
Options are as follows:

Expand Down Expand Up @@ -256,15 +264,27 @@ def _did_you_mean_units(cls, unit: str) -> str:
return did_you_mean(unit, cls._units, fix=cls._fix_deprecated)

@classmethod
def _validate_unit(cls, s: str) -> UnitBase:
def _validate_unit(
cls, s: str, deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN
) -> UnitBase:
if s in cls._deprecated_units:
alternative = (
unit.represents if isinstance(unit := cls._units[s], Unit) else None
)
msg = f"The unit {s!r} has been deprecated in the {cls.__name__} standard."
if alternative:
msg += f" Suggested: {cls.to_string(alternative)}."
warnings.warn(msg, UnitsWarning)

match DeprecatedUnitAction(deprecations):
case DeprecatedUnitAction.WARN:
warnings.warn(msg, UnitsWarning)
case DeprecatedUnitAction.RAISE:
raise UnitsError(msg)
case DeprecatedUnitAction.SILENT:
pass
case _:
assert_never(deprecations)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.


return cls._units[s]

@classmethod
Expand All @@ -275,25 +295,32 @@ def _invalid_unit_error_message(cls, unit: str) -> str:
)

@classmethod
def _decompose_to_known_units(cls, unit: CompositeUnit | NamedUnit) -> UnitBase:
def _decompose_to_known_units(
cls,
unit: CompositeUnit | NamedUnit,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> UnitBase:
"""
Partially decomposes a unit so it is only composed of units that
are "known" to a given format.
"""
if isinstance(unit, CompositeUnit):
return CompositeUnit(
unit.scale,
[cls._decompose_to_known_units(base) for base in unit.bases],
[
cls._decompose_to_known_units(base, deprecations)
for base in unit.bases
],
unit.powers,
_error_check=False,
)
if isinstance(unit, NamedUnit):
name = unit._get_format_name(cls.name)
try:
return cls._validate_unit(name)
return cls._validate_unit(name, deprecations=deprecations)
except KeyError:
if isinstance(unit, Unit):
return cls._decompose_to_known_units(unit._represents)
return cls._decompose_to_known_units(unit._represents, deprecations)
raise ValueError(cls._invalid_unit_error_message(name)) from None
raise TypeError(
f"unit argument must be a 'NamedUnit' or 'CompositeUnit', not {type(unit)}"
Expand Down
6 changes: 5 additions & 1 deletion astropy/units/format/cds.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from astropy.extern.ply.lex import Lexer
from astropy.units.core import CompositeUnit, Unit, UnitBase
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.utils import is_effectively_unity
from astropy.utils import classproperty, parsing
from astropy.utils.parsing import ThreadSafeParser
Expand Down Expand Up @@ -268,7 +269,10 @@ def _format_superscript(cls, number: str) -> str:

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = False,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
# Remove units that aren't known to the format
unit = cls._decompose_to_known_units(unit)
Expand Down
6 changes: 5 additions & 1 deletion astropy/units/format/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import ClassVar, Literal

from astropy.units.core import UnitBase
from astropy.units.enums import DeprecatedUnitAction

from . import base

Expand Down Expand Up @@ -53,7 +54,10 @@ def _format_multiline_fraction(

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = False,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
# Change default of fraction to False, i.e., we typeset
# without a fraction by default.
Expand Down
6 changes: 5 additions & 1 deletion astropy/units/format/fits.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import numpy as np

from astropy.units.core import CompositeUnit, UnitBase
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitScaleError
from astropy.utils import classproperty

Expand Down Expand Up @@ -58,7 +59,10 @@ def _units(cls) -> dict[str, UnitBase]:

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = False,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
# Remove units that aren't known to the format
unit = cls._decompose_to_known_units(unit)
Expand Down
5 changes: 4 additions & 1 deletion astropy/units/format/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from astropy.extern.ply.lex import Lexer
from astropy.units.core import CompositeUnit, Unit, UnitBase, get_current_unit_registry
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitsWarning
from astropy.units.typing import UnitScale
from astropy.utils import classproperty, parsing
Expand Down Expand Up @@ -411,7 +412,9 @@ def _units(cls) -> dict[str, UnitBase]:
return get_current_unit_registry().registry

@classmethod
def _validate_unit(cls, s: str) -> UnitBase:
def _validate_unit(
cls, s: str, deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN
) -> UnitBase:
if s in cls._unit_symbols:
s = cls._unit_symbols[s]

Expand Down
7 changes: 6 additions & 1 deletion astropy/units/format/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import ClassVar, Literal

from astropy.units.core import NamedUnit, UnitBase
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.typing import UnitPower

from . import console
Expand Down Expand Up @@ -62,6 +63,7 @@ def to_string(
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = "multiline",
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
s = super().to_string(unit, fraction=fraction)
return rf"$\mathrm{{{s}}}$"
Expand All @@ -82,6 +84,9 @@ class LatexInline(Latex):

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = False,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
return super().to_string(unit, fraction=fraction)
8 changes: 6 additions & 2 deletions astropy/units/format/ogip.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from astropy.extern.ply.lex import Lexer
from astropy.units.core import CompositeUnit, UnitBase
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitParserWarning, UnitsWarning
from astropy.units.typing import UnitScale
from astropy.utils import classproperty, parsing
Expand Down Expand Up @@ -337,10 +338,13 @@ def _format_superscript(cls, number: str) -> str:

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = "inline"
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = "inline",
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
# Remove units that aren't known to the format
unit = cls._decompose_to_known_units(unit)
unit = cls._decompose_to_known_units(unit, deprecations)

if isinstance(unit, CompositeUnit):
# Can't use np.log10 here, because p[0] may be a Python long.
Expand Down
16 changes: 12 additions & 4 deletions astropy/units/format/vounit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
dimensionless_unscaled,
si_prefixes,
)
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitParserWarning, UnitScaleError, UnitsError
from astropy.units.typing import UnitScale
from astropy.utils import classproperty
Expand Down Expand Up @@ -124,7 +125,11 @@ def _get_unit(cls, t: LexToken) -> UnitBase:
raise

@classmethod
def _decompose_to_known_units(cls, unit: CompositeUnit | NamedUnit) -> UnitBase:
def _decompose_to_known_units(
cls,
unit: CompositeUnit | NamedUnit,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> UnitBase:
# The da- and d- prefixes are discouraged. This has the
# effect of adding a scale to value in the result.
if isinstance(unit, PrefixUnit) and unit._represents.scale in (0.1, 10.0):
Expand All @@ -134,7 +139,7 @@ def _decompose_to_known_units(cls, unit: CompositeUnit | NamedUnit) -> UnitBase:
and unit._get_format_name(cls.name) in cls._custom_units
):
return unit
return super()._decompose_to_known_units(unit)
return super()._decompose_to_known_units(unit, deprecations)

@classmethod
def _def_custom_unit(cls, unit: str) -> UnitBase:
Expand Down Expand Up @@ -190,10 +195,13 @@ def _format_inline_fraction(

@classmethod
def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: UnitBase,
fraction: bool | Literal["inline", "multiline"] = False,
deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN,
) -> str:
# Remove units that aren't known to the format
unit = cls._decompose_to_known_units(unit)
unit = cls._decompose_to_known_units(unit, deprecations)

if unit.physical_type == "dimensionless" and unit.scale != 1:
raise UnitScaleError(
Expand Down
21 changes: 17 additions & 4 deletions astropy/units/tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Unit,
UnitBase,
UnitParserWarning,
UnitsError,
UnitsWarning,
cds,
dex,
Expand Down Expand Up @@ -356,11 +357,12 @@ class RoundtripBase:
def check_roundtrip(self, unit, output_format=None):
if output_format is None:
output_format = self.format_.name
with warnings.catch_warnings():
warnings.simplefilter("ignore") # Same warning shows up multiple times
s = unit.to_string(output_format)

s = unit.to_string(output_format, deprecations="silent")
if s in self.format_._deprecated_units:
with pytest.raises(UnitsError, match="deprecated"):
unit.to_string(output_format, deprecations="raise")
with pytest.warns(UnitsWarning, match="deprecated"):
assert unit.to_string(output_format) == s
with pytest.warns(UnitsWarning, match="deprecated") as w:
a = Unit(s, format=self.format_)
assert len(w) == 1
Expand Down Expand Up @@ -740,6 +742,17 @@ def test_deprecated_did_you_mean_units():
assert len(w) == 1


def test_invalid_deprecated_units_handling():
with pytest.raises(
ValueError,
match=(
r"^invalid deprecation handling option: 'ignore'\. Valid options are "
r"'silent', 'warn', 'raise'\.$"
),
):
u.erg.to_string(format="vounit", deprecations="ignore")


@pytest.mark.parametrize("string", ["mag(ct/s)", "dB(mW)", "dex(cm s**-2)"])
def test_fits_function(string):
# Function units cannot be written, so ensure they're not parsed either.
Expand Down
4 changes: 4 additions & 0 deletions docs/changes/units/18586.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Some unit formats have deprecated units and converting such units to strings
emits a warning.
The new ``deprecations`` parameter of the unit ``to_string()`` methods allows
silencing those warnings or raising them as errors instead.
Loading