Skip to content

TYP: set up typechecking for units with basedpyright - #17918

Closed
neutrinoceros wants to merge 11 commits into
astropy:mainfrom
neutrinoceros:typ/configure_basepyright
Closed

neutrinoceros wants to merge 11 commits into
astropy:mainfrom
neutrinoceros:typ/configure_basepyright

Conversation

@neutrinoceros

Copy link
Copy Markdown
Contributor

Description

Close #16312

I propose using basedpyright as our type checker from the get go since it's been blessed by numpy devs, contrary to mypy.
Here I'm just laying out a fundation to build upon: a basic configuration with CI, outputing a reduced and hopefully manageable number of errors. The intention isn't to address said errors here but to provide a shared baseline for what to tackle next. This PR shouldn't be merged until we converge to a state where no errors are reported (either because they are fixed, or explicitly ignored, albeit temporarily).

notes

  • I'm using a separate config file instead of pyproject.toml, although I note that in this instance this means no comments can be used (not a feature in .json)
  • setting basic type checking mode (matching pyright's default) instead of recommended (basedpyight's default) because the number of errors and warnings is already in the thousands. Using recommended would add about 200 errors and 20k warnings...
  • baseline (no configuration, other than selecting astropy.units): 5752 errors, 55 warnings, 0 notes
  • Add stub files with unit definitions #17902 would already improve the situation by a lot: 4401 errors, 60 warnings, 0 notes
  • (temporarily) excluding tests reduces the number of errors to ~800
  • (temporarily) turning off a selection of errors (e.g. reportAttributeAccessIssue) makes the baseline much more manageable (less than 200 errors)

I haven't managed to properly configure tox so far; at the time of opening it seems that pyright's config file is completely ignored when run from tox. As a results, the number of errors reported is 2 orders of magnitudes beyond what I just called "manageable". I'm still opening as a draft to see if CI is properly set up.

  • 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.

@neutrinoceros neutrinoceros added this to the v7.1.0 milestone Mar 20, 2025
@neutrinoceros neutrinoceros added dev-automation typing related to type annotations labels Mar 20, 2025
@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.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@neutrinoceros neutrinoceros changed the title TYP: set up typechecking for \units\ with \basedpyright\ TYP: set up typechecking for units with basedpyright Mar 20, 2025
@neutrinoceros neutrinoceros mentioned this pull request Mar 20, 2025
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Ok, CI appears to fail as expected (good) with a whooping 27706 errors. Now the one issue is to fix tox's behavior.
At the moment, I have no clue why basedpyright ignores my configuration file only when run through tox. I will try to research it more, but any help would be appreciated.

Also limit it to a set of astropy/units/format files that actually
are types correctly (for "basic" mode).
@mhvk

mhvk commented Mar 20, 2025

Copy link
Copy Markdown
Contributor

OK, I found a fix - pushing up shortly

Comment thread pyrightconfig.json
{
"typeCheckingMode": "basic",
"include": [
"astropy/units/format"

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 thought we'd give it a set of files that actually work for this first test.

Comment thread tox.ini
changedir = {tox_root} # restore default
dependency_groups = typecheck
commands =
basedpyright --stats

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.

Added stats here so one gets at least an idea of how many files are actually checked...

Comment thread pyrightconfig.json
@@ -0,0 +1,17 @@
{
"typeCheckingMode": "basic",

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 guess a worry with this is that I think we may want different modes for different parts of astropy...

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.

We could have more than one configuration file if it comes to that, but it's a long shot.

@mhvk

mhvk commented Mar 20, 2025

Copy link
Copy Markdown
Contributor

Hmm, weird, things passed locally before I pushed, but I cannot seem to reproduce that any more. My general worry about the configuration is that it seems very hard to do it piece-by-piece -- and in particular to move towards better typing incrementally at different speeds/modes in different parts.

Comment thread pyrightconfig.json Outdated
],
"executionEnvironments": [
{
"root": "astropy/units",

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.

the idea here was to explicitly create subpackage-level settings. I'm not sure why you changed it @mhvk, can you explain ?

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 thought I just got errors if I left it in-place, but will admit I was slightly randomly changing things...

Anyway, I just checked and that is actually not a problem, so fine to revert that change! The one question then is how to do parts of a package, since the include and exclude seem to be astropy wide rather than per execution environment...

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.

I think for now we'll have to share include and exclude globally. If it becomes a problem, we might consider having specialized configuration file per-subpackage, but setting this up will be more involved so it's probably best to not do it until it's actually needed ?

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

thanks ! I also cannot seem to reproduce the 14 remaining errors locally, but it's not the only difference I'm seeing:
CI reports:

Total files parsed and bound: 267
Total files checked: 16

while locally, I have

Total files parsed and bound: 123
Total files checked: 16

@mhvk

mhvk commented Mar 20, 2025

Copy link
Copy Markdown
Contributor

About the number of errors: weird, indeed, since I now got 0 errors again too.

... experimenting ...

It seems to depend on whether one uses tox with --develop (which I usually do, but not always):

tox -e typecheck
# 14 errors, 0 warnings, 0 notes
git clean -fxd
tox -e typecheck --develop
# 0 errors, 0 warnings, 0 notes

Note that if I remove the vounit exclusion, I do get errors, so it is not like it doesn't do a thing.

Given that without --develop, most of the errors are not about the code in astropy/units/format, but rather about classproperty (which comes from astropy.utils), could it be that in develop mode, checks are only done for the files themselves, not at anything imported? That may actually be a sane thing to start with...

@neutrinoceros

neutrinoceros commented Mar 21, 2025 •

Copy link
Copy Markdown
Contributor Author

Nice catch, I didn't think of playing with editable installs at all.

could it be that in develop mode, checks are only done for the files themselves, not at anything imported?

If I'm reading basedpyright's documentation right, this is not a feature; rather, the way our editable installs are setup slightly breaks static analysis, so I don't think we should rely on it.
That said, if we want to establish a baseline to primarily guard against new errors, maybe we could use https://docs.basedpyright.com/latest/benefits-over-pyright/baseline/ ? (mostly mentioning it here in passing, but I think this goes beyond the intentionally narrow scope of the present PR)

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

turning off reportCallIssue for now, which should leave use with only 5 errors. Hopefully that's low enough that we can reason about each one individually and come up with solutions.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

I fixed one of these 4 of 5 errors. Here's the remaining one:

/Users/clm/dev/astropy-project/coordinated/astropy/astropy/units/format/base.py
  /Users/clm/dev/astropy-project/coordinated/astropy/astropy/units/format/base.py:291:58 - error: Argument of type "str | Any | CompositeUnit | Unknown | UnitBase | UnrecognizedUnit | <subclass of Unit and StructuredUnit> | StructuredUnit" cannot be assigned to parameter "unit" of type "CompositeUnit | NamedUnit" in function "_decompose_to_known_units"
    Type "str | Any | CompositeUnit | Unknown | UnitBase | UnrecognizedUnit | <subclass of Unit and StructuredUnit> | StructuredUnit" is not assignable to type "CompositeUnit | NamedUnit"
      Type "StructuredUnit" is not assignable to type "CompositeUnit | NamedUnit"
        "StructuredUnit" is not assignable to "CompositeUnit"
        "StructuredUnit" is not assignable to "NamedUnit" (reportArgumentType)
1 error, 0 warnings, 0 notes

the problem is, I can't find where the _represents attribute's type is annotated or narrowed, so I don't know how to address this yet.

def to_string(
cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = False
cls,
unit: CompositeUnit | NamedUnit,

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 parameter makes the signature inconsistent with Base.to_string().

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.

Much more importantly, the updated signature is inconsistent with UnitBase.to_string(). So this change is not solving a type error, it's just pushing the error to astropy.units.core, but you can't tell that because you are configuring the type checker to ignore that module 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.

Hum, this seems like a strong argument to re-expand the initial target to the whole units subpackage. What do you think, @mhvk ?

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.

Indeed, perhaps we should go back to astropy.units as a whole. Though that beats my goal here of making it small enough that we can actually get some CI going in a short timeframe. (I quickly checked whether something else might work, but as of now, not even astropy/units/utils.py is OK...)

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.

How about starting with stats then (#16562) ?

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.

Yes, for settting it up, anything that passes is good enough.

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.

Running mypy --follow-imports silent astropy/stats/ reports 471 errors. The exact number might be different if I'd install scipy-stubs, but even then I wouldn't get my hopes up. My understanding is that in stats type information from docstrings was rewritten as type annotations, but there were no attempts to ensure that the resulting annotations were correct, or even self-consistent, or even valid.

Running mypy --follow-imports silent astropy/config/*.py doesn't report any errors, but not everything in config is annotated and Mypy skips unannotated functions, so basedpyright might report many errors even in config.

) -> str:
# Remove units that aren't known to the format
unit = cls._decompose_to_known_units(unit)
base_unit = cls._decompose_to_known_units(unit)

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.

Why do you have to rename this?

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.

The error that is solved this way is:

  /data/mhvk/packages/astropy/astropy/units/format/cds.py:281:46 - error: Argument of type "UnitBase" cannot be assigned to parameter "unit" of type "CompositeUnit | NamedUnit" in function "_decompose_to_known_units"
    Type "UnitBase" is not assignable to type "CompositeUnit | NamedUnit"
      "UnitBase" is not assignable to "CompositeUnit"
      "UnitBase" is not assignable to "NamedUnit" (reportArgumentType)
/data/mhvk/packages/astropy/astropy/units/format/fits.py

I just tried and this can be solved also by changing

diff --git a/astropy/units/format/base.py b/astropy/units/format/base.py
index 648cc630be..82e6dd9441 100644
--- a/astropy/units/format/base.py
+++ b/astropy/units/format/base.py
@@ -255,7 +255,7 @@ class _ParsingFormatMixin:
         return did_you_mean(unit, cls._units, fix=cls._fix_deprecated)
 
     @classmethod
-    def _validate_unit(cls, unit: str, detailed_exception: bool = True) -> UnitBase:
+    def _validate_unit(cls, unit: str, detailed_exception: bool = True) -> CompositeUnit | NamedUnit:
         try:
             return cls._units[unit]
         except KeyError:
@@ -271,7 +271,7 @@ class _ParsingFormatMixin:
         )
 
     @classmethod
-    def _decompose_to_known_units(cls, unit: CompositeUnit | NamedUnit) -> UnitBase:
+    def _decompose_to_known_units(cls, unit: CompositeUnit | NamedUnit) -> CompositeUnit | NamedUnit:
         """
         Partially decomposes a unit so it is only composed of units that
         are "known" to a given format.

This probably is better.

It does make one wonder if we should have an alias for CompositeUnit | NamedUnit as basically being the only two subclasses of UnitBase that will end up existing... Or, perhaps conversely, loosen the types elsewhere.

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.

More to the point here, I personally prefer for this PR to make, as far as possible, only changes to the typing, not to the code.

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.

Given #17918 (comment), perhaps the safer thing to do here is to broaden the typing 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.

More to the point here, I personally prefer for this PR to make, as far as possible, only changes to the typing, not to the code.

me too. I'm treating the draft stage of the PR as a sandbox where all changes are allowed, but plan to actually split them out and have them merged before this one.

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.

This probably is better.

I would argue that we should avoid type unions in return annotations as much as possible, as it usually moves the difficulty to call sites.

It does make one wonder if we should have an alias for CompositeUnit | NamedUnit as basically being the only two subclasses of UnitBase that will end up existing..

This would make sense indeed, but maybe a protocol would be better. What's the shared interface between these two that is not part of UnitBase ?

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.

Actually, looking more closely, I admit being confused why we did the NamedUnit | CompositeUnit combo: basically, UnitBase explicitly states it is meant to be an abstract base class, though it doesn't inherit from that (no surprise, didn't exist when this code was written!). NamedUnit is, effectively, also an abstract base class, for all units that have a name. But it doesn't define some of the attributes required to have a string representation, those are defined in its non-abstract subclasses, IrreducibleUnit, UnrecognizedUnit, Unit itself, and PrefixUnit. And CompositeUnit is for composites without a name.

It may make most sense to broaden the type back to UnitBase, at least just to get going.

# Can't use np.log10 here, because p[0] may be a Python long.
if math.log10(unit.scale) % 1.0 != 0.0:
if (
not isinstance(base_unit.scale, float)

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.

Have you checked if the OGIP format even allows complex scales?

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 think for this PR perhaps just ensuring that a warning is raised if the scale is not float would be appropriate. It is a good question, though, whether it shouldn't be an error...

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.

I'm taking the existing warning at face value here: if the scale needs to be a power of 10, it cannot be complex.

@mhvk

mhvk commented Mar 21, 2025

Copy link
Copy Markdown
Contributor

could it be that in develop mode, checks are only done for the files themselves, not at anything imported?

If I'm reading basedpyright's documentation right, this is not a feature; rather, the way our editable installs are setup slightly breaks static analysis, so I don't think we should rely on it.

That's sad! It is so darn slow to work via a non-editable install...

@mhvk

mhvk commented Mar 21, 2025

Copy link
Copy Markdown
Contributor

@neutrinoceros - I can get rid of the last problem with a clarification of .represents -- I pushed it directly to your branch.

@eerovaher

Copy link
Copy Markdown
Member

The problem is that units is not type-checkable. This is because of the structure of the code and it can't be addressed just by tweaking a few annotations here and there. The only type errors that can easily be fixed by only updating annotations are the errors about missing units that are generated by def_unit() at runtime, which can be solved by introducing stub files for the unit definition modules. In other cases the code should be updated, but often it's not clear how to update the code without breaking public API.

In order to type check units in CI you would have to turn off so many rules that it is not clear you would be type checking anything.

@mhvk

mhvk commented Mar 25, 2025

Copy link
Copy Markdown
Contributor

@eerovaher - that is a sad statement, but also fine - happy to focus more on the code than on its annotations.

I would note, though, that from a user perspective, I don't think there are that many backward compatibility issues. In the end, at least in the rest of astropy, almost all use of units is indirect, through Quantity.

@mhvk mhvk mentioned this pull request Mar 25, 2025
1 task done
@astrofrog astrofrog modified the milestones: v7.1.0, v7.2.0 Apr 28, 2025
@github-actions github-actions Bot added the Close? Tell stale bot that this issue/PR is stale label Aug 19, 2025
@github-actions

Copy link
Copy Markdown
Contributor

Hi humans 👋 - this pull request hasn't had any new commits for approximately 4 months. I plan to close this in 30 days if the pull request doesn't have any new commits by then.

In lieu of a stalled pull request, please consider closing this and open an issue instead if a reminder is needed to revisit in the future. Maintainers may also choose to add keep-open label to keep this PR open but it is discouraged unless absolutely necessary.

If this PR still needs to be reviewed, as an author, you can rebase it to reset the clock.

If you believe I commented on this pull request incorrectly, please report this here.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

for what it's worth I'm not convinced this PR is worth keeping around; ty (astral's new, alpha type checker) looks much more promising to me for type checking large code bases progressively with minimal hassle. Hopefully we can try it out soon when it reaches beta (supposedly, in a couple month)

@github-actions github-actions Bot added the closed-by-bot Closed by stale bot label Sep 19, 2025
@github-actions

Copy link
Copy Markdown
Contributor

I'm going to close this pull request as per my previous message. If you think what is being added/fixed here is still important, please remember to open an issue to keep track of it. Thanks!

If this is the first time I am commenting on this issue, or if you believe I closed this issue incorrectly, please report this here.

@github-actions github-actions Bot closed this Sep 19, 2025
@eerovaher eerovaher mentioned this pull request Dec 10, 2025
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Close? Tell stale bot that this issue/PR is stale closed-by-bot Closed by stale bot dev-automation installation testing typing related to type annotations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants