-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Allow controlling warnings when converting deprecated units to strings #18586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
👋 Thank you for your draft pull request! Do you know that you can use |
32ceac9
to
f6b3199
Compare
If automatic replacement is not possible then the deprecated unit is used for | ||
constructing the string and a warning is emitted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to add a code example, but our public API does not directly expose any units suitable for this purpose and parsing a string would emit a different warning. In the unit tests I avoided that warning by going through private API, but in user documentation that is unacceptable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the prettiest, but in principle one could do,
from astropy.units import cds
cds.Crab.to_string(format="ogip")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p.s. It is indeed a bit annoying that u.Unit("Crab", format="ogip", parse_strict="silent")
actually is not silent...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using cds
here would make the example too confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nice cleanup.
---------- | ||
unit : |Unit| | ||
The unit to convert. | ||
deprecations : {"warn", "silent", "raise", "convert"}, optional, keyword-only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are strings permitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is best if these options do not differ needlessly from the parse_strict
options the unit parsing methods have, and those are all strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then are the type annotations correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests and documentation only use strings.
astropy/units/format/base.py
Outdated
if s in cls._deprecated_units and deprecations != DeprecatedUnitAction.SILENT: | ||
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: | ||
if deprecations == DeprecatedUnitAction.CONVERT: | ||
return alternative | ||
msg += f" Suggested: {cls.to_string(alternative)}." | ||
warnings.warn(msg, UnitsWarning) | ||
|
||
if deprecations == DeprecatedUnitAction.RAISE: | ||
raise UnitsError(msg) | ||
if deprecations == DeprecatedUnitAction.WARN: | ||
warnings.warn(msg, UnitsWarning) | ||
elif deprecations == DeprecatedUnitAction.CONVERT: | ||
warnings.warn( | ||
msg + " It cannot be automatically converted.", UnitsWarning | ||
) | ||
else: | ||
valid_options = ", ".join(map(repr, map(str, DeprecatedUnitAction))) | ||
raise ValueError( | ||
f"invalid deprecation handling option: {deprecations!r}. Valid " | ||
f"options are {valid_options}." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if s in cls._deprecated_units and deprecations != DeprecatedUnitAction.SILENT: | |
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: | |
if deprecations == DeprecatedUnitAction.CONVERT: | |
return alternative | |
msg += f" Suggested: {cls.to_string(alternative)}." | |
warnings.warn(msg, UnitsWarning) | |
if deprecations == DeprecatedUnitAction.RAISE: | |
raise UnitsError(msg) | |
if deprecations == DeprecatedUnitAction.WARN: | |
warnings.warn(msg, UnitsWarning) | |
elif deprecations == DeprecatedUnitAction.CONVERT: | |
warnings.warn( | |
msg + " It cannot be automatically converted.", UnitsWarning | |
) | |
else: | |
valid_options = ", ".join(map(repr, map(str, DeprecatedUnitAction))) | |
raise ValueError( | |
f"invalid deprecation handling option: {deprecations!r}. Valid " | |
f"options are {valid_options}." | |
) | |
if s in cls._deprecated_units and deprecations != DeprecatedUnitAction.SILENT: | |
if isinstance(unit := cls._units[s], Unit): | |
if deprecations == DeprecatedUnitAction.CONVERT: | |
return unit.represents | |
msg = ( | |
DEPRECATED_UNIT_MSG.format(s, cls.__name__) | |
+ f" Suggested: {cls.to_string(unit.represents)}." | |
) | |
else: | |
msg = DEPRECATED_UNIT_MSG.format(s, cls.__name__) | |
match DeprecatedUnitAction(deprecations): | |
case DeprecatedUnitAction.RAISE: | |
raise UnitsError(msg) | |
case DeprecatedUnitAction.WARN: | |
warnings.warn(msg, UnitsWarning) | |
case DeprecatedUnitAction.CONVERT: | |
warnings.warn( | |
msg + " It cannot be automatically converted.", UnitsWarning | |
) |
With the following defined elsewhere
DEPRECATED_UNIT_MSG: Final = "The unit {} has been deprecated in the {} standard."
I'm not sure deprecations = DeprecatedUnitAction(deprecations)
is necessary given the type annotation, but the docstring made me put it in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is slightly more compact and avoids assigning to intermediate variables unless needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The creation of msg
in separate branches may be an over-optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all your suggestion would mean the deprecations
value is checked whether or not there are any deprecated units, whereas with my code deprecations
is ignored unless it is actually needed. That is a visible difference in code behavior.
For code readability it is best if things are defined close to where they are used unless doing so would cause code duplication. Unfortunately some of your suggestions move definitions away from usage while also increasing duplication. The claim that the suggested code is more compact is not true because shortening the function here by a few lines is achieved by adding even more lines elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that it's a str enum, can't the parse step be moved anywhere?
Edit: I just did. I moved it to address your first point.
And doesn't _missing_
move things closer to their usage? It's like __contains__
and performs a similar separation of concerns between determining if an action is valid and performing that action. And Enums naturally contain a _missing_
check, which the proposed code doesn't use when raising on an incompatible input. This suggestion customizes _missing_
and uses the Enum mechanism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quite like the match
here, as well as using the StrEnum
(if we're using it at all) to generate the error message. I would not worry at all about performance in this path, where we're already dealing with a deprecated unit, so in an unlikely scenario.
I'd do CONVERT
first, since then alternative is still close by.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this - I really like the overall idea.
My one doubt is about the enum
-- I don't really see the benefit of it, especially because it is not used throughout (and I agree we cannot use it in user-facing parts). I would just stick with strings, which means fewer lines added too.
If automatic replacement is not possible then the deprecated unit is used for | ||
constructing the string and a warning is emitted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the prettiest, but in principle one could do,
from astropy.units import cds
cds.Crab.to_string(format="ogip")
If automatic replacement is not possible then the deprecated unit is used for | ||
constructing the string and a warning is emitted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p.s. It is indeed a bit annoying that u.Unit("Crab", format="ogip", parse_strict="silent")
actually is not silent...
astropy/units/format/base.py
Outdated
if s in cls._deprecated_units and deprecations != DeprecatedUnitAction.SILENT: | ||
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: | ||
if deprecations == DeprecatedUnitAction.CONVERT: | ||
return alternative | ||
msg += f" Suggested: {cls.to_string(alternative)}." | ||
warnings.warn(msg, UnitsWarning) | ||
|
||
if deprecations == DeprecatedUnitAction.RAISE: | ||
raise UnitsError(msg) | ||
if deprecations == DeprecatedUnitAction.WARN: | ||
warnings.warn(msg, UnitsWarning) | ||
elif deprecations == DeprecatedUnitAction.CONVERT: | ||
warnings.warn( | ||
msg + " It cannot be automatically converted.", UnitsWarning | ||
) | ||
else: | ||
valid_options = ", ".join(map(repr, map(str, DeprecatedUnitAction))) | ||
raise ValueError( | ||
f"invalid deprecation handling option: {deprecations!r}. Valid " | ||
f"options are {valid_options}." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quite like the match
here, as well as using the StrEnum
(if we're using it at all) to generate the error message. I would not worry at all about performance in this path, where we're already dealing with a deprecated unit, so in an unlikely scenario.
I'd do CONVERT
first, since then alternative is still close by.
f6b3199
to
d821e67
Compare
The docstrings of |
msg + " It cannot be automatically converted.", UnitsWarning | ||
) | ||
case _: | ||
assert_never(deprecations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM :)
@mhvk had a few points to address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I like the match
. As I write inline, I think it is a place where clarity is more important than performance, so would prefer to keep it simple. But if you prefer not to, that's fine too, so approving now.
astropy/units/format/base.py
Outdated
def _validate_unit( | ||
cls, s: str, deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN | ||
) -> UnitBase: | ||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the match
is working out! But I'd still prefer to forego performance here and just start the match
after the existing
if s in cls._deprecated_units:
msg = "..."
if (alternative := ...):
msg += ...
match DeprecatedUnitAction(deprecations):
case DeprecatedUnitAction.CONVERT:
if alternative:
return alternative
warnings.warn(msg + ...)
case ...
It keeps the code easy to follow, with all logic to the various deprecated unit possibilities in one place; performance really is not a worry here.
It looks like the demonstration of |
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.
The "vounit" and "ogip" formats have deprecated units. It is now possible to instruct the unit formatters to try to replace deprecated units when writing them to strings. If automatic replacement is not possible the deprecated unit is used and a warning is emitted. An alternative would have been to implement a new method in `UnitBase` to replace deprecated units, which could be used like `u.erg.convert_deprecated("vounit").to_string(format="vounit")` but then the user would have to specify the unit format twice and such repetition should be avoided.
d821e67
to
4088611
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for making that last change.
|
||
If automatic replacement is not possible then the deprecated unit is used for | ||
constructing the string and a warning is emitted. | ||
As a last resort, it is possible to silence the warnings:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Late to this party, but calling this as the last resort is a bit overly judgy -- erg and Angstroms will likely stay with us for a while, no need for this snarky comment in the docs.
Description
The
"vounit"
and"ogip"
unit formats have deprecated units and converting them to strings raises warnings that currently cannot be avoided. The first commit introduces a newdeprecations
parameter to theUnitBase
and unit formatterto_string()
methods which allows silencing those warnings or replacing them with errors. This is analogous to theparse_strict
parameter the unit parsing methods have. The default is to warn, which is howastropy
behaves right now, but it is also sensible default behavior.The second commit introduces a fourth
"convert"
option, which tellsastropy
to automatically replace deprecated units. If that is not possible then the deprecated unit is written out and a warning is emitted. In practice, with"vounit"
automatic replacement is always possible and with"ogip"
it is never possible.The four options are implemented as members of a
StrEnum
, but the enum is not exposed to the users yet, nor is it explicitly used in the tests. This implementation demonstrates that we can start using enums internally, so that when we do make them public the amount of code changes will be smaller. Furthermore, by usingStrEnum
we can update the public API without breaking backwards compatibility in practice. The enum is implemented in a separate module so that it (and, in the future, other enums) could be imported without having to worry about import loops.I briefly considered adding a new method to
UnitBase
instead of adding a fourth option for handling deprecated units, but using that design would look something likei.e. the user would have to specify the unit format twice. Such repetition should be avoided, so the
deprecations="convert"
option is better.Closes #18304