Repository navigation
ENH: redesign astropy.io.ascii.Column.type as an enum, improving maintainability and fine-level handling for all expected cases - #20744
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.
|
11c334a to
eb45fdd
Compare
| ) | ||
| except Exception as exc: | ||
| raise ValueError(f"column {col.name!r} failed to convert: {exc}") | ||
| raise ValueError( |
There was a problem hiding this comment.
not strictly related, just a fly-by improvement. Happy to split it out on request
| # bug-for-bug backward compat | ||
| fillval = "0" |
There was a problem hiding this comment.
temporary comment.
Question to reviewers: shouldn't this be
| # bug-for-bug backward compat | |
| fillval = "0" | |
| fillval = "0.0" |
?
There was a problem hiding this comment.
Originally, we used "0" because the same value can be used as a string, int, and float. That's convenient for setting the fill_value because you can universally set the same value for all columns, even for bool columns (since "0" evaluates to a bool value in "if 0").
That's what the comment "bug-for-bug backward compat" means. It might not be the most elegant choice, but it's been a choice for 15 years, so we would need very, very good reasons to change it. "Looks odd" isn't good enough. Would we write this like this today? No. But is that good enough a reason to break backwards compatibility by changing it? Absolutely not.
And (in this and similar comments), it's important to remember that in the most modern use the fill value doesn't matter at all, because you should use a masked table if values are indeed masked by using a fill value.
There was a problem hiding this comment.
That's what the comment "bug-for-bug backward compat" means.
indeed. I wrote this first to raise awareness and ensure it was discussed in review, but it's a perfectly valid option to just keep it there too, if that's your preference.
| # bug-for-bug backward compat | ||
| fillval = "0" |
There was a problem hiding this comment.
also strictly equivalent to the existing code, but suspicious (to me). Do we really want to represent a boolean False as '0', and how is that consistent with BOOL not being considered a subtype to NUMERIC ?
| case core.ColumnType.ANY | core.ColumnType.UNDEFINED: | ||
| raise ValueError |
There was a problem hiding this comment.
this branch is completely new. I believe it is correct to raise in both cases, which wasn't considered before
| case _ as unreachable: | ||
| assert_never(unreachable) |
There was a problem hiding this comment.
thanks to the addition of the previous branch, we can now check at type-check time that all possible cases are covered. In other words, we can check that the match/case tree is exhaustive
| raise TypeError( | ||
| f"could not convert reader output to {cls.__name__} class." | ||
| ) | ||
| ) from exc |
There was a problem hiding this comment.
not strictly related, just a fly-by improvement. Happy to split it out on request
| type does not match ``StrType``, ``FloatType``, or ``IntType``. | ||
| """ | ||
| class ColumnType(Enum): | ||
| ANY = auto() # top type |
There was a problem hiding this comment.
this is the equivalent of NoType in the previous version, but using a conventional name for a top type
| FLOAT = auto() | ||
| INTEGER = auto() | ||
| BOOL = auto() | ||
| UNDEFINED = auto() # bottom type |
There was a problem hiding this comment.
this is the equivalent of AllType in the previous version, with the benefit that it cannot be made inconsistent with the rest of the model, where AllType could break if a type was added to the hierarchy but not made a parent to it.
71db273 to
abba522
Compare
hamogu
left a comment
There was a problem hiding this comment.
Everything in this PR makes sense and everything makes the code better. Still, I think the entire PR should be rejected (with the possible exception of the "from None" and "from exec" in the lines where an error is thrown).
As I detail in one of my answers to specific comments, this module is old and both Python and we as developers have learned things since it was written. There were no "switch" statements, no type hierarchy that put "numeric" on the top and no "from None" for exceptions.
However, reading ascii tables is also one of the top subpackages in astropy by use as far as we know. There are literally thousands of astronomers out there that use it to read their tables, from undergrad students to operation scripts of major space telescopes and somewhere someone relies on the fact wording of an error or the exact (unconventional, user defined) "FloatType". So we need to be very, very careful anytime we change something, even if it seems trivial like changing an error message from "convert FloatType" to "convert Float".
Of course, we want to fix bugs that stop people from using this (such as crashes, not-helpful errors, or MRT tables that the AAS would accept for publications because it's not the format they need), but we need to avoid changes for the sake of change at all costs.
It's not clear what the use case for this PR is beyond "it's better now", which it undeniably is, but how does a user profit enough to warrant breaking things? How does changing a mask value from "0" to "0.0" help a user? Purists might shutter, but "0" in float columns has worked for more than a decade and I don't see a reason why masking float columns with "0" should break in the foreseeable future.
While the old code is not the easiest to read, the improvements are marginal in the new code (because it has to adhere closely to the existing code).
I don't want to close this PR out of hand because the PR comes after careful work from an experienced developer, but without a clear need and obvious user-facing benefits, I'm opposed to changing things.
| assert len(w) == 4 | ||
| for i in range(len(w)): | ||
| assert f"OverflowError converting to FloatType in column col{i + 2}" in str( | ||
| assert f"OverflowError converting to Float in column col{i + 2}" in str( |
There was a problem hiding this comment.
I'm not a big fan of changes, even if it's only an error message, that don't have a clear purpose. The old message was understandable to users, and so is the new one. I get that "FloatType" is not how we typically write things in Python today, but is that really worth the change? This is a 15 year old module and somewhere someone is checking the error message to catch something in their code and this breaks it needlessly.
In other cases, there is a clear improvement that justifies a change (e.g. adding text to tell users how to fix something or in which row or column the error occurs, but here is only saves 4 characters.
Personally, I would revert all changes this and similar error messages that were changes in this PR.
| # bug-for-bug backward compat | ||
| fillval = "0" |
There was a problem hiding this comment.
Originally, we used "0" because the same value can be used as a string, int, and float. That's convenient for setting the fill_value because you can universally set the same value for all columns, even for bool columns (since "0" evaluates to a bool value in "if 0").
That's what the comment "bug-for-bug backward compat" means. It might not be the most elegant choice, but it's been a choice for 15 years, so we would need very, very good reasons to change it. "Looks odd" isn't good enough. Would we write this like this today? No. But is that good enough a reason to break backwards compatibility by changing it? Absolutely not.
And (in this and similar comments), it's important to remember that in the most modern use the fill value doesn't matter at all, because you should use a masked table if values are indeed masked by using a fill value.
|
thanks for your prompt, detailed and honest answer. I'm switching this to draft while I iron out the bits you like least. In particular, I'm reverting changes to error messages and extracting the consensual parts to dedicated PRs. On the question of user impact, I believe that while indirect, it is the end goal here. I should also share my entire plan so far: what I'm actually trying to achieve is to make the
This PR started when I tried to add a type annotation to |
abba522 to
098d38b
Compare
| type does not match ``StrType``, ``FloatType``, or ``IntType``. | ||
| """ | ||
| @property | ||
| def nickname(self) -> str: |
There was a problem hiding this comment.
@hamogu this property is the backward compatible layer I added in response to your concerns regarding error messages
|
I'd like to strongly second what @hamogu said. Basically this is legacy code and despite flaws in the original design it serves the community well. The maintenance burden with the current code has NOT been an issue and I foresee no problems going forward. Thus improving maintainability is not a driver for large-scale code changes. In short, any proposals for making design changes to |
…intainability and fine-level handling for all expected cases2026-10-30 1
098d38b to
f39fdf0
Compare
| converter_type = ColumnType.FLOAT | ||
| case "b": | ||
| converter_type = ColumnType.BOOL | ||
| case "U": |
There was a problem hiding this comment.
@hamogu this is the faithful translation of the existing code but it begs the question: should np.dtype('T') (whose name is 'StringDType128') also match this branch ? This case didn't exist last time this code was edited, so it wasn't a defect then, but arguably it is one now. In any case I figure this defect is very minor and probably inconsequential since this function isn't supposed to be public, but it seems best to address the defect right now than to go through a painful rediscovery process if this ever has any undesirable effect, because it wouldn't be immediately visible
There was a problem hiding this comment.
this comment remains to be discussed even with this PR rejected
@hamogu @taldcroft
There was a problem hiding this comment.
nevermind, it's actually more a problem of test coverage, so I don't expect controversy on this point.
See gh-20762
|
Just to reiterate in case my point didn't make it through: this has nothing to do with modernizing for the sake of it and everything with making the code easier to reason about in order to fix real bugs down the road. The bugs I'm talking about do not have to live in the exact area I'm refactoring here. In fact I hope the whole exercise reduced that chance to ~0. The idea is that if this part is obviously correct, then the mental overhead it takes for someone reasoning about a bigger picture is virtually zero. I'm seeing it as an investment, and that's precisely why I take on this kind of tasks early in the process, before delivering more user-impacting patches, so it has a greater chance to pay off rapidly. |
|
@neutrinoceros - thanks for thinking about In particular, package stability is the highest priority for me. The change to using I also disagree with the need for this highly-impacting change for the sake of long-term maintenance. We have a lot of experience now and the package is stable. The current maintainers are managing without too much trouble and have not considered this a package that needs help. With appropriate use of AI, fixing bugs now is often quite easy and I already have fixes available for many of the existing bugs that I need to review internally. My expectation for the future of Here is the long form discussion about the API break from Claude which I agree with: Public API removed with no deprecation.
This cannot be shimmed cleanly. Enum members can't stand in for classes under |
|
I think @taldcroft and I agree that this PR is unsuitable for Thanks @neutrinoceros for throughly investigating other possibilities and comparing what could be done and what the potential advantages are. It's good to know what we're missing out on and this would a good starting point if we ever decided to do a "ascii 2.0", similar t the units team planning a "units 2.0". But for the current |
I should clarify here: I genuienely had no idea this would be considered impacting. My whole argument was based on the assumption that this code was purely internal. I would not knowingly propose breaking changes for the sake of maintainability. |
|
parts of this PR were just adding type annotations to existing code without changing any API. The process wasn't always obvious and the result is not disrupting anything. Is it okay that I re-open another PR with just these ? |
|
I would also like some feedback on #20744 (comment) which I believes highlights an actual defect of the code that should be addressed even with this particular PR rejected |
Description
Part of #18388
This started as an excuse for a deep dive in the conversion machinery. Now I think it genuinely improves maintainability with two main benefits:
typing.assert_never)Another benefit of this side quest is that I learned a lot about the core of
io.ascii.AI Disclosure
N/A
Merge method