Skip to content

Modernize test infrastructure (Python 3.12+, uv oldestdeps), replace pytz with zoneinfo, fix LocalTimeConstraint - #641

Open
tepickering wants to merge 17 commits into
astropy:mainfrom
tepickering:cleanup
Open

tepickering wants to merge 17 commits into
astropy:mainfrom
tepickering:cleanup

Conversation

@tepickering

@tepickering tepickering commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

This PR fixes several problems in the testing infrastructure, raises the minimum Python version to 3.12, and fixes two long-standing issues. Each commit is self-contained and can be reviewed separately.

Closes #466, closes #601, closes #631.

Testing infrastructure

  • Python 3.12+ requires-python is now >=3.12, matching astropy main, where 3.11 has been dropped ahead of its next release. 3.11 is removed from tox and CI in this PR.
    • numpy 1.23.2 and matplotlib 3.6.0 have no Python 3.12 builds on PyPI, so their minimums are now 1.26.0 and 3.7.3.
    • Read the Docs now builds with Python 3.14.
  • CI reorganized. The default Python is 3.14.
    • Linux runs 3.12 and 3.13 with base dependencies, plus 3.12 with the oldest dependencies.
    • macOS and Windows run 3.14 with base dependencies.
    • Linux 3.14 runs all dependencies, remote data and coverage.
    • An allowed-to-fail job runs Linux with dev dependencies and remote data on Python 3.15 (currently a release candidate, installed by uv). Nightly numpy and astropy install there; matplotlib has no 3.15 nightlies yet, so it uses its latest release until it does.
    • A new job, Linux - threaded Python, base dependencies, runs the tests on free-threaded Python 3.14 (py314t-test). py314t and py315t tox environments are added. They set PYTHON_GIL=0, as astropy's free-threaded environments do: pyerfa doesn't declare free-threading support yet, so importing it would turn the GIL back on with a RuntimeWarning, which filterwarnings = error makes fatal.
    • A new Linux 3.14 job, py314-test-image, runs the image comparison tests with --mpl. It pins matplotlib to 3.11.2, the version the reference images were made with, because rendering changes between releases. Its results/ folder (HTML summary and any diff images) is uploaded as an artifact.
    • Job names no longer contain Python versions. They describe roles instead: "oldest", "intermediate" and "current" Python (current is default_python), plus "latest" for the devdeps job, which moves to a new Python first. For example, Linux - oldest Python, oldest dependencies and macOS - current Python, base dependencies. Required checks are matched by name, so future Python bumps only change the tox envs and default_python, never branch protection. A comment in ci_tests.yml explains the convention.
  • oldestdeps uses uv's lowest resolution (TST: improving reliability of oldestdeps ? #631). tox now uses tox-uv, which tox installs automatically through requires. The oldest-deps environment installs every package at the lowest version allowed, instead of relying on hand-written pins such as the pyparsing==3.2.5 workaround.
    • Minimums for packages astroplan depends on directly come from pyproject.toml.
    • Minimums for packages that are only pulled in by other packages are in oldestdeps-constraints.txt, applied through UV_CONSTRAINT. Each one is the first release after Python 3.12.0 came out, and the rule is written in the file's header.
    • The list is worked out across Linux, macOS and Windows (uv pip compile --universal), because some dependencies are only installed on one platform. For example, keyring pulls in secretstorage and cryptography only on Linux.
    • Testing the real minimums showed several didn't work on Python 3.12. pytest is raised to 7.4.0, and minimums are added for astroquery, pytest-astropy and pytest-mpl. The pytz minimum was also too low, but pytz is now dropped entirely (see below).
    • pip-specific settings are replaced with their uv equivalents: {list_dependencies_command} instead of pip freeze, and UV_EXTRA_INDEX_URL with unsafe-best-match so devdeps can mix nightly and PyPI packages.
  • dev extra. pip install -e ".[dev]" installs the all and test extras plus tox and tox-uv, so the tox environments can be run locally without installing tox separately.
  • No GUI windows during tests. The root conftest.py forces the Agg matplotlib backend. Doc examples that call plt.show() were opening windows when pytest ran outside tox.
  • pytest-mpl baselines. The mpl_image_compare markers now point at baseline_images/. pytest-mpl looks in baseline/ by default, so --mpl could never find the images. test_image_example.png is regenerated for current matplotlib; the only change is font rendering.
  • test_timezone fixed. It plotted at datetime.now(), so no reference image could ever match. It also didn't return its figure and needed the network only to look up Subaru's location. It now uses a fixed time and Subaru's coordinates directly, returns its figure, and has a reference image, so it runs offline in every job.
  • Dead code removed. _mock_remote_data, _unmock_remote_data, EarthLocation_mock, FixedTarget._from_name_mock and _open_shelve were never called.

Replace pytz with zoneinfo (#601)

  • As discussed in Use dateutil.tz over pytz ? #601, timezones given as strings now become zoneinfo.ZoneInfo, and pytz.utc becomes datetime.timezone.utc. pytz is no longer a dependency.
  • tzdata is now a dependency. Windows has no system timezone database, so zoneinfo needs it.
  • pytz timezones supplied by users still work. UTC-to-local conversion uses astimezone(), which is correct for both kinds, and naive datetimes are still passed through localize() when the timezone provides it. The alldeps environments install pytz so this stays tested.
  • API change: when timezone is given as a string, Observer.timezone is now a ZoneInfo object, so it no longer has pytz methods such as localize. This is noted in CHANGES.rst.

Fix LocalTimeConstraint ignoring the local timezone (#466)

  • compute_constraint looked up the observer's timezone, or the timezone attached to min/max, but never used it, so the limits were compared against UTC. This PR converts times with Time.to_datetime(timezone) before comparing. This fix follows the approach proposed by @Lagrang3 in LocalTimeConstraint fixed timezone (#466) #470, which stalled waiting for tests and a changelog entry; they are credited as co-author on that commit.
  • The window is now checked element by element, so time grids of any shape work.
  • A timezone attached to the limits now only selects which timezone to convert to.
  • self.min is no longer overwritten when only max is given.
  • test_local_time_constraint_hawaii_tz previously asserted the buggy behavior. Its time is moved so the expectations only hold with the conversion to local time.
  • New offline tests use the example from LocalTimeConstraint does not consider local time-zone #466, with both zoneinfo and pytz.
  • The periodic tutorial's printed output had never been real output. With the fix, 7 of 100 HD 209458 b transits are observable from APO; with the old code the result was none. The output and a broken import line are corrected.
  • Behavior change: results change for any observer not in UTC.

Testing

All CI jobs have passed on this PR, including the new image comparison job on Linux and the Read the Docs build with Python 3.14. The link check can fail on network problems: one docs example downloads an image from SkyView.

Also run locally with tox:

Environment Result
py312-test-oldestdeps (astropy 6.0.0, numpy 1.26.0, matplotlib 3.7.3, pytest 7.4.0) passes
py312-test, py313-test, py314-test, codestyle pass
py314-test-alldeps-cov, remote data passes
py314-test-devdeps, remote data (astropy 8.1.dev, numpy 2.6.dev) passes
py315-test-devdeps, remote data (Python 3.15.0rc2; astropy 8.1.dev, numpy 2.6.dev, matplotlib 3.11.2) passes
py314t-test, py314t-test-alldeps, py315t-test (free-threaded, PYTHON_GIL=0) pass
py314-test-image (--mpl); also confirmed to fail on a mismatched image passes
build_docs only warning is graphviz dot missing on the local machine; Read the Docs installs graphviz

Branch protection

Because the jobs are renamed, main's required status checks list four names that no longer report, so the PR shows as blocked even though CI passes. A repo admin needs to update the required checks once; with the version-independent names, they shouldn't need changing on future Python bumps. The list of checks to remove, add and keep is in this comment.

This PR also adds a CLAUDE.md with development notes for Claude Code.

🤖 Generated with Claude Code

tepickering and others added 9 commits September 24, 2026 13:23
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
_mock_remote_data, _unmock_remote_data, EarthLocation_mock and
FixedTarget._from_name_mock were never called; the conftest that used
them no longer exists. Network-dependent tests use
@pytest.mark.remote_data instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Doctests in docs/tutorials call plt.show(), which opened (and blocked
on) GUI windows when pytest was run outside tox with an interactive
default backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…line

pytest-mpl defaults to a 'baseline/' directory, but the baselines live
in 'baseline_images/', so --mpl runs could never find them. Point the
markers at the right directory and regenerate the baseline, whose text
rendering no longer matched current matplotlib (RMS 8.9).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Bump requires-python to >=3.12 to match astropy; drop 3.11 and add
  3.14 to the tox envlist.
- Raise numpy/matplotlib minimums (and oldestdeps pins) to 1.26.0 and
  3.7.3, the oldest releases with Python 3.12 wheels.
- CI: default Python 3.14; base-dependency runs on Linux 3.12/3.13 and
  macOS/Windows 3.14; oldest deps on 3.12; all deps + remote data +
  coverage on 3.14; allowed-failure devdeps + remote data on 3.14.
- Build docs on RTD with Python 3.14.
- Ignore setuptools_scm-generated astroplan/_version.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use tox-uv so the oldestdeps environments install every dependency at
the lowest version allowed by the declared lower bounds, rather than
hand-maintained pins that break when an unpinned transitive dependency
(e.g. pyparsing) releases. Transitive lower bounds live in
oldestdeps-constraints.txt, applied via UV_CONSTRAINT.

Testing the real lower bounds surfaced floors that don't work on
Python 3.12, so raise pytz to 2023.3.post1 and pytest to 7.4.0, and add
lower bounds for astroquery, pytest-astropy and pytest-mpl.

Also switch pip-specific settings to their uv equivalents: use tox's
list_dependencies_command instead of pip freeze, and UV_EXTRA_INDEX_URL
with pip-like index strategy for devdeps.

See astropy#631.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pytz is only needed on Python < 3.9; use zoneinfo.ZoneInfo for string
timezones and datetime.timezone.utc for UTC. Depend on tzdata so
timezone data is available on platforms without a system database
(e.g. Windows).

Timezones created with pytz are still accepted: UTC->local conversion
uses astimezone(), which is correct for both, and naive datetimes are
attached with localize() when the timezone provides it. The alldeps
environments install pytz so this remains tested.

Closes astropy#601.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
compute_constraint looked up the observer's (or the limits') timezone
but then compared the limits against UTC times. Convert times to that
timezone with Time.to_datetime before comparing, as proposed in astropy#470.

Also evaluate the window element-wise so any shape of time grid works
(replacing a bare except fallback), compare naive wall-clock limits so
a tzinfo on min/max only selects the timezone, and stop overwriting
self.min when only max is given.

The Hawaii regression test previously encoded the buggy behaviour; move
its time so the expectations only hold with local-time conversion, and
add offline tests (zoneinfo and pytz) based on the issue's example. The
periodic tutorial's example output was stale: with the fix, 7 of 100
HD 209458 b transits are observable from APO rather than none.

Closes astropy#466.

Co-authored-by: Lagrang3 <32647090+Lagrang3@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 78.05%. Comparing base (7b1c154) to head (5310e3c).

Files with missing lines Patch % Lines
astroplan/plots/time_dependent.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #641      +/-   ##
==========================================
+ Coverage   76.57%   78.05%   +1.48%     
==========================================
  Files          14       14              
  Lines        1844     1800      -44     
==========================================
- Hits         1412     1405       -7     
+ Misses        432      395      -37     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The transitive lower bounds were generated on macOS, so Linux-only
dependencies (keyring -> secretstorage -> cryptography) had none and
uv's lowest resolution picked cryptography 2.0, which fails to build on
Python 3.12. Regenerate the list from a universal resolution, adding
Linux- and Windows-only packages, and drop entries no longer reached.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tepickering

tepickering commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Maintainers: branch protection needs updating before this can merge.

This PR renames the CI jobs, and main's branch protection still requires some of them by their old names. GitHub keeps waiting for checks that will never report, so the PR shows as blocked.

The new names describe roles rather than Python versions ("oldest", "current" = default_python, "threaded" = its free-threaded build, and "latest" for devdeps, which moves to a new Python first). Future Python bumps won't rename jobs, so this should be the last time the required checks need updating for that reason.

Could an admin update the required status checks (Settings → Branches → main)?

Remove (old names that no longer report):

  • ci_tests / OS X - Python 3.13 with minimal dependencies
  • ci_tests / Windows - Python 3.11 with all optional dependencies
  • ci_tests / Python 3.12 with remote data, all dependencies, and coverage
  • ci_tests / Python 3.11 with oldest version of key dependencies

Add:

New required check Replaces
ci_tests / macOS - current Python, base dependencies OS X - Python 3.13 with minimal dependencies
ci_tests / Windows - current Python, base dependencies Windows - Python 3.11 with all optional dependencies
ci_tests / Linux - current Python, all dependencies, remote data, coverage Python 3.12 with remote data, all dependencies, and coverage
ci_tests / Linux - oldest Python, oldest dependencies Python 3.11 with oldest version of key dependencies
ci_tests / Linux - oldest Python, base dependencies (new job)
ci_tests / Linux - current Python, image comparison tests (new job)
ci_tests / Linux - threaded Python, base dependencies (new job, free-threaded Python)

Keep as they are: ci_tests / Code style checks, build_and_publish / Build source and wheel distribution and docs/readthedocs.org:astroplan.

I'd suggest not requiring Linux - intermediate Python, base dependencies (its meaning shifts as versions change), the allowed-failure devdeps job, or Link checks. The link check depends on external sites and on a SkyView download in the docs, and it can fail on network problems.

The two skipped "report overall test coverage" jobs are expected. They're also skipped on main, because this repo reports coverage through Codecov rather than coverage: github.

tepickering and others added 6 commits September 24, 2026 15:03
It plotted at datetime.now(), so no baseline could ever match, never
returned its figure, and needed remote data only to look up Subaru's
location. Use a fixed time and explicit coordinates, return the figure,
and add a baseline image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add an 'image' tox factor that pins matplotlib to the version the
baseline images were generated with (rendering changes between
releases) and runs only the mpl_image_compare tests with --mpl,
writing an HTML summary to results/. Run it in CI as
py314-test-image and upload results/ as an artifact.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… notes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Job names are required status checks on main, so versioned names force
an admin to update branch protection on every Python bump. Name jobs by
role instead: oldest/intermediate/current Python (current being
default_python), and latest for devdeps, which moves to a new Python
first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Python 3.15 release candidates are available through uv, so move the
allowed-failure devdeps job (the "latest Python" job) to 3.15. Nightly
numpy and astropy install there; matplotlib has no 3.15 nightlies yet
and falls back to its latest release until it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add py314t and py315t tox envs and a "Linux - threaded Python, base
dependencies" CI job on py314t. Set PYTHON_GIL=0 there, as astropy does,
since pyerfa doesn't declare free-threading support yet and importing it
would otherwise re-enable the GIL with a RuntimeWarning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Running the tox environments locally needs tox and tox-uv, which no extra
installed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TST: improving reliability of oldestdeps ? Use dateutil.tz over pytz ? LocalTimeConstraint does not consider local time-zone

1 participant