Skip to content

Commit 7ee8d4e

Browse files
authored
Add typing block for inspection (#659)
* Add typing block for inspection * Add tests for public API and fix some missing imports * Rm stub
1 parent 6d700a9 commit 7ee8d4e

File tree

2 files changed

+259
-1
lines changed

2 files changed

+259
-1
lines changed

ultraplot/__init__.py

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,151 @@
77

88
import sys
99
from pathlib import Path
10-
from typing import Optional
10+
from typing import TYPE_CHECKING, Optional
1111

1212
from ._lazy import LazyLoader, install_module_proxy
1313

14+
if TYPE_CHECKING:
15+
# These imports are never executed at runtime, so they have zero effect on
16+
# import performance. They exist solely so that type checkers (pyright, mypy)
17+
# can resolve names that are otherwise provided by the lazy loader at runtime.
18+
#
19+
# Keep this block in sync with _LAZY_LOADING_EXCEPTIONS and every submodule's
20+
# __all__ — that is the only maintenance burden.
21+
import matplotlib.pyplot as pyplot
22+
23+
from .axes import Axes as Axes
24+
from .axes import CartesianAxes as CartesianAxes
25+
from .axes import ExternalAxesContainer as ExternalAxesContainer
26+
from .axes import GeoAxes as GeoAxes
27+
from .axes import PlotAxes as PlotAxes
28+
from .axes import PolarAxes as PolarAxes
29+
from .axes import ThreeAxes as ThreeAxes
30+
from .colors import ColormapDatabase as ColormapDatabase
31+
from .colors import ColorDatabase as ColorDatabase
32+
from .colors import ContinuousColormap as ContinuousColormap
33+
from .colors import DiscreteColormap as DiscreteColormap
34+
from .colors import DiscreteNorm as DiscreteNorm
35+
from .colors import DivergingNorm as DivergingNorm
36+
from .colors import LinearSegmentedColormap as LinearSegmentedColormap
37+
from .colors import LinearSegmentedNorm as LinearSegmentedNorm
38+
from .colors import ListedColormap as ListedColormap
39+
from .colors import PerceptualColormap as PerceptualColormap
40+
from .colors import PerceptuallyUniformColormap as PerceptuallyUniformColormap
41+
from .colors import SegmentedNorm as SegmentedNorm
42+
from .colors import _cmap_database as colormaps
43+
from .config import config_inline_backend as config_inline_backend
44+
from .config import Configurator as Configurator
45+
from .config import rc as rc
46+
from .config import rc_matplotlib as rc_matplotlib
47+
from .config import rc_ultraplot as rc_ultraplot
48+
from .config import register_cmaps as register_cmaps
49+
from .config import register_colors as register_colors
50+
from .config import register_cycles as register_cycles
51+
from .config import register_fonts as register_fonts
52+
from .config import use_style as use_style
53+
from .constructor import Colormap as Colormap
54+
from .constructor import Cycle as Cycle
55+
from .constructor import Formatter as Formatter
56+
from .constructor import FORMATTERS as FORMATTERS
57+
from .constructor import Locator as Locator
58+
from .constructor import LOCATORS as LOCATORS
59+
from .constructor import Norm as Norm
60+
from .constructor import NORMS as NORMS
61+
from .constructor import Proj as Proj
62+
from .constructor import PROJS as PROJS
63+
from .constructor import Scale as Scale
64+
from .constructor import SCALES as SCALES
65+
from .demos import show_channels as show_channels
66+
from .demos import show_cmaps as show_cmaps
67+
from .demos import show_colorspaces as show_colorspaces
68+
from .demos import show_colors as show_colors
69+
from .demos import show_cycles as show_cycles
70+
from .demos import show_fonts as show_fonts
71+
from .figure import Figure as Figure
72+
from .gridspec import GridSpec as GridSpec
73+
from .gridspec import SubplotGrid as SubplotGrid
74+
from .legend import GeometryEntry as GeometryEntry
75+
from .legend import Legend as Legend
76+
from .legend import LegendEntry as LegendEntry
77+
from .proj import Aitoff as Aitoff
78+
from .proj import Hammer as Hammer
79+
from .proj import KavrayskiyVII as KavrayskiyVII
80+
from .proj import NorthPolarAzimuthalEquidistant as NorthPolarAzimuthalEquidistant
81+
from .proj import NorthPolarGnomonic as NorthPolarGnomonic
82+
from .proj import (
83+
NorthPolarLambertAzimuthalEqualArea as NorthPolarLambertAzimuthalEqualArea,
84+
)
85+
from .proj import SouthPolarAzimuthalEquidistant as SouthPolarAzimuthalEquidistant
86+
from .proj import SouthPolarGnomonic as SouthPolarGnomonic
87+
from .proj import (
88+
SouthPolarLambertAzimuthalEqualArea as SouthPolarLambertAzimuthalEqualArea,
89+
)
90+
from .proj import WinkelTripel as WinkelTripel
91+
from .scale import CutoffScale as CutoffScale
92+
from .scale import ExpScale as ExpScale
93+
from .scale import FuncScale as FuncScale
94+
from .scale import InverseScale as InverseScale
95+
from .scale import LinearScale as LinearScale
96+
from .scale import LogitScale as LogitScale
97+
from .scale import LogScale as LogScale
98+
from .scale import MercatorLatitudeScale as MercatorLatitudeScale
99+
from .scale import PowerScale as PowerScale
100+
from .scale import SineLatitudeScale as SineLatitudeScale
101+
from .scale import SymmetricalLogScale as SymmetricalLogScale
102+
from .text import CurvedText as CurvedText
103+
from .ultralayout import ColorbarLayoutSolver as ColorbarLayoutSolver
104+
from .ultralayout import compute_ultra_positions as compute_ultra_positions
105+
from .ultralayout import get_grid_positions_ultra as get_grid_positions_ultra
106+
from .ultralayout import is_orthogonal_layout as is_orthogonal_layout
107+
from .ultralayout import UltraLayoutSolver as UltraLayoutSolver
108+
from .ticker import AutoCFDatetimeFormatter as AutoCFDatetimeFormatter
109+
from .ticker import AutoCFDatetimeLocator as AutoCFDatetimeLocator
110+
from .ticker import AutoFormatter as AutoFormatter
111+
from .ticker import CFDatetimeFormatter as CFDatetimeFormatter
112+
from .ticker import DegreeFormatter as DegreeFormatter
113+
from .ticker import DegreeLocator as DegreeLocator
114+
from .ticker import DiscreteLocator as DiscreteLocator
115+
from .ticker import FracFormatter as FracFormatter
116+
from .ticker import IndexFormatter as IndexFormatter
117+
from .ticker import IndexLocator as IndexLocator
118+
from .ticker import LatitudeFormatter as LatitudeFormatter
119+
from .ticker import LatitudeLocator as LatitudeLocator
120+
from .ticker import LongitudeFormatter as LongitudeFormatter
121+
from .ticker import LongitudeLocator as LongitudeLocator
122+
from .ticker import SciFormatter as SciFormatter
123+
from .ticker import SigFigFormatter as SigFigFormatter
124+
from .ticker import SimpleFormatter as SimpleFormatter
125+
from .ui import close as close
126+
from .ui import figure as figure
127+
from .ui import ioff as ioff
128+
from .ui import ion as ion
129+
from .ui import isinteractive as isinteractive
130+
from .ui import show as show
131+
from .ui import subplot as subplot
132+
from .ui import subplots as subplots
133+
from .ui import switch_backend as switch_backend
134+
from .utils import arange as arange
135+
from .utils import check_for_update as check_for_update
136+
from .utils import edges as edges
137+
from .utils import edges2d as edges2d
138+
from .utils import get_colors as get_colors
139+
from .utils import saturate as saturate
140+
from .utils import scale_luminance as scale_luminance
141+
from .utils import scale_saturation as scale_saturation
142+
from .utils import set_alpha as set_alpha
143+
from .utils import set_hue as set_hue
144+
from .utils import set_luminance as set_luminance
145+
from .utils import set_saturation as set_saturation
146+
from .utils import shade as shade
147+
from .utils import shift_hue as shift_hue
148+
from .utils import to_hex as to_hex
149+
from .utils import to_rgb as to_rgb
150+
from .utils import to_rgba as to_rgba
151+
from .utils import to_xyz as to_xyz
152+
from .utils import to_xyza as to_xyza
153+
from .utils import units as units
154+
14155
name = "ultraplot"
15156

16157
try:

ultraplot/tests/test_imports.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import ast
12
import importlib.util
23
import json
34
import os
45
import subprocess
56
import sys
7+
from pathlib import Path
68

79
import pytest
810

@@ -139,6 +141,121 @@ def test_internals_lazy_attrs():
139141
assert "axes.grid" in rc_matplotlib
140142

141143

144+
def _parse_type_checking_names():
145+
"""
146+
Parse ultraplot/__init__.py and return every name declared inside the
147+
``if TYPE_CHECKING:`` block, mapped to its import origin.
148+
149+
Returns a dict of ``{public_name: (module, original_name)}``.
150+
"""
151+
init_path = Path(__file__).parent.parent / "__init__.py"
152+
tree = ast.parse(init_path.read_text(encoding="utf-8"))
153+
154+
names = {}
155+
for node in tree.body:
156+
if not isinstance(node, ast.If):
157+
continue
158+
# Match `if TYPE_CHECKING:` (handles both bare name and attribute access)
159+
test = node.test
160+
is_type_checking = (
161+
isinstance(test, ast.Name) and test.id == "TYPE_CHECKING"
162+
) or (isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING")
163+
if not is_type_checking:
164+
continue
165+
for stmt in ast.walk(node):
166+
if isinstance(stmt, ast.ImportFrom):
167+
for alias in stmt.names:
168+
public_name = alias.asname or alias.name
169+
names[public_name] = (stmt.module, alias.name)
170+
elif isinstance(stmt, ast.Import):
171+
for alias in stmt.names:
172+
public_name = alias.asname or alias.name.split(".")[0]
173+
names[public_name] = (alias.name, None)
174+
return names
175+
176+
177+
def test_type_checking_names_accessible_at_runtime():
178+
"""
179+
Every name declared inside ``if TYPE_CHECKING:`` in ``__init__.py`` must
180+
also be accessible at runtime via the lazy loader.
181+
182+
This ensures the TYPE_CHECKING block never silently drifts out of sync with
183+
what the package actually exposes.
184+
"""
185+
import ultraplot
186+
187+
declared = _parse_type_checking_names()
188+
assert declared, "TYPE_CHECKING block is empty or could not be parsed"
189+
190+
missing = [name for name in declared if not hasattr(ultraplot, name)]
191+
assert not missing, (
192+
"Names declared in the TYPE_CHECKING block are not accessible at runtime.\n"
193+
"Either remove them from the TYPE_CHECKING block or expose them via the "
194+
"lazy loader / _LAZY_LOADING_EXCEPTIONS:\n"
195+
+ "\n".join(f" ultraplot.{n}" for n in missing)
196+
)
197+
198+
199+
def test_type_checking_block_covers_public_all():
200+
"""
201+
Every name in ``ultraplot.__all__`` that is a public class or callable
202+
should be declared in the TYPE_CHECKING block so type checkers can resolve it.
203+
204+
The following categories are intentionally excluded:
205+
206+
- Registry-derived names (e.g. ``LogNorm``) — populated dynamically from
207+
matplotlib at runtime; cannot be enumerated statically.
208+
- Submodule names (e.g. ``internals``) — are modules, not types.
209+
- Module-level scalars defined directly in ``__init__.py`` (``name``,
210+
``version``, ``__version__``, ``setup``) — already visible to type checkers
211+
without an import.
212+
- Deprecated ``_rename_objs`` wrappers (e.g. ``RcConfigurator``,
213+
``inline_backend_fmt``, ``Colors``) — their runtime type is a dynamically
214+
generated class/function from ``internals.warnings``; they cannot be
215+
imported cleanly and are not worth exposing to type checkers.
216+
"""
217+
import types
218+
import ultraplot
219+
220+
declared = set(_parse_type_checking_names())
221+
all_names = set(ultraplot.__all__)
222+
223+
# Registry-derived names — dynamically populated from matplotlib.
224+
ultraplot._build_registry_map()
225+
registry_names = set(ultraplot._REGISTRY_ATTRS or {})
226+
227+
# Submodule names — are modules, not types.
228+
submodule_names = {
229+
n
230+
for n in all_names
231+
if isinstance(getattr(ultraplot, n, None), types.ModuleType)
232+
}
233+
234+
# Names already defined at module-level in __init__.py itself — type
235+
# checkers see them directly without needing an import statement.
236+
init_level = {"__version__", "version", "name", "setup"}
237+
238+
# Deprecated _rename_objs wrappers — their __module__ is internals.warnings
239+
# because that is where the wrapper factory lives.
240+
deprecated_wrappers = {
241+
n
242+
for n in all_names
243+
if getattr(getattr(ultraplot, n, None), "__module__", "").endswith(
244+
"internals.warnings"
245+
)
246+
}
247+
248+
expected = (
249+
all_names - registry_names - submodule_names - init_level - deprecated_wrappers
250+
)
251+
missing_from_type_checking = sorted(expected - declared)
252+
assert not missing_from_type_checking, (
253+
"Names in ultraplot.__all__ are missing from the TYPE_CHECKING block.\n"
254+
"Add them to the ``if TYPE_CHECKING:`` block in ultraplot/__init__.py:\n"
255+
+ "\n".join(f" {n}" for n in missing_from_type_checking)
256+
)
257+
258+
142259
def test_docstring_missing_triggers_lazy_import():
143260
from ultraplot.internals import docstring
144261

0 commit comments

Comments
 (0)