Skip to content

Add script to make type stub for units - #18547

Merged
nstarman merged 2 commits into
astropy:mainfrom
mikez:main
Aug 29, 2025
Merged

nstarman merged 2 commits into
astropy:mainfrom
mikez:main

Conversation

@mikez

@mikez mikez commented Aug 24, 2025 •

Copy link
Copy Markdown
Contributor

Description

This pull request is to address #15808.

Advice is requested as for desired way to trigger this. (/cc @neutrinoceros)

Some options include:

  1. use a GitHub workflow as done for update_astropy_iers_data_pin.py;
  2. run it as part of .pre-commit-config.yaml
  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions

Copy link
Copy Markdown
Contributor

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.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@mikez

mikez commented Aug 24, 2025

Copy link
Copy Markdown
Contributor Author

Note: ruff check seems to be a false positive. There's only one "real" import.

@nstarman nstarman added this to the v7.2.0 milestone Aug 24, 2025
@nstarman
nstarman requested a review from eerovaher August 24, 2025 05:44
@nstarman

Copy link
Copy Markdown
Member

I like the idea to generate type stubs and to tie it into CI in some way.
Things to consider:

  1. do these stubs elide the other imports? I think the contents of __all__ need to be added.
  2. Maybe make this a script that can be called outside of CI?
    This relates to your pre-commit.ci suggestion, though I'm not sure we want it at the pre-commit stage rather than a post-commit.

@mikez

mikez commented Aug 24, 2025 •

Copy link
Copy Markdown
Contributor Author

@nstarman: Thanks for the quick reply.

If I understand your comment correctly: this stub iterates through astropy.units.all, but then makes a selection as for what types to import. This is in the spirit of "good enough". Sorting through the long tail will require some time and this way we get 98% of the types with little effort.
Will happily put it wherever you maintainers think it fits best.

@nstarman

Copy link
Copy Markdown
Member

I think we want to import everything that's in __all__, which defines the public API. In particular what first tripped my notice was Quantity

@mikez

mikez commented Aug 25, 2025 •

Copy link
Copy Markdown
Contributor Author

@nstarman: Thank you clarifying and pointing out Quantity. I reworked the code to handle classes as well. Now only the types functions, functools._lru_cache_wrapper, method, and DataInfoMeta are missing... and, again, I think from experience, good enough is usually a good strategy. It can always be improved upon as the need arises, and will save us development time.

@nstarman

nstarman commented Aug 25, 2025 •

Copy link
Copy Markdown
Member

Great. The generation LGTM.
I think we can figure out the CI of this — including committing the changes — in a followup PR.
But for this one can you add unit tests for the generated pyi file?
Like in Astropy/units/tests use a tempdir and call main and show a few imports.

@eerovaher

Copy link
Copy Markdown
Member

The main objection to #17902 was the size of the generated files, and this pull request must have the same problem.

@nstarman

nstarman commented Aug 25, 2025 •

Copy link
Copy Markdown
Member

I think the difference being the focus on autogen.
In #17902 my objection wasn't wasn't the resulting file size but the initial file size — balking at committing 5k lines that had been generated by an external script, e.g. see #17902 (comment).
I do think we can solve this with autogen + CI (see above comment).
I think the CI bit can be for a different PR, but I'm thinking about ...

  1. As part of CI, before unit/etc tests are run check if astropy/units has been changed. If so run this script. Check the git diff. If different, auto-commit, ignoring any git-ignore. Probably also check that the only committer to the pyi files are GH actions.
  2. Add the pyi files to .gitignore so that only GH Actions can add them to our git history
  3. Expose the script as a [project.scripts] so devs can trigger the stub gen (probably also how GHA would do it). .gitignore ensures this isn't committed.

I think @mikez's script is a good contribution.
If we all like the autogen route, then maybe @eerovaher we merge in your script from #17902 and build the CI stuff to autogen all the necessary stubs.
Ping @mhvk for additional thoughts.

@mikez

mikez commented Aug 25, 2025 •

Copy link
Copy Markdown
Contributor Author

@eerovaher Thanks for the pointer to your commit. I missed that one. I agree with @nstarman here; for maintenance reasons, these files should be auto-generated by a bot – then file sizes aren't an issue. I'm glad we both had the same intuition that generating stub files are a good idea. :)

@nstarman I like your route. CI helps with maintenance, .gitignore protects against humans accidentally committing, and [project.scripts] ensure we or bots can run/test it as needed.

I had two additional thoughts:

  1. We could go the [project.scripts] route before we even add it to CI... that way anybody who wants stubs can generate them today already... and we'll get an opportunity to test-run it in practice.
  2. CI: Checking if astropy/units has changed... would you do this by checking if the prefix of any changed file has astropy/units in it? Or were you envisioning something more involved?

@pllim pllim added typing related to type annotations testing no-changelog-entry-needed labels Aug 25, 2025

@pllim pllim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this PR be converted to draft for now?

How does this tie into dev workflow? The dev docs need to be updated if we proceed.

Thanks!

Comment thread .github/workflows/make_type_stubs.py Outdated
@@ -0,0 +1,77 @@
"""
Simple stub generator. Focussed on `astropy.units` for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Simple stub generator. Focussed on `astropy.units` for now.
Simple stub generator. Focused on `astropy.units` for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

British spelling... will fix. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@pllim If we take the [project.scripts] route, then we can add this right away and think about the CI part at a later stage.

@mikez

mikez commented Aug 25, 2025 •

Copy link
Copy Markdown
Contributor Author

@nstarman If we take the [project.scripts] route, what would be a good home for the script? If so, I'll move it there, update pyproject.toml and .gitignore and then we can discuss CI in another PR.

@mhvk

mhvk commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Thanks for the ping. I like the approach here, and like even more the idea to put this in [project.scripts] or another place where any user can execute it, so that those interested in typing can run with it already and check that things work as expected.

I also really like how this separates concerns: after this PR providing the script, we can add automated tests that ensure that whatever it produces is up to date (probably mostly the unit test that is to be included here), and then find out how best to ensure the .pyi files gets auto-included on pypi (or auto-generated as needed? a bit like .pyc files?).

On the home for the script, given the precedent by the other sub-packages in pyproject.toml, I guess astropy.units.scripts.<name> would seem best. I'd change the name to make_units_type_stubs or just units_type_stubs, because the scripts are all going to be in one place as far as the user can see, so that avoids name collisions.

@mikez

mikez commented Aug 25, 2025

Copy link
Copy Markdown
Contributor Author

@mhvk I like your approach. I moved the script to astropy/units/scripts and renamed it to typestubs_for_units.py. For the pyproject.toml, I called it typestubs-for-units. Just tested it on my machine and it worked great.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Super! To do still is a test. Note also my comment about possible arguments for the script - if we go the route of allowing an alternate path, testing would be even easier.

@mikez

mikez commented Aug 25, 2025 •

Copy link
Copy Markdown
Contributor Author

Super! To do still is a test. Note also my comment about possible arguments for the script - if we go the route of allowing an alternate path, testing would be even easier.

@mhvk What did you have in mind in terms of testing? I'd prefer not to over-complicate this for maintenance reasons.

@eerovaher

Copy link
Copy Markdown
Member

Responding to #18547 (comment)

I think the difference being the focus on autogen. In #17902 my objection wasn't wasn't the resulting file size but the initial file size — balking at committing 5k lines that had been generated by an external script, e.g. see #17902 (comment). I do think we can solve this with autogen + CI (see above comment).

What difference does it make if the stubs are generated in CI or not? The size of the stub files depends on how many objects need to be annotated and that is the same either way.

2. Add the pyi files to `.gitignore` so that only GH Actions can add them to our git history

Git documentation makes it clear that "A gitignore file specifies intentionally untracked files that Git should ignore. Files already tracked by Git are not affected;" In other words if the stubs have been committed then them being listed in .gitignore makes no difference.

Responding to #18547 (comment)

I like the approach here, and like even more the idea to put this in [project.scripts] or another place where any user can execute it, so that those interested in typing can run with it already and check that things work as expected.

The standard way of distributing optional stubs annotations is by publishing them as a stub-only package. I sounds like you are attempting to invent a custom way of distributing optional type stubs, but you haven't justified why the standard way isn't good enough.

we can add automated tests that ensure that whatever it produces is up to date (probably mostly the unit test that is to be included here),

It would be simplest to use stubtest that is bundled with Mypy. But stubtest will not approve astropy unit stubs no matter how they are generated because astropy dynamically inserts entries into the unit definition module __all__ lists that are duplicates under NFKC normalization. Addressing that requires code changes in astropy.

and then find out how best to ensure the .pyi files gets auto-included on pypi (or auto-generated as needed? a bit like .pyc files?).

Why was the size of the stub files a problem for #17902 but not here?

@mikez

mikez commented Aug 25, 2025

Copy link
Copy Markdown
Contributor Author

ruff side-remark: I001 (unsorted-imports); does anyone know why ruff wants to place sys before pathlib and collections? That seems like a weird sorted order for me. To keep the sorting order, I personally use isort here and not ruff.

@mikez

mikez commented Aug 25, 2025

Copy link
Copy Markdown
Contributor Author

@eerovaher All your remarks make sense to me, thank you for looking into this and helping me understand stubs better! Maybe we can split the CI/.gitignore-related issues into a separate discussion, since this PR will only handle adding the script and putting it in pyproject.toml.

The standard way of distributing optional stubs annotations is by publishing them as a stub-only package.

Wonderful remark. Thank you! This seems to be the method chosen by pandas, see here. They also keep some internal stubs for development here.

As far as I understood, this approach can lead to a significant maintenance burden and double-maintenance (since two repos need to be kept in sync). In that respect, I think we'd be clever here to choose a lightweight solution that auto-generates only astropy.units for now so as not to make the same mistake. (Also, as you suggested, use other development strategies like inline annotations and rethinking the __all__ approach down the line so we don't need to generate PYI files at all.)

It would be simplest to use stubtest that is bundled with Mypy.

I see pandas uses that approach for their internal stubs here. However, if I understood you correctly, their stubtests approach won't work here. So we'd need to figure out another clever solution if we want to test this at all. I'd be happy to handle this testing in another PR, so we can give units stubs to everyone who needs them... and if the approach turns out to be the wrong one, it's an easy fix.. just delete one line in pyproject.toml and the file in units/scripts/.

In short: experience has taught me, it's good to have a simple fix for now that's good enough for daily work (for those that want it) and then in parallel work on a more long-term solution that keeps our maintenance burden low.

@mhvk

mhvk commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

My main wish would be not to have auto-generated stubs in the git parts (I'd like to similarly get rid of the auto-generated lextab, parstab files). I mind much less if the stubs are distributed along with the package on pypi, but a separate package is arguably better, so if that is the standard, that's great.

Anyway, as noted by @mikez, the nice part of the present approach is that for this PR, we do not have to worry about the details of how the stubs are distributed. Though obviously we should not do something that precludes standard methods.

@mhvk

mhvk commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

As for tests, maybe spot checks that the types are present and work. @nstarman seems to have had a more concrete idea in #18547 (comment)

mikez added a commit to mikez/astropy that referenced this pull request Aug 28, 2025
@mikez

mikez commented Aug 28, 2025

Copy link
Copy Markdown
Contributor Author

Thank you @eerovaher, for your patience. I didn't understand you at first, but now it clicked.
We could easily fix our current script to include def_unit and generate just __init__.pyi; however, why complicate the script even more, when we can make it easier... by leveraging what the type checker already is good at. :)

I hope, for my health and teeth grinding, we can all agree on this version as a first step. 😅

mikez added a commit to mikez/astropy that referenced this pull request Aug 28, 2025

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me. The test using @eerovaher's example would require adding mypy as an optional dependency, which I think is better done in follow-up.

My remaining question is how to advertise this, while at the same time making clear that this is a temporary solution. My suggestion would be to have changelog and what's-new entries, and add something along the following lines to docs/units/type_hints.rst, at l.77 (before "Type Annotations Module"):

.. _typestubs_for_units:

Script for Creating Unit Type Stubs
===================================

While the main classes in `astropy.units` have type hints, the individual
units do not. These are autogenerated, which makes adding hints more
difficult. In astropy 7.2, we have experimental support for adding
appropriate type hint ``.pyi`` files in your astropy installation, using
the ``typestubs_for_units`` script, which can be called from the
command line.

.. warning:: This script is meant to gain information about whether the
    type hints provided this way are sufficient. Once a better, automatic
    solution has been identified, the script will be removed. In the
    meantime, we welcome feedback.

(I think the text of the what's-new entry could be very similar, and the changelog entry a shortened version of it.)

I'm happy to just push the above to your branch, as long as there is agreement this makes sense. (But let's not let this keep us from getting something good in!)

@mikez

mikez commented Aug 28, 2025

Copy link
Copy Markdown
Contributor Author

@mhvk Thank you, again, for your guidance here. Much appreciate it. I slightly reworded the changelog and added it to the commit.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Super, that text is much better than what I suggested!

I think we still need a formal changelog entry (you'add a file docs/changes/units/18547.feature.rst) as well as a description in docs/whatsnew/7.2.rst -- but maybe hold off just a bit to see what @nstarman and @eerovaher think; I want to be sure that we don't have too much to and fro, and the text you added now to the docs would be the basis of those anyway.

@eerovaher

Copy link
Copy Markdown
Member

Generating type stubs should not be the responsibility of the users and the reason why the three options for distributing type stubs I listed in #18547 (comment) are the only ones worth considering is that those are the three options that don't make type stubs the user's problem. With options 1) or 2) type stubs would be the responsibility of astropy developers and with option 3) they would be the responsibility of the astropy-stubs developers. We decided not to choose option 1) because the type stubs for units are too large and according to our documentation we cannot choose option 2):

Preventing importing at build time
==================================
It is important to make sure that ``setup_package.py`` files do not trigger an
import of the package they are in - so they should be able to be executed without
relying on imports to other parts of the package.

That means a separate astropy-stubs package is the only reasonable method for distributing astropy type stubs. It is self-evident that it is much simpler for astropy to not implement any machinery for generating or testing stubs if astropy-stubs can do it instead. And it is much simpler for astropy-stubs if it is free to do whatever it needs to do without having to worry about compatibility with the astropy machinery. More importantly, from the users point of view installing astropy-stubs is as simple as installing any other package, and if astropy-stubs is specified as a dependency of another package users don't even have to install the stubs explicitly. All astropy would have to do would be to add a sentence or two referring to astropy-stubs in its documentation and possibly the README, and optionally a What's New entry. astropy-stubs can take care of implementing, testing and documenting itself and there is no good reason why astropy should interfere with any of that.

#17902 isn't good enough and this pull request isn't any better. The very best outcome from this pull request would be that it does not cause any confusion, but it already has.

@mikez, if you want to improve astropy annotations then why don't you add inline annotations to astropy/units/quantity.py? Quantity is a very important class, but that file only has a handful of annotations.

@mikez

mikez commented Aug 28, 2025

Copy link
Copy Markdown
Contributor Author

@mikez, if you want to improve astropy annotations then why don't you add inline annotations to astropy/units/quantity.py? Quantity is a very important class, but that file only has a handful of annotations.

@eerovaher I'd like you to hear me differently. My aim was not to improve annotations... my aim was to make using astropy easier for beginners like myself without seeing all those type checker errors on u.deg, u.m and so on. My aim was not to criticize your approach, rather to do a small incremental "good enough" improvements, rather than a perfect solution. I'm grateful for the insights you've given, thank you again, they've made the code better.

@mhvk @nstarman Maybe you can help me take it from here since it seems @eerovaher isn't willing to meet in the middle. I tried my best, but I only have so much energy.

@nstarman
nstarman requested review from mhvk and nstarman August 29, 2025 02:20

@nstarman nstarman left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok. Putting on my astropy.units maintainer hat for a second.
Thank you @mikez for your contribution and patience in this process! We are all invested in improving the user experience for Astropy, e.g. all those annoying type check errors in IDEs that motivated you to make this PR.

It seems that @mhvk and I are in consensus that this PR is good. IMO this is for 3 reasons:

  1. For devs and motivated users it alleviates a pain point.
  2. It gives us a concrete starting point towards our intended end goal: a painless inclusion of in-line typing and type files for units that users shouldn't have to manage themselves.
  3. It's not public API and not scoped as such, leaving us room to iterate and improve. More complete solutions, like a separate stubs library, have their own bureaucratic complications, making this PR an appropriate stopgap.

Thanks again @mikez!

@eerovaher

Copy link
Copy Markdown
Member

Introducing a script that the users are supposed to run to generate the type stubs is fundamentally a bad idea and, independently from the above, the script here does not address any of the shortcomings #17902 had. This pull request would have been very valuable if it had been opened before #17902, but it was instead opened 5 months after #17902 came to a better conclusion.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree that this is only a step towards a better solution, but I think it is a good one, as whatever the solution is, a script like this will be needed. So, I think we should go for it.

As I indicated earlier, I did feel it needed a changelog and what's-new as well, so added those directly. I will approve now, but it would be good if @nstarman had a quick look at my additions before merging.

On the path forward, conceptually, I think there are actually three possibilities:

  1. Generate the stubs upon packaging (it seems that should be possible, though I don't think we have discussed this yet).
  2. Generate them upon installation. Here, our documentation states that setup_package should not import from the package, but that almost certainly had in mind its most common (only?) use within astropy, which is to compile libraries. For those, importing parts is obviously risky or even impossible (since it would require importing the library that is still to be compiled), but it is much less clear there is a problem for units.
  3. Provide a separate type stubs package. This of course is fine too, if there is interest, and would free those interested in typing from being held back by those, like me, who feel typing takes away too much time from improving the code. I note that it is not that difficult to get a new repository set up under astropy.

Finally, @mikez, thanks very much for taking this on. I think between all the wondering about the longer term, the discussion helped improve the script very much, so that it is now in fact a good basis to work from!

]

# ignore deprecation warnings triggered by accessing units in deprecated.py
warnings.simplefilter("ignore", AstropyDeprecationWarning)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This won't make the whole test run globally ignore the warning even for unrelated tests, will it?

@nstarman nstarman Aug 29, 2025 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think so during runtime because typestubs_for_units isn't imported normally.
But it is for tests... so it might have an impact during tests 🤦.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

so it might have an impact during tests

That is no good. Is @mikez able to open a follow-up PR to localize the ignore with context manager somehow? Or just not care about deprecated.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Post mortem review (sorry for taking so long to get to this) I think this new dir has no buisiness being "public" (even if it's considered private by convention). We should mark it as explicitly private before it goes published.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testing typing related to type annotations units

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants