Repository navigation
Avoid implicit bytes/text coercion in tables and metadata - #20599
Conversation
|
Hi 👋 and thank you for your contribution! 🙏 This pull request is being closed automatically - but don't worry, this is not the end, and we may re-open it. For some background, we have recently started seeing a rapid increase in the number of pull requests opened. Some of these are from good-faith humans, but some of which are from autonomous LLM agents or humans using LLMs who do not have a genuine understanding of, or interest in, the project. We are therefore auto-closing pull requests from new contributors, but if you are a good-faith human, we want to make sure your pull request gets considered! So if you would like us to re-open your pull request so that it gets reviewed, you need to do two things:
The second step is important, as we may otherwise miss notifications about this pull request. To be clear, our AI policy does allow the use of LLMs as part of contributions, but we need to see authentic engagement and understanding from humans making the contribution. Notes for maintainersIf you know this is a genuine user and the PR is is compliance with the AI policy, you may re-open this straight away. After two successful merged pull requests, the user will be added to the allowlist and future PRs will no longer be closed automatically. This user has not made any pull requests to this repository prior to this one. In addition, here are some statistics on the user's activity on GitHub:
|
|
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.
|
|
Ping @mhvk as well |
|
Hmm, funny that the first encounter I have with the auto-close bot is you... |
|
ping @taldcroft, as it is more up your alley. This is part of trying to make casting between bytes and strings more logical (starting with a deprecation). |
|
@ngoldbaum - thanks! This looks generally good but I'll need some time to get through it. The one thing that you can address now are getting proper towncrier changelog fragments for the changes instead of the text-type-bugfix.rst files. Those changelogs should be first aimed at users and describe changes that might affect the behavior of existing code (and not so much the implementation details). This includes the obvious bugfix one of not raising Also, are there any performance impacts? |
pllim
left a comment
There was a problem hiding this comment.
We usually backport numpy compatibility changes, but will this be too disruptive for v8.0.x or v7.2.x ? Max-pin numpy on them is also an option if we choose not to backport. Hope you all can advise. Thanks!
There was a problem hiding this comment.
You need to rename this fragment as per https://github.com/astropy/astropy/blob/main/docs/changes/README.rst
I'm not planning to change any behavior in NumPy, except optionally, via an env variable, for testing. So I'd say "do nothing" and let old astropy releases trigger deprecation warnings in new NumPy in a few spots is fine. The warning will also only trigger if someone really is mixing strings and bytes, which they should be able to work around by explicitly encoding or decoding data. If you're really nervous I could make the NumPy warning be a PendingDeprecationWarning, but basically no one sees those. |
I agree that is not very useful. NumPy should do what it thinks is sensible and we can adapt here one way or another. I guess it was more of a question or @taldcroft or @mhvk in terms of astropy maintenance itself. :) |
I applied the |
|
I've updated the changelogs to cover an existing The patch now preserves existing encoding behavior, dtypes, and join matching rather than assuming UTF-8 encoding. The benchmark run on CI I think shows noise in the Quantity benchmarks. However some targeted microbenchmarks did identify some slowdowns. I found opportunities to use some compiled helpers to buy back some performance. Local benchmarks against main show faster dataframe imports and byte-column operations enabled by the new Cython helper. I now think there are no performance regressions. |
|
|
||
|
|
||
| def _get_string_dtype(value): | ||
| """Identify flat bytes-only or Unicode sequences without promotion.""" |
There was a problem hiding this comment.
I could deal with nested sequences too but thought those were too niche for the additional complexity.
There was a problem hiding this comment.
how much additional complexity would it take to just detect them and emit a clear NotImplementedError ?
There was a problem hiding this comment.
We'd have to add the additional complexity (~25 lines of code here) to detect them and raise the error. Also nested all-bytes or all-strings sequences are fine and need to continue to work, the only possible issue is nested sequences that mix bytes and strings, which I think are a pretty niche thing that aren't worth that extra complexity to detect.
There was a problem hiding this comment.
Fair enough. Thanks !
|
devdeps job is green now, so I think this is ready for @taldcroft to re-review. 🙏 |
neutrinoceros
left a comment
There was a problem hiding this comment.
Thank you Nathan, this looks amazing. I have a couple, hopefully minor questions.
| # FITS does not perfectly preserve dtype: byte order can change, and | ||
| # unicode gets stored as bytes. So, we just check safe casting, to | ||
| # ensure we do not, e.g., accidentally change integer to float, etc. | ||
| if a1.names is not None: |
There was a problem hiding this comment.
Note to self there are actually 5 versions of this assert function, only 2 of which are edited in this PR. I checked that the change doesn't make sense for the other 3.
I have a draft PR trying to unify them that's been blocked for a while, but this gives motivation to revive it.
| @pytest.mark.parametrize("dtype", [None, "S", "U", object]) | ||
| def test_masked_multidim_as_list(self, dtype): | ||
| data = np.ma.MaskedArray([1, 2], mask=[True, False], dtype=dtype) | ||
| c = table.MaskedColumn([data], dtype=dtype) |
There was a problem hiding this comment.
shouldn't we check that dtype is implicitly conserved instead of forcing it explicitly here ?
There was a problem hiding this comment.
I'll expand this test a bit to test table.MaskedColumn([data]) as well.
There was a problem hiding this comment.
I read the existing conversation about not backporting the entire PR, which seems sensible, but I want to ask wether it'd be possible to backport just this part ?
|
|
||
|
|
||
| def _get_string_dtype(value): | ||
| """Identify flat bytes-only or Unicode sequences without promotion.""" |
There was a problem hiding this comment.
how much additional complexity would it take to just detect them and emit a clear NotImplementedError ?
| not isinstance(item, (int, float, complex, np.number)) | ||
| and not isinstance(item, np.bool_) |
There was a problem hiding this comment.
I don't understand the point of making two calls to isinstance here when one would suffice. Am I missing anything ?
There was a problem hiding this comment.
you're right, I will consolidate these.
| if fill is None: | ||
| fill = "" if isinstance(value, str) else b"" | ||
| elif legacy_pandas: | ||
| if value is not nan: |
There was a problem hiding this comment.
I didn't know that numpy.nan was a singleton ! Just to make sure I understand this correctly, does numpy only use a single NaN value across multiple dtypes ?
There was a problem hiding this comment.
it's not a singleton:
>>> np.nan is float(float('nan'))
False
However, this is a pre-existing bug in this code path:
astropy/astropy/table/_dataframes.py
Lines 531 to 533 in 0bfd49c
Do you want me to address this issue here? Or is just a followup issue ok?
There was a problem hiding this comment.
I see. I think it's okay to address here. table._dataframes is very new so I don't think we need an opt-in bug-for-bug legacy path at all.
There was a problem hiding this comment.
the diff is kinda big and I don't want to hold off merging this on reviewing another big chunk of code, so I'm going to open an issue I think.
There was a problem hiding this comment.
Yes, I'd like to get this merged basically like it is.
|
@ngoldbaum - can you use Claude to add more complete NumpyDoc-style docstrings and add some code comments? This is my preference for table code (especially now that it is so easy to generate). This will help me and future maintainers read the code. |
Happy to do that. It's not my personal preference: I find it a lot harder to read heavily commented code. |
this has been my opinion too for well before LLMs were a thing, but if their popularity actively incites devs to add comments that no-one wants to write, it's going to get even harder to want to read them... |
Yeah, I noticed that in another numpy PR. I'm just the opposite. 😄 |
|
@ngoldbaum - I've reviewed this and understood all the bits and it looks basically good. The only thing are the docstrings and comments, and the comment from @neutrinoceros about the separate |
|
The comments are more concise now, let me know if you'd like another pass on them. |
|
@ngoldbaum - it looks like you merged main into this branch, which is not the "astropy way". Can you remove |
|
Sorry! Thanks for dealing with me fumbling along... |
f7815dc to
145065c
Compare
taldcroft
left a comment
There was a problem hiding this comment.
Thanks @ngoldbaum, great work!
Description
This pull request addresses implicit NumPy bytes/text conversions in
astropy.tableandastropy.utils.metadata, exposed by the pending NumPy PR numpy/numpy#32757. I have a draft NumPy change that deprecates bytes/text promotion and safe/same-kind casts and adds a newNPY_COERCION_POLICY=strictmode for downstream testing. I used it to check astropy and found some issues.Here is how Astropy
mainand this PR compare (strictis the policy from numpy/numpy#32757):I've made the following changes in Astropy:
Table.from_pandasandTable.from_dfuse a matching bytes or text placeholder for missing values. This also fixes the pandas path used by ECSV reading.metadata_conflictssetting.I also benchmarked and optimized these paths against main. With released NumPy and 100,000 elements, dataframe imports with missing strings take 24–28% less time. Byte-column construction from a list of bytes improves from 8.86 to 2.80 ms, assignment from 13.94 to 7.94 ms, and comparison from 13.47 to 7.49 ms. Text-list performance is essentially unchanged. I didn't identify any significant regressions attributable to this patch in the Astropy benchmarks I investigated or the targeted synthetic benchmarks.
AI Disclosure
OpenAI Codex (GPT-6 Astra) assisted with the implementation, tests, changelog entries, downstream investigation, performance measurements, and this description. Those portions include content produced in a session iterating with an AI model. Additional AI review used Claude Opus 5.5.
Merge method