From 59c95cc2a620594ce04e926495c17f9efc7bcf36 Mon Sep 17 00:00:00 2001 From: Kriti Birda <164247895+kritibirda26@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:18:25 +0530 Subject: [PATCH] r.info: Add JSON output (#3744) Uses parson to add JSON output format support to the r.info module. The tool has various flags to control the fields being output in case of the shell script style format (aka plain in the code). The JSON output always contains all fields even without the flags being specified, except for statistics. The statistics must be set with a flag because the statistics are only optionally stored in the raster data and computing them on the fly takes a lot of time. The option value for format is plain (shell/key-value) and json and internally, the plain, human output is treated separately from the machine readable formats which are PLAIN and JSON. --- raster/r.info/Makefile | 2 +- raster/r.info/main.c | 426 ++++++++++++++++++++----- raster/r.info/r.info.html | 45 +++ raster/r.info/testsuite/test_r_info.py | 72 ++++- 4 files changed, 458 insertions(+), 87 deletions(-) diff --git a/raster/r.info/Makefile b/raster/r.info/Makefile index 3cfdfce125a..a04fa7e4749 100644 --- a/raster/r.info/Makefile +++ b/raster/r.info/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = r.info -LIBES = $(RASTERLIB) $(GISLIB) +LIBES = $(RASTERLIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(RASTERDEP) $(GISDEP) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/raster/r.info/main.c b/raster/r.info/main.c index 7951bc1e343..19292673c31 100644 --- a/raster/r.info/main.c +++ b/raster/r.info/main.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "local_proto.h" #define printline(x) fprintf(out, " | %-74.74s |\n", x) @@ -34,6 +35,8 @@ fprintf(out, "-"); \ fprintf(out, "%c\n", x) +enum OutputFormat { PLAIN, JSON }; + /* local prototypes */ static void format_double(const double, char *); static void compose_line(FILE *, const char *, ...); @@ -42,7 +45,7 @@ int main(int argc, char **argv) { const char *name, *mapset; const char *title; - char tmp1[100], tmp2[100], tmp3[100]; + char tmp1[100], tmp2[100], tmp3[100], tmp4[100]; char timebuff[256]; <<<<<<< HEAD <<<<<<< HEAD @@ -96,8 +99,12 @@ int main(int argc, char **argv) RASTER_MAP_TYPE data_type; struct Reclass reclass; struct GModule *module; - struct Option *opt1; + struct Option *opt1, *fopt; struct Flag *gflag, *rflag, *eflag, *hflag, *sflag; + enum OutputFormat format; + + JSON_Value *root_value; + JSON_Object *root_object; /* Initialize GIS Engine */ G_gisinit(argv[0]); @@ -133,6 +140,9 @@ int main(int argc, char **argv) hflag->key = 'h'; hflag->description = _("Print raster history instead of info"); + fopt = G_define_standard_option(G_OPT_F_FORMAT); + fopt->guisection = _("Print"); + if (G_parser(argc, argv)) exit(EXIT_FAILURE); @@ -145,6 +155,15 @@ int main(int argc, char **argv) if ((mapset = G_find_raster2(name, "")) == NULL) G_fatal_error(_("Raster map <%s> not found"), name); + if (strcmp(fopt->answer, "json") == 0) { + format = JSON; + root_value = json_value_init_object(); + root_object = json_value_get_object(root_value); + } + else { + format = PLAIN; + } + Rast_get_cellhd(name, "", &cellhd); cats_ok = Rast_read_cats(name, "", &cats) >= 0; title = Rast_get_cats_title(&cats); @@ -222,7 +241,7 @@ int main(int argc, char **argv) } if (!gflag->answer && !rflag->answer && !sflag->answer && !eflag->answer && - !hflag->answer) { + !hflag->answer && format == PLAIN) { divider('+'); compose_line(out, "Map: %-29.29s Date: %s", name, @@ -507,7 +526,7 @@ int main(int argc, char **argv) else { /* g, r, s, e, or h flags */ int need_range, have_range, need_stats, have_stats; - need_range = rflag->answer; + need_range = rflag->answer || format == JSON; need_stats = sflag->answer; if (need_stats) need_range = 1; @@ -584,52 +603,96 @@ int main(int argc, char **argv) } } - if (gflag->answer) { - G_format_northing(cellhd.north, tmp1, -1); - G_format_northing(cellhd.south, tmp2, -1); - fprintf(out, "north=%s\n", tmp1); - fprintf(out, "south=%s\n", tmp2); - - G_format_easting(cellhd.east, tmp1, -1); - G_format_easting(cellhd.west, tmp2, -1); - fprintf(out, "east=%s\n", tmp1); - fprintf(out, "west=%s\n", tmp2); - - G_format_resolution(cellhd.ns_res, tmp3, -1); - fprintf(out, "nsres=%s\n", tmp3); - - G_format_resolution(cellhd.ew_res, tmp3, -1); - fprintf(out, "ewres=%s\n", tmp3); - - fprintf(out, "rows=%d\n", cellhd.rows); - fprintf(out, "cols=%d\n", cellhd.cols); - - fprintf(out, "cells=%" PRId64 "\n", - (grass_int64)cellhd.rows * cellhd.cols); - - fprintf(out, "datatype=%s\n", - (data_type == CELL_TYPE - ? "CELL" - : (data_type == DCELL_TYPE - ? "DCELL" - : (data_type == FCELL_TYPE ? "FCELL" : "??")))); - if (cats_ok) - format_double((double)cats.num, tmp1); - fprintf(out, "ncats=%s\n", cats_ok ? tmp1 : "??"); + if (gflag->answer || format == JSON) { + const char *data_type_f = + (data_type == CELL_TYPE + ? "CELL" + : (data_type == DCELL_TYPE + ? "DCELL" + : (data_type == FCELL_TYPE ? "FCELL" : "??"))); + grass_int64 total_cells = (grass_int64)cellhd.rows * cellhd.cols; + + switch (format) { + case PLAIN: + G_format_northing(cellhd.north, tmp1, -1); + G_format_northing(cellhd.south, tmp2, -1); + fprintf(out, "north=%s\n", tmp1); + fprintf(out, "south=%s\n", tmp2); + + G_format_easting(cellhd.east, tmp1, -1); + G_format_easting(cellhd.west, tmp2, -1); + fprintf(out, "east=%s\n", tmp1); + fprintf(out, "west=%s\n", tmp2); + + G_format_resolution(cellhd.ns_res, tmp3, -1); + fprintf(out, "nsres=%s\n", tmp3); + + G_format_resolution(cellhd.ew_res, tmp3, -1); + fprintf(out, "ewres=%s\n", tmp3); + + fprintf(out, "rows=%d\n", cellhd.rows); + fprintf(out, "cols=%d\n", cellhd.cols); + + fprintf(out, "cells=%" PRId64 "\n", total_cells); + + fprintf(out, "datatype=%s\n", data_type_f); + + if (cats_ok) + format_double((double)cats.num, tmp4); + fprintf(out, "ncats=%s\n", cats_ok ? tmp4 : "??"); + break; + case JSON: + json_object_set_number(root_object, "north", cellhd.north); + json_object_set_number(root_object, "south", cellhd.south); + json_object_set_number(root_object, "nsres", cellhd.ns_res); + + json_object_set_number(root_object, "east", cellhd.east); + json_object_set_number(root_object, "west", cellhd.west); + json_object_set_number(root_object, "ewres", cellhd.ew_res); + + json_object_set_number(root_object, "rows", cellhd.rows); + json_object_set_number(root_object, "cols", cellhd.cols); + json_object_set_number(root_object, "cells", total_cells); + + json_object_set_string(root_object, "datatype", data_type_f); + if (cats_ok) { + json_object_set_number(root_object, "ncats", cats.num); + } + else { + json_object_set_null(root_object, "ncats"); + } + break; + } } - if (rflag->answer || sflag->answer) { + if (rflag->answer || sflag->answer || format == JSON) { if (data_type == CELL_TYPE) { CELL min, max; Rast_get_range_min_max(&crange, &min, &max); if (Rast_is_c_null_value(&min)) { - fprintf(out, "min=NULL\n"); - fprintf(out, "max=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "min=NULL\n"); + fprintf(out, "max=NULL\n"); + break; + case JSON: + json_object_set_null(root_object, "min"); + json_object_set_null(root_object, "max"); + break; + } } else { - fprintf(out, "min=%i\n", min); - fprintf(out, "max=%i\n", max); + switch (format) { + case PLAIN: + fprintf(out, "min=%i\n", min); + fprintf(out, "max=%i\n", max); + break; + case JSON: + json_object_set_number(root_object, "min", min); + json_object_set_number(root_object, "max", max); + break; + } } } else { @@ -637,17 +700,33 @@ int main(int argc, char **argv) Rast_get_fp_range_min_max(&range, &min, &max); if (Rast_is_d_null_value(&min)) { - fprintf(out, "min=NULL\n"); - fprintf(out, "max=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "min=NULL\n"); + fprintf(out, "max=NULL\n"); + break; + case JSON: + json_object_set_null(root_object, "min"); + json_object_set_null(root_object, "max"); + break; + } } else { - if (data_type == FCELL_TYPE) { - fprintf(out, "min=%.7g\n", min); - fprintf(out, "max=%.7g\n", max); - } - else { - fprintf(out, "min=%.15g\n", min); - fprintf(out, "max=%.15g\n", max); + switch (format) { + case PLAIN: + if (data_type == FCELL_TYPE) { + fprintf(out, "min=%.7g\n", min); + fprintf(out, "max=%.7g\n", max); + } + else { + fprintf(out, "min=%.15g\n", min); + fprintf(out, "max=%.15g\n", max); + } + break; + case JSON: + json_object_set_number(root_object, "min", min); + json_object_set_number(root_object, "max", max); + break; } } } @@ -656,9 +735,17 @@ int main(int argc, char **argv) if (sflag->answer) { if (!gflag->answer) { + grass_int64 total_cells = + (grass_int64)cellhd.rows * cellhd.cols; /* always report total number of cells */ - fprintf(out, "cells=%" PRId64 "\n", - (grass_int64)cellhd.rows * cellhd.cols); + switch (format) { + case PLAIN: + fprintf(out, "cells=%" PRId64 "\n", total_cells); + break; + case JSON: + json_object_set_number(root_object, "cells", total_cells); + break; + } } if (rstats.count > 0) { @@ -686,39 +773,77 @@ int main(int argc, char **argv) } } - fprintf(out, "n=%" PRId64 "\n", rstats.count); - fprintf(out, "mean=%.15g\n", mean); - fprintf(out, "stddev=%.15g\n", sd); - fprintf(out, "sum=%.15g\n", rstats.sum); + switch (format) { + case PLAIN: + fprintf(out, "n=%" PRId64 "\n", rstats.count); + fprintf(out, "mean=%.15g\n", mean); + fprintf(out, "stddev=%.15g\n", sd); + fprintf(out, "sum=%.15g\n", rstats.sum); + break; + case JSON: + json_object_set_number(root_object, "n", rstats.count); + json_object_set_number(root_object, "mean", mean); + json_object_set_number(root_object, "stddev", sd); + json_object_set_number(root_object, "sum", rstats.sum); + break; + } } else { - fprintf(out, "n=0\n"); - fprintf(out, "mean=NULL\n"); - fprintf(out, "stddev=NULL\n"); - fprintf(out, "sum=NULL\n"); + switch (format) { + case PLAIN: + fprintf(out, "n=0\n"); + fprintf(out, "mean=NULL\n"); + fprintf(out, "stddev=NULL\n"); + fprintf(out, "sum=NULL\n"); + break; + case JSON: + json_object_set_number(root_object, "n", 0); + json_object_set_null(root_object, "mean"); + json_object_set_null(root_object, "stddev"); + json_object_set_null(root_object, "sum"); + break; + } } } - if (eflag->answer) { + if (eflag->answer || format == JSON) { char xname[GNAME_MAX], xmapset[GMAPSET_MAX]; + const char *maptype, *date, *creator; G_unqualified_name(name, mapset, xname, xmapset); - fprintf(out, "map=%s\n", xname); - fprintf(out, "maptype=%s\n", - hist_ok ? Rast_get_history(&hist, HIST_MAPTYPE) : "??"); - fprintf(out, "mapset=%s\n", mapset); - fprintf(out, "location=%s\n", G_location()); - fprintf(out, "project=%s\n", G_location()); - fprintf(out, "database=%s\n", G_gisdbase()); - fprintf(out, "date=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_MAPID) : "??"); - fprintf(out, "creator=\"%s\"\n", - hist_ok ? Rast_get_history(&hist, HIST_CREATOR) : "??"); - fprintf(out, "title=\"%s\"\n", title); + maptype = hist_ok ? Rast_get_history(&hist, HIST_MAPTYPE) : "??"; + date = hist_ok ? Rast_get_history(&hist, HIST_MAPID) : "??"; + creator = hist_ok ? Rast_get_history(&hist, HIST_CREATOR) : "??"; + + switch (format) { + case PLAIN: + fprintf(out, "map=%s\n", xname); + fprintf(out, "maptype=%s\n", maptype); + fprintf(out, "mapset=%s\n", mapset); + fprintf(out, "location=%s\n", G_location()); + fprintf(out, "project=%s\n", G_location()); + fprintf(out, "database=%s\n", G_gisdbase()); + fprintf(out, "date=\"%s\"\n", date); + fprintf(out, "creator=\"%s\"\n", creator); + fprintf(out, "title=\"%s\"\n", title); + break; + case JSON: + json_object_set_string(root_object, "map", name); + json_object_set_string(root_object, "maptype", maptype); + json_object_set_string(root_object, "mapset", mapset); + json_object_set_string(root_object, "location", G_location()); + json_object_set_string(root_object, "project", G_location()); + json_object_set_string(root_object, "database", G_gisdbase()); + json_object_set_string(root_object, "date", date); + json_object_set_string(root_object, "creator", creator); + json_object_set_string(root_object, "title", title); + break; + } if (time_ok && (first_time_ok || second_time_ok)) { G_format_timestamp(&ts, timebuff); +<<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD @@ -744,10 +869,29 @@ int main(int argc, char **argv) >>>>>>> osgeo-main /*Create the r.info timestamp string */ fprintf(out, "timestamp=\"%s\"\n", timebuff); +======= + switch (format) { + case PLAIN: + /*Create the r.info timestamp string */ + fprintf(out, "timestamp=\"%s\"\n", timebuff); + break; + case JSON: + json_object_set_string(root_object, "timestamp", timebuff); + break; + } +>>>>>>> 5fbf526387 (r.info: Add JSON output (#3744)) } else { - fprintf(out, "timestamp=\"none\"\n"); + switch (format) { + case PLAIN: + fprintf(out, "timestamp=\"none\"\n"); + break; + case JSON: + json_object_set_null(root_object, "timestamp"); + break; + } } +<<<<<<< HEAD fprintf(out, "units=%s\n", units ? units : "\"none\""); fprintf(out, "vdatum=%s\n", vdatum ? vdatum : "\"none\""); fprintf(out, "semantic_label=%s\n", @@ -806,25 +950,139 @@ int main(int argc, char **argv) ======= >>>>>>> osgeo-main fprintf(out, "\"\n"); +======= + + switch (format) { + case PLAIN: + fprintf(out, "units=%s\n", units ? units : "\"none\""); + fprintf(out, "vdatum=%s\n", vdatum ? vdatum : "\"none\""); + fprintf(out, "semantic_label=%s\n", + semantic_label ? semantic_label : "\"none\""); + fprintf(out, "source1=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_DATSRC_1) + : "\"none\""); + fprintf(out, "source2=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_DATSRC_2) + : "\"none\""); + fprintf(out, "description=\"%s\"\n", + hist_ok ? Rast_get_history(&hist, HIST_KEYWRD) + : "\"none\""); + if (Rast_history_length(&hist)) { + fprintf(out, "comments=\""); + for (i = 0; i < Rast_history_length(&hist); i++) + fprintf(out, "%s", Rast_history_line(&hist, i)); + fprintf(out, "\"\n"); + } + break; + case JSON: + if (units) { + json_object_set_string(root_object, "units", units); + } + else { + json_object_set_null(root_object, "units"); + } + if (vdatum) { + json_object_set_string(root_object, "vdatum", vdatum); + } + else { + json_object_set_null(root_object, "vdatum"); + } + if (semantic_label) { + json_object_set_string(root_object, "semantic_label", + semantic_label); + } + else { + json_object_set_null(root_object, "semantic_label"); + } + + if (hist_ok) { + json_object_set_string( + root_object, "source1", + Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string( + root_object, "source2", + Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string( + root_object, "description", + Rast_get_history(&hist, HIST_KEYWRD)); + JSON_Value *comments_value = json_value_init_array(); + JSON_Array *comments = json_array(comments_value); + if (Rast_history_length(&hist)) { + for (i = 0; i < Rast_history_length(&hist); i++) { + json_array_append_string( + comments, Rast_history_line(&hist, i)); + } + } + json_object_set_value(root_object, "comments", + comments_value); + } + else { + json_object_set_null(root_object, "source1"); + json_object_set_null(root_object, "source2"); + json_object_set_null(root_object, "description"); + json_object_set_null(root_object, "comments"); + } + break; +>>>>>>> 5fbf526387 (r.info: Add JSON output (#3744)) } } - if (hflag->answer) { + if (hflag->answer || format == JSON) { if (hist_ok) { - fprintf(out, "Data Source:\n"); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_DATSRC_1)); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_DATSRC_2)); - fprintf(out, "Data Description:\n"); - fprintf(out, " %s\n", Rast_get_history(&hist, HIST_KEYWRD)); - if (Rast_history_length(&hist)) { - fprintf(out, "Comments:\n"); - for (i = 0; i < Rast_history_length(&hist); i++) - fprintf(out, " %s\n", Rast_history_line(&hist, i)); + switch (format) { + case PLAIN: + fprintf(out, "Data Source:\n"); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_DATSRC_1)); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_DATSRC_2)); + fprintf(out, "Data Description:\n"); + fprintf(out, " %s\n", + Rast_get_history(&hist, HIST_KEYWRD)); + if (Rast_history_length(&hist)) { + fprintf(out, "Comments:\n"); + for (i = 0; i < Rast_history_length(&hist); i++) + fprintf(out, " %s\n", + Rast_history_line(&hist, i)); + } + break; + case JSON: + json_object_set_string( + root_object, "source1", + Rast_get_history(&hist, HIST_DATSRC_1)); + json_object_set_string( + root_object, "source2", + Rast_get_history(&hist, HIST_DATSRC_2)); + json_object_set_string( + root_object, "description", + Rast_get_history(&hist, HIST_KEYWRD)); + JSON_Value *comments_value = json_value_init_array(); + JSON_Array *comments = json_array(comments_value); + if (Rast_history_length(&hist)) { + for (i = 0; i < Rast_history_length(&hist); i++) { + json_array_append_string( + comments, Rast_history_line(&hist, i)); + } + } + json_object_set_value(root_object, "comments", + comments_value); + break; } } } } /* else rflag or sflag or tflag or gflag or hflag or mflag */ + if (format == JSON) { + char *serialized_string = NULL; + serialized_string = json_serialize_to_string_pretty(root_value); + if (serialized_string == NULL) { + G_fatal_error(_("Failed to initialize pretty JSON string.")); + } + puts(serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); + } + return EXIT_SUCCESS; } diff --git a/raster/r.info/r.info.html b/raster/r.info/r.info.html index 33d04598d79..8de1e1c603f 100644 --- a/raster/r.info/r.info.html +++ b/raster/r.info/r.info.html @@ -183,6 +183,51 @@

EXAMPLES

comments="slope map elev = elev_ned10mzfactor = 1.00 format = degreesmin_slp_allowed = 0.000000" +

+Finally, the output from r.info can be output in JSON by passing the format=json option. + +

+r.info slope format=json
+
+
+{
+    "north": 228500,
+    "south": 215000,
+    "nsres": 10,
+    "east": 645000,
+    "west": 630000,
+    "ewres": 10,
+    "rows": 1350,
+    "cols": 1500,
+    "cells": 2025000,
+    "datatype": "FCELL",
+    "ncats": 255,
+    "min": 0,
+    "max": 38.68939208984375,
+    "map": "slope",
+    "maptype": "raster",
+    "mapset": "PERMANENT",
+    "location": "nc_spm_08_grass7",
+    "project": "nc_spm_08_grass7",
+    "database": "/grassdata",
+    "date": "Tue Nov  7 01:11:23 2006",
+    "creator": "helena",
+    "title": "South-West Wake county: slope in degrees",
+    "timestamp": null,
+    "units": null,
+    "vdatum": null,
+    "semantic_label": null,
+    "source1": "raster elevation file elev_ned10m",
+    "source2": "",
+    "description": "generated by r.slope.aspect",
+    "comments": [
+        "slope map elev = elev_ned10m",
+        "zfactor = 1.00 format = degrees",
+        "min_slp_allowed = 0.000000"
+    ]
+}
+
+

SEE ALSO

diff --git a/raster/r.info/testsuite/test_r_info.py b/raster/r.info/testsuite/test_r_info.py index a4d554232dd..49f91a5723f 100644 --- a/raster/r.info/testsuite/test_r_info.py +++ b/raster/r.info/testsuite/test_r_info.py @@ -5,10 +5,12 @@ Author: Sunveer Singh, Google Code-in 2017 Copyright: (C) 2017 by Sunveer Singh and the GRASS Development Team Licence: This program is free software under the GNU General Public - License (>=v2). Read the file COPYING that comes with GRASS - for details. + License (>=v2). Read the file COPYING that comes with GRASS + for details. """ +import json + from grass.gunittest.case import TestCase from grass.gunittest.gmodules import SimpleModule @@ -18,6 +20,35 @@ class TestReport(TestCase): def setUpClass(cls): """Use temporary region settings""" cls.use_temp_region() + cls.json_format_expected_no_stats = { + "north": 228500, + "south": 215000, + "east": 645000, + "west": 630000, + "nsres": 10, + "ewres": 10, + "rows": 1350, + "cols": 1500, + "min": 34300, + "max": 43600, + "cells": 2025000, + "datatype": "CELL", + "ncats": 43600, + "map": "lakes", + "maptype": "raster", + "mapset": "PERMANENT", + "date": "Fri Jan 19 23:49:34 2007", + "creator": "helena", + "title": "South-West Wake county: Wake county lakes", + "timestamp": None, + "units": None, + "vdatum": None, + "semantic_label": None, + "source1": "", + "source2": "", + "description": "generated by r.mapcalc", + "comments": ["1 * lakes_large"], + } @classmethod def tearDownClass(cls): @@ -67,6 +98,43 @@ def test_flagh(self): """Testing flag h with map zipcodes""" self.assertModule("r.info", map="lakes", flags="h") + def _test_format_json_helper(self, module, expected): + self.runModule(module) + result = json.loads(module.outputs.stdout) + + # the following fields vary with the Grass sample data's path + # therefore only check for their presence in the JSON output + # and not exact values + remove_fields = ["location", "project", "database"] + for field in remove_fields: + self.assertIn(field, result) + result.pop(field) + + self.assertCountEqual(list(expected.keys()), list(result.keys())) + + for key, value in expected.items(): + if isinstance(value, float): + self.assertAlmostEqual(value, result[key]) + else: + self.assertEqual(value, result[key]) + + def test_format_json(self): + """Testing using simple module in json format""" + module = SimpleModule("r.info", map="lakes", flags="g", format="json") + self._test_format_json_helper(module, self.json_format_expected_no_stats) + + def test_sflag_format_json(self): + """Testing using simple module in json format with sflag""" + expected_json_with_stats = { + **self.json_format_expected_no_stats, + "n": 36011, + "sum": 1404513600, + "stddev": 739.7965366431155, + "mean": 39002.349282163785, + } + module = SimpleModule("r.info", map="lakes", flags="gs", format="json") + self._test_format_json_helper(module, expected_json_with_stats) + if __name__ == "__main__": from grass.gunittest.main import test