Skip to content

grass.pygrass: Fix doctests so they run in gunittest again - #744

Open
echoix wants to merge 3 commits into
mainfrom
pygrass-doctests-gunittest
Open

echoix wants to merge 3 commits into
mainfrom
pygrass-doctests-gunittest

Conversation

@echoix

@echoix echoix commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Description

Re-enable the gunittest, pygrass raster and pygrass rpc doctest wrappers that were excluded in .gunittest.cfg, by fixing what made them fail:

  • RowIO: build the Rowio_setup() callbacks from the type declared by the generated binding and accept the fourth (row length) argument. The 3-argument callbacks were rejected by ctypes or crashed.
  • Buffer: make __array_wrap__ work with NumPy 2, which passes a plain ndarray and an additional return_scalar argument.
  • Columns: drop the database connection when pickling, so the RPC data provider can send the columns to the client process.
  • GridModule: use the parameter typedesc instead of its Python type when adding the mapset to input maps, and handle multiple values and vector maps. Inputs never got a mapset before, so tiles failed.
  • get_raster_for_points: do not depend on the NumPy scalar repr in the doctest.

Motivation and context

Doctests only run through gunittest wrappers. Several of them were excluded or silently broken, so regressions in the documented examples went unnoticed.

How has this been tested?

Ran python3 -m grass.gunittest.main --location-type nc --config .gunittest.cfg (as in test_thorough.sh) with the nc_spm_full_v2beta1 dataset on python/grass/gunittest, python/grass/pygrass/raster and python/grass/pygrass/rpc: all files pass. All other pygrass doctest wrappers also pass. Existing pygrass raster, vector and table tests give the same results as before.

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

Enable the gunittest, pygrass raster and pygrass rpc doctest wrappers
that were excluded in .gunittest.cfg.

- RowIO: Build the Rowio_setup() callbacks from the type declared by the
  generated binding and accept the fourth (row length) argument. The
  three-argument callbacks were rejected by ctypes or crashed.
- Buffer: Make __array_wrap__ work with NumPy 2, which passes a plain
  ndarray and an additional return_scalar argument.
- Columns: Drop the database connection when pickling, so that the RPC
  data provider can send the columns to the client process.
- GridModule: Look at the parameter typedesc instead of its Python type
  when adding the mapset to input maps, and handle multiple values and
  vector maps. Before, inputs never got a mapset and tiles failed.
- get_raster_for_points: Do not depend on the NumPy scalar repr in the
  doctest.

The changes were developed with the help of an AI assistant (Claude).
get_raster_for_points() opens the vector map if it is not open and does
not close it. The doctest then opened the map again without closing it,
so the first handle stayed open. Windows cannot delete files which are
open, so the final remove() failed with "Unable to delete file" and the
doctest wrapper failed on Windows.

The change was developed with the help of an AI assistant (Claude).
@echoix echoix changed the title pygrass: Fix doctests so they run in gunittest again grass.pygrass: Fix doctests so they run in gunittest again Oct 14, 2026
Comment on lines +654 to +664
if inm.typedesc not in {"raster", "vector"} or not inm.value:
continue
find_mapset = (
get_mapset_raster if inm.typedesc == "raster" else get_mapset_vector
)
names = inm.value if isinstance(inm.value, list) else [inm.value]
qualified = [
name if "@" in name else "%s@%s" % (name, find_mapset(name))
for name in names
]
inm.value = qualified if isinstance(inm.value, list) else qualified[0]

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 seems hard to follow.

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.

Agreed, the nested conditional expressions were hard to read. In 34034ec it is now a plain if/elif that picks the lookup function, and a small _with_mapset() helper qualifies one name. The list and single value cases are handled in one place.


Generated by Claude Code

Comment thread python/grass/pygrass/utils.py Outdated

Add a new column and sample again

>>> vect.close()

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.

Why is this not close to the same bloc of code that opens it?

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 should be. The call to get_raster_for_points() is what opens the map, so in 34034ec the vect.close() comes right after that call and its checks, with a short note, instead of at the start of the next block.


Generated by Claude Code

Comment thread python/grass/pygrass/raster/rowio.py Outdated
Comment on lines +16 to +19
# The callback type must be the one the generated binding declares for
# Rowio_setup(). It takes the row length as a fourth argument, and an equivalent
# type created with ctypes.CFUNCTYPE() is rejected by ctypes.
CMPFUNC = librowio.Rowio_setup.argtypes[4]

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 understand correctly what's it's doing

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.

Rowio_setup() takes a callback that reads one row into a buffer, and the generated binding declares the exact ctypes type of that callback. CMPFUNC reuses that type to wrap the three Python row readers. A type made here with ctypes.CFUNCTYPE() is a different class, which ctypes rejects as an argument. I rewrote the comment in 34034ec to say this.


Generated by Claude Code

Split GridModule.define_mapset_inputs() into simpler steps with a helper
that qualifies a map name, explain why the Rowio callback type comes from
the binding, and close the vector right after the call that opened it in
the get_raster_for_points() doctest.
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