Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ultraplot/*.pyi linguist-generated=true
ultraplot/**/*.pyi linguist-generated=true
52 changes: 51 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
- 'environment.yml'
- '.github/workflows/**'
- 'tools/ci/**'
- 'tools/generate_stubs.py'

select-tests:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -99,6 +100,7 @@ jobs:
--always-full 'pyproject.toml' \
--always-full 'environment.yml' \
--always-full 'ultraplot/__init__.py' \
--always-full 'tools/generate_stubs.py' \
--ignore 'docs/**' \
--ignore 'README.rst'
echo "Selection output:"
Expand Down Expand Up @@ -138,6 +140,53 @@ jobs:
echo "Detected test matrix: $(echo "$OUTPUT" | jq -c '.test_matrix')"
python tools/ci/version_support.py --format github-output >> $GITHUB_OUTPUT

stubs:
name: Static API stubs
runs-on: ubuntu-latest
needs:
- run-if-changes
if: always() && needs.run-if-changes.outputs.run == 'true'
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: pip

- name: Install UltraPlot and typing tools
run: pip install -e ".[typing]"

- name: Verify generated stubs
run: python tools/generate_stubs.py --check

- name: Check Pylance-compatible consumption
run: basedpyright tools/ci/stub_consumer.py --level error

- name: Check Pyrefly consumption and generated syntax
run: |
pyrefly check tools/ci/stub_consumer.py \
--search-path . \
--python-interpreter-path "$(command -v python)" \
--progress-bar no
pyrefly check 'ultraplot/**/*.pyi' \
--search-path . \
--python-interpreter-path "$(command -v python)" \
--ignore-missing-imports icecream \
--ignore-missing-imports cartopy \
--ignore-missing-imports cartopy.crs \
--ignore-missing-imports cartopy.feature \
--ignore-missing-imports cartopy.io \
--ignore-missing-imports cartopy.mpl.feature_artist \
--ignore-missing-imports cartopy.mpl.geoaxes \
--ignore-missing-imports cartopy.mpl.gridliner \
--ignore-missing-imports cartopy.mpl.path \
--ignore-missing-imports cartopy.mpl.ticker \
--ignore-missing-imports cftime \
--ignore-missing-imports mpl_toolkits.basemap \
--ignore-missing-imports matplotlib.fontconfig_pattern \
--progress-bar no

coverage:
name: Coverage
runs-on: ubuntu-latest
Expand Down Expand Up @@ -213,14 +262,15 @@ jobs:
needs:
- build
- run-if-changes
- stubs
if: always()
runs-on: ubuntu-latest
steps:
- run: |
if [[ '${{ needs.run-if-changes.outputs.run }}' == 'false' ]]; then
echo "No changes detected, tests skipped."
else
if [[ '${{ needs.build.result }}' == 'success' ]]; then
if [[ '${{ needs.build.result }}' == 'success' && '${{ needs.stubs.result }}' == 'success' ]]; then
echo "All tests passed successfully!"
else
echo "Tests failed!"
Expand Down
16 changes: 16 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# Import statements
import datetime
import inspect
import logging
import os
import re
Expand Down Expand Up @@ -637,5 +638,20 @@ def _replace_snippet(match):
pass


def process_signature(
app, what, name, obj, options, signature, return_annotation
):
"""Use compact signatures marked by UltraPlot only in generated docs."""
marked = getattr(obj, "__ultraplot_doc_signature__", None)
if marked is None and inspect.ismethod(obj):
marked = getattr(obj.__func__, "__ultraplot_doc_signature__", None)
if marked is None and inspect.isclass(obj):
marked = getattr(obj.__init__, "__ultraplot_doc_signature__", None)
if marked is not None:
return marked, return_annotation
return signature, return_annotation


def setup(app):
app.connect("autodoc-process-docstring", process_docstring)
app.connect("autodoc-process-signature", process_signature)
31 changes: 31 additions & 0 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,37 @@ When adding a new submodule, make sure it is compatible with the lazy loader:
By following these steps, your module will integrate cleanly with the lazy loading
system without requiring manual registry updates.

Editor type information and docstrings
--------------------------------------

UltraPlot ships generated ``.pyi`` files so static analysis tools such as Pylance
and Pyrefly can see the public API and fully expanded docstrings without importing
the package. The runtime modules remain the source of truth and continue to use the
lazy loader.

After changing a Python signature, annotation, public import, or docstring snippet,
install the pinned typing tools, regenerate the stubs from the repository root, and
commit the updated ``.pyi`` files:

.. code-block:: bash

pip install -e ".[typing]"
python tools/generate_stubs.py

Installation does not generate or modify these files. Release artifacts include the
stubs that were generated and checked into the repository. The generator runs
Pyrefly against an isolated source-only package, merges its inferred annotations
into a complete syntax-derived representation of the package, and statically
expands registered docstring snippets. This preserves declarations that Pyrefly
cannot discover through decorators or lazy loading.

To rerun inference and verify that every committed stub is up to date without
changing files, run:

.. code-block:: bash

python tools/generate_stubs.py --check


.. _contrib_pr:

Expand Down
6 changes: 3 additions & 3 deletions docs/projections.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,13 @@
# projections global extent by calling :meth:`~cartopy.mpl.geoaxes.GeoAxes.set_global`.
# This is a deviation from cartopy, which determines map boundaries automatically
# based on the coordinates of the plotted content. To revert to cartopy's
# default behavior, set :rcraw:`geo.extent` to ``'auto`` or pass ``extent='auto'``
# default behavior, set :rcraw:`geo.extent` to ``'auto'`` or pass ``extent='auto'``
# to :func:`~ultraplot.axes.GeoAxes.format`.
# * By default, UltraPlot gives circular boundaries to polar cartopy and basemap
# projections like :class:`~cartopy.crs.NorthPolarStereo` (see `this example
# <https://cartopy.readthedocs.io/stable/gallery/lines_and_polygons/always_circular_stereo.html>`__
# from the cartopy website). To disable this feature, set :rcraw:`geo.round` to
# ``False`` or pass ``round=False` to :func:`~ultraplot.axes.GeoAxes.format`. Please note
# ``False`` or pass ``round=False`` to :func:`~ultraplot.axes.GeoAxes.format`. Please note
# that older versions of cartopy cannot add gridlines to maps bounded by circles.
# * To make things more consistent, the :class:`~ultraplot.constructor.Proj` constructor
# function lets you supply native `PROJ <https://proj.org>`__ keyword names
Expand Down Expand Up @@ -332,7 +332,7 @@
# (i.e., Plate Carrée) coordinates the *default* coordinate system for all plotting
# commands by internally passing ``transform=ccrs.PlateCarree()`` to cartopy commands
# and ``latlon=True`` to basemap commands. And again, when `basemap`_ is the backend,
# plotting is done "cartopy-style" by calling methods from the `ultraplot.axes.GeoAxes`
# plotting is done "cartopy-style" by calling methods from the :class:`~ultraplot.axes.GeoAxes`
# instance rather than the :class:`~mpl_toolkits.basemap.Basemap` instance.
#
# To ensure that a 2D :class:`~ultraplot.axes.PlotAxes` command like
Expand Down
14 changes: 13 additions & 1 deletion docs/sphinxext/custom_roles.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Custom :rc: and :rcraw: roles for rc settings.
Custom roles used by UltraPlot documentation.
"""

import os
Expand Down Expand Up @@ -57,10 +57,22 @@ def rc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # no
return node_list, []


def mpltype_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: U100
"""
Render Matplotlib's ``:mpltype:`` annotations as inline literals.

Matplotlib uses this role in inherited docstrings, but its documentation
extension is not loaded by this project. Registering it locally prevents
unresolved-role warnings and visibly broken API markup.
"""
return [nodes.literal(rawtext, text)], []


def setup(app):
"""
Set up the roles.
"""
app.add_role("rc", rc_role)
app.add_role("rcraw", rc_raw_role)
app.add_role("mpltype", mpltype_role)
return {"parallel_read_safe": True, "parallel_write_safe": True}
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ dynamic = ["version"]
packages = {find = {exclude=["docs*", "baseline*", "logo*"]}}
include-package-data = true

[tool.setuptools.package-data]
ultraplot = ["py.typed", "*.pyi", "**/*.pyi"]

[tool.setuptools_scm]
write_to = "ultraplot/_version.py"
write_to_template = "__version__ = '{version}'\n"
Expand All @@ -71,6 +74,10 @@ filterwarnings = [
]
mpl-default-style = { axes.prop_cycle = "cycler('color', ['#4c72b0ff', '#55a868ff', '#c44e52ff', '#8172b2ff', '#ccb974ff', '#64b5cdff'])" }
[project.optional-dependencies]
typing = [
"basedpyright==1.31.4",
"pyrefly==1.2.0",
]
docs = [
"jupyter",
"jupytext",
Expand Down
13 changes: 13 additions & 0 deletions tools/ci/stub_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Representative lazy public imports consumed by static type checkers."""

import ultraplot as uplt

reveal_type(uplt.subplots)
reveal_type(uplt.Axes.format)

figure, axes = uplt.subplots()
figure_check: uplt.Figure = figure
axes_check: uplt.SubplotGrid = axes
axis_check: uplt.Axes = axes[0]
axes[0].format(title="Static typing")
reveal_type(axes.plot)
Loading