Skip to content

grass.script,grass.temporal: Fix doctests and run them in gunittest - #745

Open
echoix wants to merge 6 commits into
pygrass-doctests-gunittestfrom
doctests-temporal-script
Open

echoix wants to merge 6 commits into
pygrass-doctests-gunittestfrom
doctests-temporal-script

Conversation

@echoix

@echoix echoix commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Description

Builds on #744 (this PR is based on that branch, so only the last three commits are new).

  • grass.gunittest: the gettext workaround for doctests replaced only sys.displayhook. Doctest resets it to sys.__displayhook__ before each example, so printed values replaced the _ gettext function in builtins and later _() calls failed. sys.__displayhook__ is replaced as well.
  • grass.temporal: the doctest wrapper was disabled with "stalls" for Python 3. It does not stall, but about half of the doctests failed (missing imports, run_command() returning None, dictionary order, exceptions in Python 3). Fix them and enable the wrapper. STRDSMetadata.get_semantic_labels() now returns None without a register table instead of querying a table named None.
  • grass.script: only array was in the doctest wrapper. Load all modules with doctests and fix the examples, which were written for another dataset or older Python.

Motivation and context

Doctests only run through the gunittest wrappers, so broken examples were not noticed.

How has this been tested?

python3 -m grass.gunittest.main --location-type nc --config .gunittest.cfg with nc_spm_full_v2beta1 on python/grass/script, python/grass/gunittest, python/grass/pygrass and python/grass/temporal. All files pass, except the two temporal files that are already excluded in .gunittest.cfg. The doctest wrappers also pass with different PYTHONHASHSEED values. ruff format and ruff check pass.

Not done: gui_support doctests (they expect a space time dataset in PERMANENT), and test_pygrass_modules_grid_doctests.py still loads no tests.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as before)

Checklist

  • PR title provides summary of the changes and starts with one of the
    pre-defined prefixes
  • My code follows the code style
    of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.

The changes were developed with the help of an AI assistant (Claude).


Generated by Claude Code

The doctest workaround replaced only sys.displayhook. Doctest sets
sys.displayhook to sys.__displayhook__ before each example, so the
default hook was used again and assigned the printed values to _ in
builtins, replacing the gettext function. Any later _() call in the
tested code failed with "'str' object is not callable". Replace
sys.__displayhook__ as well.

The change was developed with the help of an AI assistant (Claude).
The temporal doctest wrapper was disabled with a comment that it stalls
with Python 3. It does not stall, but about half of the doctests failed.

- Use tgis.init() with an import in the docstrings that called init()
  or get_current_mapset() without importing them, and import datetime
  where it is used.
- Do not expect 0 from run_command(), which returns None.
- Update expected output for the insertion order of dictionaries, the
  new semantic label line, floating point precision, and exceptions
  which are qualified in Python 3.
- Call get_time_tuple_function() with a list of maps and call the
  private method of an instance in _update_where_statement_by_spatial_extent().
- Return None from STRDSMetadata.get_semantic_labels() when
  there is no register table, instead of querying a table named None.
- Enable the wrapper and the list_stds doctests.

The changes were developed with the help of an AI assistant (Claude).
Only the grass.script.array doctests were run. Load the doctests of the
other modules in the gunittest wrapper and fix the examples, which were
written for another dataset or an older Python and never ran.

- Use the maps of the CI dataset (lsat7_2002_*, lakes) and qualify a mask
  from another mapset with its mapset name.
- Import grass.script as gs where it is used, use gs.run_command() and
  continue multiline examples with "...".
- Do not expect 0 from run_command() and write_command(), which return
  None.
- Use "map" instead of "raster" in the r.support calls, and overwrite
  maps and regions which are created by more than one example.
- Match values which are random or differ between runs and builds
  (temporary names, version(), Popen repr, mapset names) with ellipsis.
- Update expected output for the insertion order of dictionaries and the
  results of command_info(), raster_info(), vector_info() and others.
- Use Python 3 syntax for the encoding examples, and call encode() in
  the example of encode().
- Use db_connection(force=True) so the result does not depend on whether
  the mapset already has a connection.

The changes were developed with the help of an AI assistant (Claude).
Comment thread python/grass/temporal/testsuite/parser.out Outdated
Comment thread python/grass/temporal/testsuite/parser.out Outdated
The file is written by the ply parser when the temporal algebra tests
run and was committed by accident. It does not pass the whitespace
checks of pre-commit.

The change was developed with the help of an AI assistant (Claude).
@echoix echoix changed the title grass.script, grass.temporal: Fix doctests and run them in gunittest grass.script,grass.temporal: Fix doctests and run them in gunittest Oct 14, 2026
Comment on lines -306 to +308
... gs.run_command("r.mask", raster="state_boundary")
... gs.parse_command("r.univar", map="elevation", format="json")
... gs.run_command("r.mask", raster="lakes")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find more about the old raster. I don't really like changing layers like that

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state_boundary was added in OSGeo#2392 (Feb 2025) as an illustrative name. It is not in nc_spm_full_v2beta1, nc_spm_08_grass7, or anywhere else in the repository, so the example could never run. The closest real raster is ncmask_500m, but it covers the whole elevation region, so the mask would change nothing (n stays 2025000). lakes does mask cells (n = 36011), so the example shows an effect. Happy to switch to ncmask_500m if you prefer keeping the state-wide idea.


Generated by Claude Code

Comment thread python/grass/script/raster.py Outdated
Comment on lines +365 to +366
... "r.mapcalc", expression="masked_elevation = elevation", overwrite=True
... )

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you find what needed you to add overwrite? Just have different names for different examples

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples in the same docstrings reused output names (slope, study_area) in one mapset. Fixed in 71eaef1 with distinct names (slope_activated, study_area_env), and all the overwrite=True I added are removed. The one on manager.save("elevation_region", overwrite=True) was already there, so I left it.


Generated by Claude Code

Comment thread python/grass/script/raster.py Outdated
... "r.mapcalc", expression=f"masked_elevation = elevation", env=manager.env
... "r.mapcalc",
... expression="masked_elevation = elevation",
... overwrite=True,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make sure overwrite is really needed, and not a workaround

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a workaround: the two examples both wrote masked_elevation, so the second needed overwrite. In 71eaef1 the second one writes masked_elevation_env and overwrite=True is gone.


Generated by Claude Code

Comment on lines +12 to +19
import grass.script.core as gcore
import grass.script.db as gdb
import grass.script.imagery as gimagery
import grass.script.raster as graster
import grass.script.raster3d as graster3d
import grass.script.task as gtask
import grass.script.utils as gutils
import grass.script.vector as gvector

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like these renames, can you justifie more on this?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was no strong reason for the aliases. In 71eaef1 the modules are loaded by name in a loop (DocTestSuite(f"grass.script.{name}")), so there are no new imports or renames.


Generated by Claude Code

Comment thread python/grass/script/vector.py Outdated
PRECISION'}, 'SHAPE_len': {'index': 7, 'type': 'DOUBLE PRECISION'},
'cat': {'index': 0, 'type': 'INTEGER'}, 'GEOL250_ID': {'index': 4, 'type':
'INTEGER'}, 'GEO_NAME': {'index': 5, 'type': 'CHARACTER'}}
{'cat': {'type': 'INTEGER', 'index': 0}, 'onemap_pro': {'type': 'DOUBLE PRECISION', 'index': 1}, 'PERIMETER': {'type': 'DOUBLE PRECISION', 'index': 2}, 'GEOL250_': {'type': 'INTEGER', 'index': 3}, 'GEOL250_ID': {'type': 'INTEGER', 'index': 4}, 'GEO_NAME': {'type': 'CHARACTER', 'index': 5}, 'SHAPE_area': {'type': 'DOUBLE PRECISION', 'index': 6}, 'SHAPE_len': {'type': 'DOUBLE PRECISION', 'index': 7}}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be reformatted? Also, I thought that our doctest examples were reformatted with ruff, how come this wasn't?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformatted in 71eaef1, with the results wrapped across lines and +NORMALIZE_WHITESPACE. Ruff's docstring-code-format only formats the code after >>>, never the expected output lines, and E501 is ignored, so nothing flagged the long lines. I pasted the actual outputs without wrapping them.


Generated by Claude Code

Give each doctest example its own output name instead of overwriting the
output of an earlier example in the same mapset, wrap the long expected
results across lines, and load the grass.script modules by name in the
doctest wrapper instead of importing each one under a new alias.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants