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
31 changes: 31 additions & 0 deletions doc/development/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,37 @@ def test_something_which_changes_the_environment(xy_dataset_session):
...
```

## Type checking

The _ty_, _pyrefly_, _Zuban_, _pyright_ and _basedpyright_ type checkers are
configured in `pyproject.toml` without any machine-specific paths. The
`grass.lib` package is generated during the build, so the checkers need to be
pointed to a built or installed GRASS, in one of two ways.

On the command line and in CI, run the checker in a GRASS session. The `grass`
command sets `PYTHONPATH` to the Python code of the build or installation:

```bash
grass --tmp-project XY --exec ty check
```

For editors such as VS Code, which start the language server themselves, create
a virtual environment and select it as the interpreter. The script adds a
`.pth` file to it with the location of the Python code of the GRASS found on
`PATH` (or given with `--grass`). Rerun it when switching to another build:

```bash
python utils/typecheck_env.py --grass build/output/bin/grass
ty check
```

The `grass` package is taken from the build or installation, not from
`python/grass`, so rebuild after changing the source to see the change in
imports from other modules. Missing optional dependencies, such as wxPython or
matplotlib, are reported as unresolved imports unless installed in the
environment. The existing results are not clean yet, so these checks are not
enforced.

## More information

- [grass.gunittest documentation](https://grass.osgeo.org/grass-devel/manuals/libpython/gunittest_testing.html)
Expand Down
35 changes: 33 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -899,12 +899,43 @@ additional-builtins = ["_"]
exclude = ["**/__pycache__", "**/OBJ.*", "bin.*/**", "dist.*/**"]
include = ["python"]

# Set extraPath to the output of $(grass --config python_path)

# *GRASS modified*
# *GRASS TODO: keep in sync with MIN_PYTHON_VERSION supported*
pythonVersion = "3.11"

# Type checkers find the generated code (grass.lib, in the build or install
# tree) from outside the repository, so no machine-specific paths are needed
# here. See the "Type checking" section in doc/development/testing.md.
venvPath = "."
venv = ".venv"

[tool.ty.environment]
# *GRASS TODO: keep in sync with MIN_PYTHON_VERSION supported*
python-version = "3.11"
# By default ty also puts python/ first, which hides grass.lib because ty does
# not look for the rest of a package in other search paths. With only the
# repository root, the grass package comes from the GRASS build or installation,
# where it is complete with grass.lib.
root = ["."]

[tool.ty.src]
include = ["python"]
exclude = [
"**/__pycache__",
"python/grass/temporal/ply",
"python/libgrass_interface_generator",
]

[tool.pyrefly]
# *GRASS TODO: keep in sync with MIN_PYTHON_VERSION supported*
python-version = "3.11"
project-includes = ["python"]
project-excludes = [
"**/__pycache__",
"python/grass/temporal/ply",
"python/libgrass_interface_generator",
]

[tool.pyrefact]
line_length = 88

Expand Down
93 changes: 93 additions & 0 deletions utils/typecheck_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3

############################################################################
#
# MODULE: typecheck_env.py
#
# PURPOSE: Create a virtual environment for type checkers (ty, pyrefly,
# pyright) which provides the generated GRASS Python code
#
# COPYRIGHT: (C) 2026 by the GRASS Development Team
#
# This program is free software under the GNU General Public
# License (>=v2). Read the file COPYING that comes with GRASS
# for details.
#
#############################################################################

"""Make the generated code of a GRASS build or installation visible to type checkers

The grass.lib package is generated during the build, so it is not in the source
tree. This script creates a virtual environment and adds a .pth file with the
location of the Python code of a built or installed GRASS to it. Type checkers
read the environment, so the project configuration does not need any
machine-specific paths.

Usage:

python utils/typecheck_env.py
python utils/typecheck_env.py --grass build/output/bin/grass --venv .venv
"""

import argparse
import shutil
import subprocess
import sys
import venv
from pathlib import Path

PTH_NAME = "grass_generated.pth"


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"--grass",
default="grass",
help="GRASS executable of the build or installation (default: grass on PATH)",
)
parser.add_argument(
"--venv",
type=Path,
default=Path(".venv"),
help="virtual environment to create or update (default: .venv)",
)
args = parser.parse_args()

grass = shutil.which(args.grass)
if not grass:
sys.exit(f"GRASS executable '{args.grass}' not found")
python_path = subprocess.run(
[grass, "--config", "python_path"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
if not (Path(python_path) / "grass" / "lib").is_dir():
sys.exit(f"No grass.lib in {python_path}, is GRASS built with Python support?")

if not args.venv.exists():
venv.create(args.venv, with_pip=False, symlinks=True)
bin_dir = "Scripts" if sys.platform == "win32" else "bin"
venv_python = (
args.venv / bin_dir / ("python.exe" if bin_dir == "Scripts" else "python")
)
site_packages = Path(
subprocess.run(
[
str(venv_python),
"-c",
"import sysconfig; print(sysconfig.get_path('purelib'))",
],
check=True,
capture_output=True,
text=True,
).stdout.strip()
)
(site_packages / PTH_NAME).write_text(python_path + "\n", encoding="utf-8")
print(f"Added {python_path} to {args.venv}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading