diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e3a213..5455a1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,22 +55,11 @@ jobs: - name: Run tests with coverage run: pytest tests/ --cov=dissmodel --cov-report=term-missing --cov-report=xml - # Scoped to the modules whose Examples sections are runnable doctests; - # the remaining modules have illustrative pseudo-examples (external - # data / optional deps) — converting them is tracked as a follow-up. + # All >>> examples in the package are self-contained doctests; + # illustrative snippets use plain ```python fences instead + # (see CONTRIBUTING.md, "Documentation / Docstrings"). - name: Run doctests - run: | - pytest --doctest-modules \ - dissmodel/core/model.py \ - dissmodel/core/environment.py \ - dissmodel/geo/raster/backend.py \ - dissmodel/geo/raster/raster_grid.py \ - dissmodel/geo/raster/raster_model.py \ - dissmodel/geo/raster/cellular_automaton.py \ - dissmodel/geo/raster/sync_model.py \ - dissmodel/geo/vector/vector_grid.py \ - dissmodel/geo/vector/cellular_automaton.py \ - dissmodel/geo/vector/sync_model.py + run: pytest --doctest-modules dissmodel - name: Run mypy run: mypy dissmodel --ignore-missing-imports \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9e6b62..73ff052 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,5 +50,16 @@ If you have an idea for a new feature or improvement, please open an issue and t - Use type hints where possible. - Write docstrings for new functions and classes (NumPy style). +## Documentation / Docstrings + +Examples in NumPy-style docstrings that use `>>>` prompts are executed as +doctests in CI (`pytest --doctest-modules dissmodel`) and must be fully +self-contained and runnable — every name they use must be defined within the +example itself, and the expected output must match exactly. Longer +illustrative examples that assume objects from a broader context (e.g. an +existing GeoDataFrame, `Environment`, or model instance) should use plain +` ```python ` fenced code blocks instead of `>>>` prompts; mkdocstrings +renders both forms in the API reference. + ## License By contributing, you agree that your contributions will be licensed under the MIT License. \ No newline at end of file diff --git a/dissmodel/geo/vector/fill.py b/dissmodel/geo/vector/fill.py index 9e73e60..f3f8491 100644 --- a/dissmodel/geo/vector/fill.py +++ b/dissmodel/geo/vector/fill.py @@ -109,9 +109,9 @@ def _generate_sample(data: _SampleData, size: int = 1) -> list[Any]: -------- >>> import random; random.seed(0) >>> _generate_sample([1, 2, 3], size=3) - [2, 1, 3] + [3, 3, 2] >>> _generate_sample({"min": 0, "max": 1}, size=2) - [0, 1] + [1, 1] """ if isinstance(data, dict): if "min" in data and "max" in data: @@ -286,12 +286,15 @@ def fill(strategy: FillStrategy | str, **kwargs: Any) -> Any: Examples -------- - >>> fill(FillStrategy.RANDOM_SAMPLE, gdf=grid, attr="state", - ... data=[0, 1], seed=42) - >>> fill("min_distance", from_gdf=grid, to_gdf=roads, - ... attr_name="dist_road") - >>> fill(FillStrategy.PATTERN, gdf=grid, attr="zone", - ... pattern=[[1, 2], [3, 4]]) + ```python + # grid, roads: existing GeoDataFrames (e.g. from vector_grid / read_file) + fill(FillStrategy.RANDOM_SAMPLE, gdf=grid, attr="state", + data=[0, 1], seed=42) + fill("min_distance", from_gdf=grid, to_gdf=roads, + attr_name="dist_road") + fill(FillStrategy.PATTERN, gdf=grid, attr="zone", + pattern=[[1, 2], [3, 4]]) + ``` """ key = FillStrategy(strategy) if key not in _fill_strategies: diff --git a/dissmodel/geo/vector/neighborhood.py b/dissmodel/geo/vector/neighborhood.py index 2345c2e..0e9d82d 100644 --- a/dissmodel/geo/vector/neighborhood.py +++ b/dissmodel/geo/vector/neighborhood.py @@ -127,10 +127,14 @@ def attach_neighbors( Examples -------- - >>> from libpysal.weights import Queen - >>> gdf = attach_neighbors(gdf, strategy=Queen) - >>> gdf = attach_neighbors(gdf, neighbors_dict="neighborhood.json") - >>> gdf = attach_neighbors(gdf, strategy=Queen, ids="cell_id") + ```python + from libpysal.weights import Queen + + # gdf: an existing GeoDataFrame (e.g. from vector_grid or read_file) + gdf = attach_neighbors(gdf, strategy=Queen) + gdf = attach_neighbors(gdf, neighbors_dict="neighborhood.json") + gdf = attach_neighbors(gdf, strategy=Queen, ids="cell_id") + ``` """ resolved = _resolve_neighbors_dict(neighbors_dict) @@ -173,8 +177,10 @@ def get_neighbors(gdf: gpd.GeoDataFrame, idx: Any) -> list[Any]: Examples -------- - >>> get_neighbors(gdf, "10-5") - ['9-5', '11-5', '10-4', '10-6'] + ```python + # gdf: a GeoDataFrame with a "_neighs" column from attach_neighbors() + get_neighbors(gdf, "10-5") # ['9-5', '11-5', '10-4', '10-6'] + ``` """ if "_neighs" not in gdf.columns: raise ValueError( @@ -216,8 +222,10 @@ def get_neighbor_values( Examples -------- - >>> get_neighbor_values(gdf, "10-5", "land_use") - [1, 1, 2, 1] + ```python + # gdf: a GeoDataFrame with a "_neighs" column from attach_neighbors() + get_neighbor_values(gdf, "10-5", "land_use") # [1, 1, 2, 1] + ``` """ neighbors = get_neighbors(gdf, idx) if attr not in gdf.columns: @@ -246,7 +254,10 @@ def export_neighbors(gdf: gpd.GeoDataFrame, path: str) -> None: Examples -------- - >>> export_neighbors(gdf, "neighborhood.json") + ```python + # gdf: a GeoDataFrame with a "_neighs" column from attach_neighbors() + export_neighbors(gdf, "neighborhood.json") + ``` """ if "_neighs" not in gdf.columns: raise ValueError( diff --git a/dissmodel/io/_xarray.py b/dissmodel/io/_xarray.py index 5b4345f..981d20f 100644 --- a/dissmodel/io/_xarray.py +++ b/dissmodel/io/_xarray.py @@ -50,9 +50,10 @@ def load_xarray(uri: str, minio_client=None, **kwargs): Examples -------- - >>> backend, checksum = load_xarray("simulation_step_42.nc") - >>> backend.band_names() - ['uso', 'alt', 'solo'] + ```python + backend, checksum = load_xarray("simulation_step_42.nc") + backend.band_names() # ['uso', 'alt', 'solo'] + ``` """ try: import xarray as xr @@ -119,10 +120,13 @@ def save_xarray( Examples -------- - >>> checksum = save_xarray(backend, "output_step_42.nc", step=42) + ```python + # backend: a RasterBackend (e.g. from load_xarray or vector_to_raster_backend) + checksum = save_xarray(backend, "output_step_42.nc", step=42) - >>> # save as Zarr (requires zarr package) - >>> checksum = save_xarray(backend, "output.zarr", step=42) + # save as Zarr (requires zarr package) + checksum = save_xarray(backend, "output.zarr", step=42) + ``` """ try: import xarray # noqa: F401 — availability check; used via to_xarray() diff --git a/dissmodel/io/convert.py b/dissmodel/io/convert.py index 457ea24..afb1596 100644 --- a/dissmodel/io/convert.py +++ b/dissmodel/io/convert.py @@ -67,15 +67,17 @@ def vector_to_raster_backend( Examples -------- - >>> # From file path - >>> b = vector_to_raster_backend( - ... "data/mangue_grid.shp", resolution=100, attrs=["uso", "alt"] - ... ) - - >>> # From in-memory GeoDataFrame - >>> import geopandas as gpd - >>> gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984") - >>> b = vector_to_raster_backend(gdf, resolution=100, attrs={"uso": -1}) + ```python + # From file path + b = vector_to_raster_backend( + "data/mangue_grid.shp", resolution=100, attrs=["uso", "alt"] + ) + + # From in-memory GeoDataFrame + import geopandas as gpd + gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984") + b = vector_to_raster_backend(gdf, resolution=100, attrs={"uso": -1}) + ``` """ try: import rasterio diff --git a/dissmodel/visualization/chart.py b/dissmodel/visualization/chart.py index ce0cc63..7c42b2d 100644 --- a/dissmodel/visualization/chart.py +++ b/dissmodel/visualization/chart.py @@ -94,9 +94,13 @@ class Chart(Model): Examples -------- - >>> env = Environment(end_time=30) - >>> Chart(show_legend=True, show_grid=True, title="SIR Model") - >>> env.run() + ```python + from dissmodel.core import Environment + + env = Environment(end_time=30) + Chart(show_legend=True, show_grid=True, title="SIR Model") + env.run() + ``` """ fig: matplotlib.figure.Figure diff --git a/dissmodel/visualization/map.py b/dissmodel/visualization/map.py index 89fecde..eb10202 100644 --- a/dissmodel/visualization/map.py +++ b/dissmodel/visualization/map.py @@ -50,9 +50,14 @@ class Map(Model): Examples -------- - >>> env = Environment(end_time=10) - >>> Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"}) - >>> env.run() + ```python + from dissmodel.core import Environment + + # grid: a GeoDataFrame (e.g. from vector_grid or read_file) + env = Environment(end_time=10) + Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"}) + env.run() + ``` """ # Narrowing the base Model.setup(**kwargs) contract is intentional: diff --git a/dissmodel/visualization/widgets.py b/dissmodel/visualization/widgets.py index f47ae36..3b0dbf7 100644 --- a/dissmodel/visualization/widgets.py +++ b/dissmodel/visualization/widgets.py @@ -30,8 +30,11 @@ def display_inputs(obj: Any, st: Any) -> None: Examples -------- - >>> display_inputs(sir_model, st.sidebar) - >>> display_inputs(ca_model, st) + ```python + # sir_model, ca_model: Model instances; st: the imported streamlit module + display_inputs(sir_model, st.sidebar) + display_inputs(ca_model, st) + ``` """ annotations: dict[str, Any] = getattr(obj, "__annotations__", {})