|
| 1 | +# Copilot Instructions (GRASS GIS) |
| 2 | + |
| 3 | +GRASS GIS is a geospatial processing engine with 500+ tools (modules) for |
| 4 | +raster, vector, imagery, temporal, and 3D analysis. Each tool is self-contained |
| 5 | +with source, docs, Makefile, and tests in one directory. |
| 6 | + |
| 7 | +## Repository Layout |
| 8 | + |
| 9 | +| Path | Contents | |
| 10 | +| ---- | -------- | |
| 11 | +| `raster/r.*/` | Raster tools (C, e.g. `raster/r.slope.aspect/`) | |
| 12 | +| `vector/v.*/` | Vector tools (C/Python) | |
| 13 | +| `temporal/t.*/` | Temporal framework tools (Python) | |
| 14 | +| `display/d.*/` | Display/rendering tools (C) | |
| 15 | +| `general/g.*/` | General management tools | |
| 16 | +| `db/db.*/` | Database tools | |
| 17 | +| `imagery/i.*/` | Imagery processing tools | |
| 18 | +| `scripts/` | Python script tools (e.g. `scripts/r.grow/r.grow.py`) | |
| 19 | +| `lib/` | Shared C libraries (`lib/gis/`, `lib/raster/`, `lib/vector/`, etc.) | |
| 20 | +| `python/grass/` | Python packages (`script/`, `pygrass/`, `gunittest/`, `temporal/`, `tools/`) | |
| 21 | +| `gui/wxpython/` | wxPython GUI | |
| 22 | + |
| 23 | +## Module Directory Structure |
| 24 | + |
| 25 | +Each module directory contains: |
| 26 | + |
| 27 | +- `Makefile` — links to `include/Make/Module.make` (C) |
| 28 | + or `include/Make/Script.make` (Python) |
| 29 | +- `main.c` or `<tool>.py` — source code |
| 30 | +- `<tool>.md` — documentation (Markdown, **no header/footer**) |
| 31 | +- `tests/` — pytest tests (preferred for new tests) |
| 32 | +- `testsuite/` — legacy gunittest tests |
| 33 | + |
| 34 | +## Build System |
| 35 | + |
| 36 | +GRASS has two build systems. **CMake + Ninja** is recommended for development: |
| 37 | + |
| 38 | +```bash |
| 39 | +cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX="$HOME/install" |
| 40 | +cmake --build build -j$(nproc) |
| 41 | +cmake --install build |
| 42 | +``` |
| 43 | + |
| 44 | +Autotools is also supported: |
| 45 | +`./configure --prefix=$PREFIX && make -j$(nproc) && make install` |
| 46 | + |
| 47 | +## Testing |
| 48 | + |
| 49 | +Two test frameworks coexist: |
| 50 | + |
| 51 | +**pytest** (preferred for new tests) — files in `<module>/tests/`: |
| 52 | + |
| 53 | +```python |
| 54 | +# tests/conftest.py — typical fixture pattern |
| 55 | +import grass.script as gs |
| 56 | +from grass.tools import Tools |
| 57 | + |
| 58 | +@pytest.fixture |
| 59 | +def xy_dataset_session(tmp_path): |
| 60 | + project = tmp_path / "xy_test" |
| 61 | + gs.create_project(project) |
| 62 | + with gs.setup.init(project, env=os.environ.copy()) as session, \ |
| 63 | + Tools(session=session) as tools: |
| 64 | + tools.g_region(s=0, n=5, w=0, e=6, res=1) |
| 65 | + tools.r_mapcalc(expression="rows_raster = row()") |
| 66 | + yield session |
| 67 | + |
| 68 | +# tests/my_tool_test.py |
| 69 | +def test_output(xy_dataset_session): |
| 70 | + tools = Tools(session=xy_dataset_session, consistent_return_value=True) |
| 71 | + tools.r_slope_aspect(elevation="rows_raster", slope="slope_out") |
| 72 | + stats = tools.r_univar(map="slope_out", format="json") |
| 73 | + assert stats["min"] == 45 |
| 74 | +``` |
| 75 | + |
| 76 | +**gunittest** (legacy) — files in `<module>/testsuite/`, based on |
| 77 | +`grass.gunittest.case.TestCase` with assertions like `assertModule()`, |
| 78 | +`assertRasterMinMax()`. |
| 79 | + |
| 80 | +Run tests: |
| 81 | + |
| 82 | +```bash |
| 83 | +# pytest (from build directory or with GRASS in PATH) |
| 84 | +pytest raster/r.slope.aspect/tests/ |
| 85 | +# gunittest (requires NC sample dataset) |
| 86 | +grass --tmp-project XY --exec python3 -m grass.gunittest.main \ |
| 87 | + --grassdata $HOME --location nc_spm_full_v2beta1 --min-success 100 |
| 88 | +``` |
| 89 | + |
| 90 | +## Critical Coding Conventions |
| 91 | + |
| 92 | +These are GRASS-specific rules that differ from general practice. See |
| 93 | +`.github/instructions/` for full details. |
| 94 | + |
| 95 | +### Python Tools |
| 96 | + |
| 97 | +- **Import**: always `import grass.script as gs` |
| 98 | +- **Translatable messages**: use `_()` with `str.format()`, never f-strings: |
| 99 | + `gs.warning(_("Raster map <{}> not found.").format(name))` |
| 100 | +- **Non-translatable strings**: f-strings are fine: |
| 101 | + `expression = f"{output} = {input} * 3"` |
| 102 | +- **Parser interface**: defined via special comments, not argparse: |
| 103 | + |
| 104 | + ```python |
| 105 | + # %Module |
| 106 | + # % description: Short tool description |
| 107 | + # % keyword: raster |
| 108 | + # %end |
| 109 | + # %option G_OPT_R_INPUT |
| 110 | + # %end |
| 111 | + # %option G_OPT_R_OUTPUT |
| 112 | + # %end |
| 113 | + ``` |
| 114 | + |
| 115 | +- **Computational region**: never change globally. Use context manager: |
| 116 | + `with gs.RegionManager(raster=input_map): ...` |
| 117 | +- **Raster mask**: never set/remove globally. Use: |
| 118 | + `with gs.MaskManager(): gs.run_command("r.mask", raster=mask)` |
| 119 | +- **Temporary maps**: use `gs.append_node_pid("tmp_name")` + `atexit` cleanup |
| 120 | +- **Record history**: call `gs.raster_history(output)` or `gs.vector_history(output)` |
| 121 | +- **Overwrite**: never overwrite outputs unless the user passes `--overwrite` |
| 122 | +- **Mapsets**: outputs go to the current mapset; inputs may come from any mapset. |
| 123 | + Do not hard-code file paths for geodata; use GRASS map names. |
| 124 | +- **Messages**: map names in `<angle brackets>`, file paths in `'single quotes'`, |
| 125 | + capitalize, no contractions, use `gs.message()` / `gs.fatal()` / `gs.warning()` |
| 126 | + (never `print()` for messages) |
| 127 | + |
| 128 | +### C Tools |
| 129 | + |
| 130 | +- Use GRASS APIs: `G_malloc()`, `G_fatal_error()`, `G_warning()`, `Rast_open_old()` |
| 131 | +- Header include order: system → non-core libs → `grass/*.h` → local headers |
| 132 | +- Format with `clang-format` (v18+); snake_case for function names |
| 133 | +- Exit with `EXIT_SUCCESS` / `EXIT_FAILURE` |
| 134 | + |
| 135 | +### Documentation (`<tool>.md`) |
| 136 | + |
| 137 | +Required sections: DESCRIPTION, SEE ALSO, AUTHORS. |
| 138 | +Suggested: NOTES, EXAMPLES. Tool names in italics (`*r.slope.aspect*`), |
| 139 | +flags/params bold (`**-n**`, `**input**`), shell values in backticks. |
| 140 | +See `.github/instructions/docs.instructions.md` for full markup guide. |
| 141 | + |
| 142 | +## Formatting & Linting |
| 143 | + |
| 144 | +Pre-commit runs all checks. Install once, then it runs on every commit: |
| 145 | + |
| 146 | +```bash |
| 147 | +python -m pip install pre-commit && pre-commit install |
| 148 | +# Manual run: |
| 149 | +pre-commit run --all-files |
| 150 | +# Target specific files: |
| 151 | +pre-commit run --files raster/r.slope.aspect/* |
| 152 | +``` |
| 153 | + |
| 154 | +Key tools: `ruff format` + `ruff check` (Python), `clang-format` (C/C++), |
| 155 | +`markdownlint` (docs). Config in `pyproject.toml`, `.clang-format`, |
| 156 | +`.pre-commit-config.yaml`. |
| 157 | + |
| 158 | +## Key Reference Files |
| 159 | + |
| 160 | +- Python conventions: `.github/instructions/python.instructions.md` |
| 161 | +- C conventions: `.github/instructions/c.instructions.md` |
| 162 | +- Doc markup guide: `.github/instructions/docs.instructions.md` |
| 163 | +- General rules: `.github/instructions/general.instructions.md` |
| 164 | +- [GRASS Python API docs](https://grass.osgeo.org/grass-devel/manuals/libpython/) |
| 165 | +- [GRASS C Programmer's Manual](https://grass.osgeo.org/programming8/) |
0 commit comments