From 3c9fafbf37191995579d1f627336c197548e8250 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:10:00 +0000 Subject: [PATCH 1/3] pyproject.toml: Add ty and pyrefly config and a type checking environment script Add [tool.ty] and [tool.pyrefly] sections without machine-specific paths. The new utils/typecheck_env.py creates a virtual environment whose .pth file points to the Python code of a GRASS build or installation, so that type checkers find the generated grass.lib package. Pyright is pointed to the same environment. The configuration and script were written with AI assistance. --- pyproject.toml | 37 ++++++++++++++++- utils/typecheck_env.py | 93 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 utils/typecheck_env.py diff --git a/pyproject.toml b/pyproject.toml index 71be308a430..b0b7fa879ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -899,12 +899,45 @@ 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 (pyright, ty, pyrefly) find the generated code (grass.lib, in the +# build or install tree) through a virtual environment, so no machine-specific +# paths are needed here. Run utils/typecheck_env.py to create .venv. Pyright +# looks for it here, ty finds .venv on its own, and for pyrefly use +# --python-interpreter-path or activate the environment. +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 virtual environment, 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 diff --git a/utils/typecheck_env.py b/utils/typecheck_env.py new file mode 100644 index 00000000000..b5953e10ef8 --- /dev/null +++ b/utils/typecheck_env.py @@ -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()) From 4887c5012b258af884f781e82b1f81a8a78e5617 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:41:38 +0000 Subject: [PATCH 2/3] pyproject.toml: Run type checkers in a GRASS session instead of a virtual environment The grass launcher sets PYTHONPATH to the Python code of the build or installation, which ty, pyrefly, pyright, basedpyright and Zuban all use to find the generated grass.lib package. This removes the need for a helper script and for any machine-specific path in the configuration. --- pyproject.toml | 15 +++---- utils/typecheck_env.py | 93 ------------------------------------------ 2 files changed, 6 insertions(+), 102 deletions(-) delete mode 100644 utils/typecheck_env.py diff --git a/pyproject.toml b/pyproject.toml index b0b7fa879ba..e0232a610ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -903,21 +903,18 @@ include = ["python"] # *GRASS TODO: keep in sync with MIN_PYTHON_VERSION supported* pythonVersion = "3.11" -# Type checkers (pyright, ty, pyrefly) find the generated code (grass.lib, in the -# build or install tree) through a virtual environment, so no machine-specific -# paths are needed here. Run utils/typecheck_env.py to create .venv. Pyright -# looks for it here, ty finds .venv on its own, and for pyrefly use -# --python-interpreter-path or activate the environment. -venvPath = "." -venv = ".venv" +# Type checkers find the generated code (grass.lib, in the build or install +# tree) through PYTHONPATH, which the grass launcher sets, so no machine-specific +# paths are needed here. Run them in a GRASS session, for example: +# grass --tmp-project XY --exec ty check [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 virtual environment, where -# it is complete with grass.lib. +# repository root, the grass package comes from PYTHONPATH, where it is complete +# with grass.lib. root = ["."] [tool.ty.src] diff --git a/utils/typecheck_env.py b/utils/typecheck_env.py deleted file mode 100644 index b5953e10ef8..00000000000 --- a/utils/typecheck_env.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/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()) From 965b7b00a9845c1ce12b4811225c3658dbcf12bc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:53:33 +0000 Subject: [PATCH 3/3] doc: Document type checking with ty, pyrefly and others, with an environment script for editors Describe two ways to let type checkers find the generated grass.lib package: running them in a GRASS session, and a virtual environment created by utils/typecheck_env.py for editors. The configuration and script were written with AI assistance. --- doc/development/testing.md | 31 +++++++++++++ pyproject.toml | 11 +++-- utils/typecheck_env.py | 93 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 utils/typecheck_env.py diff --git a/doc/development/testing.md b/doc/development/testing.md index 90ac17e38ae..3284dd261ea 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index e0232a610ee..1efefda39e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -904,17 +904,18 @@ include = ["python"] pythonVersion = "3.11" # Type checkers find the generated code (grass.lib, in the build or install -# tree) through PYTHONPATH, which the grass launcher sets, so no machine-specific -# paths are needed here. Run them in a GRASS session, for example: -# grass --tmp-project XY --exec ty check +# 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 PYTHONPATH, where it is complete -# with grass.lib. +# repository root, the grass package comes from the GRASS build or installation, +# where it is complete with grass.lib. root = ["."] [tool.ty.src] diff --git a/utils/typecheck_env.py b/utils/typecheck_env.py new file mode 100644 index 00000000000..b5953e10ef8 --- /dev/null +++ b/utils/typecheck_env.py @@ -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())