step 1: lock public api and pure core

This commit is contained in:
2026-05-22 18:21:01 +02:00
parent 11fc1bb9bd
commit bd1ce7abff
14 changed files with 885 additions and 6 deletions

View File

@@ -2,15 +2,15 @@
## Current status ## Current status
Rebuild initialized. Step 1 complete.
## Completed steps ## Completed steps
None yet. Step 1 - Public API contract and pure core.
## Current step ## Current step
Step 1 - Public API contract and pure core. Step 2 - Thread-safe state, commands, overlays, and cache model.
## Design decisions ## Design decisions
@@ -31,7 +31,13 @@ None yet.
- Read `STEPS.md`, `FEATURES.md`, and `ARCHITECTURE.md`. - Read `STEPS.md`, `FEATURES.md`, and `ARCHITECTURE.md`.
- Created initial package, examples, tests, and agent-log structure. - Created initial package, examples, tests, and agent-log structure.
- Implemented public exports, exceptions, common types, tile provider registry, projection helpers, cache dataclasses, and GUI-dependent API stubs.
- Added Step 1 tests for imports, providers, projection, and cache dataclasses.
- Ran `uv run pytest`.
- Ran `uv run ruff check .`.
- Ran `uv run ruff format --check .`.
- Ran `uv run pyright`.
## Next action ## Next action
Implement Step 1. Implement Step 2.

View File

@@ -0,0 +1,26 @@
# dpg-map
`dpg-map` is a Dear PyGui map widget package under rebuild.
The Step 1 public API contract is in place:
```python
import dpg_map as dpgm
provider = dpgm.TileProvider(
name="custom",
url_template="https://example.com/{z}/{x}/{y}.png",
attribution="Tiles (c) Example",
)
dpgm.register_provider(provider)
```
Implemented in Step 1:
- public package exports
- tile provider definitions and registry
- Web Mercator projection helpers
- initial cache dataclasses
- explicit stubs for GUI-dependent public functions
GUI widget rendering and runtime state updates are planned for later rebuild steps.

View File

@@ -1,2 +1,87 @@
"""Public API for dpg-map."""
from .api import (
add_layer,
add_marker,
add_polyline,
add_trajectory,
clear_disk_cache,
clear_layer,
clear_map,
clear_memory_cache,
configure,
delete_overlay,
fit_bounds,
get_cache_stats,
get_center,
get_map_debug_state,
get_zoom,
hide_layer,
latlon_to_screen,
screen_to_latlon,
set_center,
set_marker_label,
set_marker_position,
set_overlay_show,
set_polyline_points,
set_provider,
set_view,
set_zoom,
show_layer,
update_marker,
update_polyline,
update_trajectory,
)
from .providers import (
TileProvider,
get_provider,
list_providers,
register_provider,
unregister_provider,
)
from .widget import map_widget
__all__ = [
"TileProvider",
"add_layer",
"add_marker",
"add_polyline",
"add_trajectory",
"clear_disk_cache",
"clear_layer",
"clear_map",
"clear_memory_cache",
"configure",
"delete_overlay",
"fit_bounds",
"get_cache_stats",
"get_center",
"get_map_debug_state",
"get_provider",
"get_zoom",
"hide_layer",
"latlon_to_screen",
"list_providers",
"map_widget",
"register_provider",
"screen_to_latlon",
"set_center",
"set_marker_label",
"set_marker_position",
"set_overlay_show",
"set_polyline_points",
"set_provider",
"set_view",
"set_zoom",
"show_layer",
"unregister_provider",
"update_marker",
"update_polyline",
"update_trajectory",
]
def main() -> None: def main() -> None:
print("Hello from dpg-map!") """Console entry point placeholder."""
print("dpg-map")

View File

@@ -1 +1,211 @@
"""Public API wrappers for dpg-map.""" """Public API wrappers for dpg-map."""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from typing import Any, NoReturn
from .cache import CacheStats
from .exceptions import DpgMapNotImplementedError
from .providers import TileProvider
from .types import Bounds, LatLon, Point, Tag
def _stub(name: str) -> NoReturn:
raise DpgMapNotImplementedError(f"dpg_map.{name} is not implemented until a later rebuild step")
def configure(
*,
user_agent: str | None = None,
cache_dir: str | Path | None = None,
default_provider: str | TileProvider = "osm",
memory_cache_max_tiles: int = 512,
disk_cache_max_bytes: int | None = 2_000_000_000,
prefetch_margin_tiles: int = 1,
tile_worker_count: int = 4,
overlay_update_policy: str = "coalesce",
debug: bool = False,
) -> None:
_stub("configure")
def set_center(lat: float, lon: float, *, map_tag: Tag | None = None) -> None:
_stub("set_center")
def get_center(*, map_tag: Tag | None = None) -> LatLon:
_stub("get_center")
def set_zoom(zoom: int, *, map_tag: Tag | None = None) -> None:
_stub("set_zoom")
def get_zoom(*, map_tag: Tag | None = None) -> int:
_stub("get_zoom")
def set_view(
*,
center: LatLon | None = None,
zoom: int | None = None,
map_tag: Tag | None = None,
) -> None:
_stub("set_view")
def fit_bounds(bounds: Bounds, *, map_tag: Tag | None = None) -> None:
_stub("fit_bounds")
def screen_to_latlon(x: float, y: float, *, map_tag: Tag | None = None) -> LatLon:
_stub("screen_to_latlon")
def latlon_to_screen(lat: float, lon: float, *, map_tag: Tag | None = None) -> Point:
_stub("latlon_to_screen")
def add_marker(
tag: Tag,
*,
lat: float,
lon: float,
label: str | None = None,
layer: str = "default",
show: bool = True,
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
_stub("add_marker")
def add_polyline(
tag: Tag,
*,
points: Sequence[LatLon] | None = None,
lats: Sequence[float] | None = None,
lons: Sequence[float] | None = None,
layer: str = "default",
show: bool = True,
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
_stub("add_polyline")
def add_trajectory(
tag: Tag,
*,
points: Sequence[LatLon] | None = None,
lats: Sequence[float] | None = None,
lons: Sequence[float] | None = None,
layer: str = "default",
show: bool = True,
map_tag: Tag | None = None,
**kwargs: Any,
) -> Tag:
_stub("add_trajectory")
def update_marker(
tag: Tag,
*,
lat: float | None = None,
lon: float | None = None,
label: str | None = None,
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
_stub("update_marker")
def update_polyline(
tag: Tag,
*,
points: Sequence[LatLon] | None = None,
lats: Sequence[float] | None = None,
lons: Sequence[float] | None = None,
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
_stub("update_polyline")
def update_trajectory(
tag: Tag,
*,
points: Sequence[LatLon] | None = None,
lats: Sequence[float] | None = None,
lons: Sequence[float] | None = None,
map_tag: Tag | None = None,
**kwargs: Any,
) -> None:
_stub("update_trajectory")
def set_marker_position(tag: Tag, lat: float, lon: float, *, map_tag: Tag | None = None) -> None:
_stub("set_marker_position")
def set_marker_label(tag: Tag, label: str, *, map_tag: Tag | None = None) -> None:
_stub("set_marker_label")
def set_polyline_points(
tag: Tag,
points: Sequence[LatLon],
*,
map_tag: Tag | None = None,
) -> None:
_stub("set_polyline_points")
def set_overlay_show(tag: Tag, show: bool, *, map_tag: Tag | None = None) -> None:
_stub("set_overlay_show")
def delete_overlay(tag: Tag, *, map_tag: Tag | None = None) -> None:
_stub("delete_overlay")
def add_layer(name: str, *, show: bool = True, map_tag: Tag | None = None) -> None:
_stub("add_layer")
def show_layer(name: str, *, map_tag: Tag | None = None) -> None:
_stub("show_layer")
def hide_layer(name: str, *, map_tag: Tag | None = None) -> None:
_stub("hide_layer")
def clear_layer(name: str, *, map_tag: Tag | None = None) -> None:
_stub("clear_layer")
def clear_map(*, map_tag: Tag | None = None) -> None:
_stub("clear_map")
def set_provider(provider: str | TileProvider, *, map_tag: Tag | None = None) -> None:
_stub("set_provider")
def clear_memory_cache(*, map_tag: Tag | None = None) -> None:
_stub("clear_memory_cache")
def clear_disk_cache(*, map_tag: Tag | None = None) -> None:
_stub("clear_disk_cache")
def get_cache_stats(*, map_tag: Tag | None = None) -> CacheStats:
_stub("get_cache_stats")
def get_map_debug_state(*, map_tag: Tag | None = None) -> dict[str, Any]:
_stub("get_map_debug_state")

View File

@@ -1 +1,36 @@
"""Memory and disk cache helpers.""" """Memory and disk cache helpers."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, slots=True)
class CacheStats:
"""Public cache statistics snapshot."""
memory_tiles: int = 0
memory_max_tiles: int = 0
memory_hits: int = 0
memory_misses: int = 0
disk_bytes: int = 0
disk_max_bytes: int | None = None
disk_hits: int = 0
disk_misses: int = 0
disk_path: Path | None = None
@dataclass(slots=True)
class MemoryCacheConfig:
"""Initial memory cache configuration."""
max_tiles: int = 512
@dataclass(slots=True)
class DiskCacheConfig:
"""Initial persistent disk cache configuration."""
path: Path | None = None
max_bytes: int | None = 2_000_000_000

View File

@@ -1 +1,29 @@
"""Public exception types.""" """Public exception types for dpg-map."""
class DpgMapError(Exception):
"""Base exception for all public dpg-map errors."""
class DpgMapNotImplementedError(DpgMapError, NotImplementedError):
"""Raised by public APIs that are intentionally stubbed during the rebuild."""
class ProviderError(DpgMapError):
"""Base exception for tile provider errors."""
class ProviderExistsError(ProviderError):
"""Raised when registering a provider name that already exists."""
class ProviderNotFoundError(ProviderError):
"""Raised when a requested tile provider is not registered."""
class InvalidProviderError(ProviderError, ValueError):
"""Raised when a tile provider definition is invalid."""
class ProjectionError(DpgMapError, ValueError):
"""Raised when geographic projection input is invalid."""

View File

@@ -1 +1,92 @@
"""Web Mercator projection helpers.""" """Web Mercator projection helpers."""
from __future__ import annotations
import math
from .exceptions import ProjectionError
from .types import LatLon, Point
WEB_MERCATOR_MAX_LAT = 85.05112878
DEFAULT_TILE_SIZE = 256
def clamp_latitude(lat: float) -> float:
"""Clamp latitude to the Web Mercator representable range."""
return min(max(lat, -WEB_MERCATOR_MAX_LAT), WEB_MERCATOR_MAX_LAT)
def map_size(zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> int:
"""Return square pixel size of the full world at a zoom level."""
if zoom < 0:
raise ProjectionError("zoom must be >= 0")
if tile_size <= 0:
raise ProjectionError("tile_size must be > 0")
return tile_size * (2**zoom)
def latlon_to_world(lat: float, lon: float, zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> Point:
"""Project latitude/longitude to world pixel coordinates."""
size = map_size(zoom, tile_size)
lat = clamp_latitude(lat)
lon = ((lon + 180.0) % 360.0) - 180.0
sin_lat = math.sin(math.radians(lat))
x = (lon + 180.0) / 360.0 * size
y = (0.5 - math.log((1.0 + sin_lat) / (1.0 - sin_lat)) / (4.0 * math.pi)) * size
return (x, y)
def world_to_latlon(x: float, y: float, zoom: int, tile_size: int = DEFAULT_TILE_SIZE) -> LatLon:
"""Unproject world pixel coordinates to latitude/longitude."""
size = map_size(zoom, tile_size)
lon = x / size * 360.0 - 180.0
n = math.pi - 2.0 * math.pi * y / size
lat = math.degrees(math.atan(math.sinh(n)))
return (lat, lon)
def latlon_to_tile(lat: float, lon: float, zoom: int) -> tuple[int, int, int]:
"""Return the XYZ tile coordinate containing a latitude/longitude point."""
x, y = latlon_to_world(lat, lon, zoom)
scale = 2**zoom
tile_x = min(max(int(x // DEFAULT_TILE_SIZE), 0), scale - 1)
tile_y = min(max(int(y // DEFAULT_TILE_SIZE), 0), scale - 1)
return (tile_x, tile_y, zoom)
def world_to_screen(
world_x: float,
world_y: float,
*,
center: LatLon,
zoom: int,
width: int,
height: int,
tile_size: int = DEFAULT_TILE_SIZE,
) -> Point:
"""Convert world pixels to screen pixels for a centered viewport."""
center_x, center_y = latlon_to_world(center[0], center[1], zoom, tile_size)
return (world_x - center_x + width / 2.0, world_y - center_y + height / 2.0)
def screen_to_world(
screen_x: float,
screen_y: float,
*,
center: LatLon,
zoom: int,
width: int,
height: int,
tile_size: int = DEFAULT_TILE_SIZE,
) -> Point:
"""Convert screen pixels to world pixels for a centered viewport."""
center_x, center_y = latlon_to_world(center[0], center[1], zoom, tile_size)
return (screen_x + center_x - width / 2.0, screen_y + center_y - height / 2.0)

View File

@@ -1 +1,145 @@
"""Tile provider definitions and registry.""" """Tile provider definitions and registry."""
from __future__ import annotations
from dataclasses import dataclass, field
from string import Formatter
from threading import RLock
from .exceptions import InvalidProviderError, ProviderExistsError, ProviderNotFoundError
_REQUIRED_TEMPLATE_FIELDS = frozenset({"x", "y", "z"})
_OPTIONAL_TEMPLATE_FIELDS = frozenset({"s", "r", "ext"})
@dataclass(frozen=True, slots=True)
class TileProvider:
"""XYZ raster tile provider definition."""
name: str
url_template: str
min_zoom: int = 0
max_zoom: int = 19
tile_size: int = 256
attribution: str = ""
headers: dict[str, str] = field(default_factory=dict)
subdomains: tuple[str, ...] = ()
retina: bool = False
file_extension: str | None = None
def __post_init__(self) -> None:
name = self.name.strip()
if not name:
raise InvalidProviderError("provider name must not be empty")
if name != self.name:
object.__setattr__(self, "name", name)
if not self.url_template.strip():
raise InvalidProviderError("provider url_template must not be empty")
fields = {
field_name
for _, field_name, _, _ in Formatter().parse(self.url_template)
if field_name is not None and field_name != ""
}
missing = _REQUIRED_TEMPLATE_FIELDS - fields
if missing:
missing_text = ", ".join(sorted(missing))
raise InvalidProviderError(f"provider url_template missing field(s): {missing_text}")
unknown = fields - _REQUIRED_TEMPLATE_FIELDS - _OPTIONAL_TEMPLATE_FIELDS
if unknown:
unknown_text = ", ".join(sorted(unknown))
raise InvalidProviderError(
f"provider url_template contains unknown field(s): {unknown_text}"
)
if self.min_zoom < 0:
raise InvalidProviderError("provider min_zoom must be >= 0")
if self.max_zoom < self.min_zoom:
raise InvalidProviderError("provider max_zoom must be >= min_zoom")
if self.tile_size <= 0:
raise InvalidProviderError("provider tile_size must be > 0")
object.__setattr__(self, "headers", dict(self.headers))
object.__setattr__(self, "subdomains", tuple(self.subdomains))
def build_url(self, *, x: int, y: int, z: int) -> str:
"""Build a concrete tile URL for an XYZ tile coordinate."""
if z < self.min_zoom or z > self.max_zoom:
raise InvalidProviderError(
f"zoom {z} is outside provider range {self.min_zoom}-{self.max_zoom}"
)
subdomain = ""
if self.subdomains:
subdomain = self.subdomains[(x + y + z) % len(self.subdomains)]
retina_suffix = "@2x" if self.retina else ""
extension = self.file_extension or ""
return self.url_template.format(
x=x,
y=y,
z=z,
s=subdomain,
r=retina_suffix,
ext=extension,
)
OSM = TileProvider(
name="osm",
url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
min_zoom=0,
max_zoom=19,
tile_size=256,
attribution="\u00a9 OpenStreetMap contributors",
)
_providers: dict[str, TileProvider] = {OSM.name: OSM}
_providers_lock = RLock()
def register_provider(provider: TileProvider, *, replace: bool = False) -> None:
"""Register a tile provider by name."""
if not isinstance(provider, TileProvider):
raise InvalidProviderError("provider must be a TileProvider")
with _providers_lock:
if provider.name in _providers and not replace:
raise ProviderExistsError(f"provider already registered: {provider.name}")
_providers[provider.name] = provider
def unregister_provider(name: str) -> None:
"""Remove a registered tile provider."""
with _providers_lock:
if name not in _providers:
raise ProviderNotFoundError(f"provider not registered: {name}")
del _providers[name]
def get_provider(name: str) -> TileProvider:
"""Return a registered tile provider."""
with _providers_lock:
try:
return _providers[name]
except KeyError as exc:
raise ProviderNotFoundError(f"provider not registered: {name}") from exc
def get_default_provider() -> TileProvider:
"""Return the default OpenStreetMap provider."""
return get_provider(OSM.name)
def list_providers() -> list[str]:
"""List registered provider names in stable sorted order."""
with _providers_lock:
return sorted(_providers)

View File

@@ -1 +1,31 @@
"""Shared type aliases and small value objects.""" """Shared type aliases and small value objects."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TypeAlias
Tag: TypeAlias = str | int
LatLon: TypeAlias = tuple[float, float]
Point: TypeAlias = tuple[float, float]
Bounds: TypeAlias = tuple[LatLon, LatLon]
TileCoord: TypeAlias = tuple[int, int, int]
Color: TypeAlias = tuple[int, int, int] | tuple[int, int, int, int]
@dataclass(frozen=True, slots=True)
class ScreenPoint:
"""A pixel position in map widget coordinates."""
x: float
y: float
@dataclass(frozen=True, slots=True)
class GeoBounds:
"""Latitude/longitude bounds with south-west and north-east corners."""
south: float
west: float
north: float
east: float

View File

@@ -1 +1,31 @@
"""Dear PyGui map widget construction.""" """Dear PyGui map widget construction."""
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from .exceptions import DpgMapNotImplementedError
from .providers import TileProvider
from .types import LatLon, Tag
@contextmanager
def map_widget(
*,
tag: Tag | None = None,
center: LatLon = (0.0, 0.0),
zoom: int = 2,
provider: str | TileProvider | None = None,
width: int = 0,
height: int = 0,
autosize_x: bool = False,
autosize_y: bool = False,
**kwargs: object,
) -> Iterator[Tag | None]:
"""Stub context manager for the future Dear PyGui widget."""
raise DpgMapNotImplementedError(
"dpg_map.map_widget is not implemented until the GUI rebuild steps"
)
yield tag

31
tests/test_cache.py Normal file
View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from pathlib import Path
from dpg_map.cache import CacheStats, DiskCacheConfig, MemoryCacheConfig
def test_cache_stats_dataclass_construction() -> None:
stats = CacheStats(
memory_tiles=3,
memory_max_tiles=512,
memory_hits=10,
memory_misses=2,
disk_bytes=1024,
disk_max_bytes=None,
disk_hits=7,
disk_misses=1,
disk_path=Path("/tmp/dpg-map-cache"),
)
assert stats.memory_tiles == 3
assert stats.disk_max_bytes is None
assert stats.disk_path == Path("/tmp/dpg-map-cache")
def test_initial_cache_config_dataclasses() -> None:
memory_config = MemoryCacheConfig()
disk_config = DiskCacheConfig()
assert memory_config.max_tiles == 512
assert disk_config.max_bytes == 2_000_000_000

44
tests/test_projection.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import pytest
from dpg_map.projection import (
WEB_MERCATOR_MAX_LAT,
clamp_latitude,
latlon_to_tile,
latlon_to_world,
screen_to_world,
world_to_latlon,
world_to_screen,
)
@pytest.mark.parametrize(
("lat", "lon", "zoom"),
[
(0.0, 0.0, 0),
(47.9029, 1.9093, 15),
(-33.8688, 151.2093, 10),
(WEB_MERCATOR_MAX_LAT, 179.999, 4),
],
)
def test_projection_roundtrip(lat: float, lon: float, zoom: int) -> None:
x, y = latlon_to_world(lat, lon, zoom)
roundtrip_lat, roundtrip_lon = world_to_latlon(x, y, zoom)
assert roundtrip_lat == pytest.approx(clamp_latitude(lat), abs=1e-7)
assert roundtrip_lon == pytest.approx(lon, abs=1e-7)
def test_latlon_to_tile_returns_xyz_coordinate() -> None:
assert latlon_to_tile(0.0, 0.0, 1) == (1, 1, 1)
def test_world_screen_roundtrip() -> None:
center = (47.9029, 1.9093)
world = latlon_to_world(47.91, 1.92, 14)
screen = world_to_screen(*world, center=center, zoom=14, width=800, height=600)
assert screen_to_world(*screen, center=center, zoom=14, width=800, height=600) == pytest.approx(
world
)

71
tests/test_providers.py Normal file
View File

@@ -0,0 +1,71 @@
from __future__ import annotations
import pytest
import dpg_map as dpgm
from dpg_map.exceptions import InvalidProviderError, ProviderExistsError, ProviderNotFoundError
def test_default_provider_is_registered() -> None:
provider = dpgm.get_provider("osm")
assert provider.name == "osm"
assert "osm" in dpgm.list_providers()
assert provider.build_url(x=1, y=2, z=3) == "https://tile.openstreetmap.org/3/1/2.png"
def test_provider_registration_roundtrip() -> None:
provider = dpgm.TileProvider(
name="unit-test-provider",
url_template="https://tiles.example.test/{z}/{x}/{y}.png",
attribution="Example",
)
dpgm.register_provider(provider)
try:
assert dpgm.get_provider("unit-test-provider") == provider
assert "unit-test-provider" in dpgm.list_providers()
finally:
dpgm.unregister_provider("unit-test-provider")
with pytest.raises(ProviderNotFoundError):
dpgm.get_provider("unit-test-provider")
def test_duplicate_provider_registration_fails() -> None:
provider = dpgm.TileProvider(
name="unit-test-duplicate",
url_template="https://tiles.example.test/{z}/{x}/{y}.png",
)
dpgm.register_provider(provider)
try:
with pytest.raises(ProviderExistsError):
dpgm.register_provider(provider)
finally:
dpgm.unregister_provider("unit-test-duplicate")
def test_provider_url_building_with_subdomains_retina_and_extension() -> None:
provider = dpgm.TileProvider(
name="unit-test-template",
url_template="https://{s}.tiles.example.test/{z}/{x}/{y}{r}.{ext}",
subdomains=("a", "b", "c"),
retina=True,
file_extension="webp",
)
assert provider.build_url(x=1, y=2, z=3) == "https://a.tiles.example.test/3/1/2@2x.webp"
@pytest.mark.parametrize(
"template",
[
"https://tiles.example.test/{z}/{x}.png",
"https://tiles.example.test/{z}/{x}/{y}/{quadkey}.png",
"",
],
)
def test_invalid_provider_templates_fail(template: str) -> None:
with pytest.raises(InvalidProviderError):
dpgm.TileProvider(name="bad-template", url_template=template)

48
tests/test_public_api.py Normal file
View File

@@ -0,0 +1,48 @@
from __future__ import annotations
def test_package_exports_required_public_api() -> None:
import dpg_map as dpgm
expected = {
"configure",
"TileProvider",
"register_provider",
"unregister_provider",
"get_provider",
"list_providers",
"map_widget",
"set_center",
"get_center",
"set_zoom",
"get_zoom",
"set_view",
"fit_bounds",
"screen_to_latlon",
"latlon_to_screen",
"add_marker",
"add_polyline",
"add_trajectory",
"update_marker",
"update_polyline",
"update_trajectory",
"set_marker_position",
"set_marker_label",
"set_polyline_points",
"set_overlay_show",
"delete_overlay",
"add_layer",
"show_layer",
"hide_layer",
"clear_layer",
"clear_map",
"set_provider",
"clear_memory_cache",
"clear_disk_cache",
"get_cache_stats",
"get_map_debug_state",
}
assert set(dpgm.__all__) == expected
for name in expected:
assert hasattr(dpgm, name)