[Git][debian-gis-team/grass][upstream] New upstream version 7.8.6~rc2

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Tue Aug 10 19:40:45 BST 2021



Bas Couwenberg pushed to branch upstream at Debian GIS Project / grass


Commits:
ba8139ed by Bas Couwenberg at 2021-08-10T20:04:57+02:00
New upstream version 7.8.6~rc2
- - - - -


25 changed files:

- INSTALL
- general/g.parser/standard_option.c
- gui/wxpython/gui_core/dialogs.py
- gui/wxpython/web_services/dialogs.py
- include/VERSION
- include/gis.h
- lib/cairodriver/text.c
- lib/gis/parser_standard_options.c
- lib/python/ctypes/ctypesgencore/parser/lex.py
- lib/python/docs/src/pygrass_modules.rst
- lib/python/gunittest/case.py
- lib/python/gunittest/gmodules.py
- lib/python/pygrass/modules/interface/module.py
- lib/python/pygrass/modules/interface/typedict.py
- lib/python/temporal/temporal_vector_algebra.py
- lib/raster/put_row.c
- locale/po/grasslibs_fr.po
- locale/po/grasslibs_pt_BR.po
- locale/po/grassmods_fr.po
- locale/po/grassmods_pt_BR.po
- locale/po/grasswxpy_pt_BR.po
- scripts/.flake8
- scripts/g.extension/g.extension.py
- temporal/t.rast.accumulate/t.rast.accumulate.py
- temporal/t.rast.neighbors/t.rast.neighbors.py


Changes:

=====================================
INSTALL
=====================================
@@ -68,7 +68,10 @@ The command,
    ./configure --help 
 
 explains the options used to disable the compilation of non-mandatory
-GRASS modules. See REQUIREMENTS.html for details.
+GRASS modules. See REQUIREMENTS.html for details on dependencies.
+Detailed Wiki notes for various operating systems (MS-Windows, GNU/Linux 
+distributions, FreeBSD, AIX, etc) are available at:
+https://grasswiki.osgeo.org/wiki/Compile_and_Install
   
 First step of the compilation (-g for debugging, or -O2 for optimization):
 
@@ -97,11 +100,7 @@ Explanation of make targets:
 
 Next step is the compilation itself:
 
-    make       
-
-Detailed Wiki notes for various operating systems (MS-Windows, GNU/Linux distros,
-FreeBSD, AIX, etc) are available at:
-https://grasswiki.osgeo.org/wiki/Compile_and_Install
+    make
 
 Note for Solaris users (see also Wiki page above):
 


=====================================
general/g.parser/standard_option.c
=====================================
@@ -65,6 +65,7 @@ static char* STD_OPT_STRINGS[] = {"G_OPT_UNDEFINED",
                                   "G_OPT_M_DIR",
                                   "G_OPT_M_REGION",
                                   "G_OPT_M_NULL_VALUE",
+                                  "G_OPT_M_NPROCS",
                                   "G_OPT_STDS_INPUT",
                                   "G_OPT_STDS_INPUTS",
                                   "G_OPT_STDS_OUTPUT",


=====================================
gui/wxpython/gui_core/dialogs.py
=====================================
@@ -596,14 +596,22 @@ class SavedRegion(wx.Dialog):
         self._selection.SetFocus()
         self._selection.Bind(wx.EVT_TEXT, self.OnRegion)
 
-        sizer.Add(box, proportion=0, flag=wx.GROW |
-                  wx.ALIGN_CENTER_VERTICAL | wx.ALL, border=5)
+        sizer.Add(
+            box,
+            proportion=0,
+            flag=wx.GROW | wx.ALL,
+            border=5,
+        )
 
         line = wx.StaticLine(
-            parent=self, id=wx.ID_ANY, size=(
-                20, -1), style=wx.LI_HORIZONTAL)
-        sizer.Add(line, proportion=0, flag=wx.GROW |
-                  wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT, border=5)
+            parent=self, id=wx.ID_ANY, size=(20, -1), style=wx.LI_HORIZONTAL
+        )
+        sizer.Add(
+            line,
+            proportion=0,
+            flag=wx.GROW | wx.LEFT | wx.RIGHT,
+            border=5,
+        )
 
         btnsizer = wx.StdDialogButtonSizer()
 


=====================================
gui/wxpython/web_services/dialogs.py
=====================================
@@ -9,7 +9,7 @@ List of classes:
  - dialogs::WSPropertiesDialog
  - dialogs::SaveWMSLayerDialog
 
-(C) 2009-2013 by the GRASS Development Team
+(C) 2009-2021 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.
@@ -66,10 +66,17 @@ class WSDialogBase(wx.Dialog):
 
         # TODO: should be in file
         self.default_servers = {
-            'OSM-WMS-EUROPE':
-            ['http://watzmann-geog.urz.uni-heidelberg.de/cached/osm', '', ''],
-            'irs.gis-lab.info (OSM)': ['http://irs.gis-lab.info', '', ''],
-            'NASA GIBS WMTS': ['http://gibs.earthdata.nasa.gov/wmts/epsg4326/best/wmts.cgi', '', '']}
+            'OSM-WMS': [
+                'https://ows.terrestris.de/osm/service?',
+                '',
+                '',
+            ],
+            'tiles.maps.eox.at (Sentinel-2)': [
+                'https://tiles.maps.eox.at/wms',
+                '',
+                '',
+            ],
+        }
 
         # holds reference to web service panel which is showed
         self.active_ws_panel = None
@@ -986,9 +993,12 @@ class SaveWMSLayerDialog(wx.Dialog):
         selSizer.Add(selTitleSizer, proportion=0,
                      flag=wx.EXPAND)
 
-        selSizer.Add(sel, proportion=1,
-                     flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL,
-                     border=5)
+        selSizer.Add(
+            sel,
+            proportion=1,
+            flag=wx.EXPAND | wx.ALL,
+            border=5,
+        )
 
         return selSizer
 


=====================================
include/VERSION
=====================================
@@ -1,4 +1,4 @@
 7
 8
-6RC1
+6RC2
 2021


=====================================
include/gis.h
=====================================
@@ -99,12 +99,12 @@ static const char *GRASS_copyright __attribute__ ((unused))
 #define U_DEGREES	8
 #define U_USFEET	9
 /* Temporal units from the datetime library */
-#define U_YEARS         DATETIME_YEAR   
-#define U_MONTHS        DATETIME_MONTH  
-#define U_DAYS          DATETIME_DAY    
-#define U_HOURS         DATETIME_HOUR   
-#define U_MINUTES       DATETIME_MINUTE 
-#define U_SECONDS       DATETIME_SECOND 
+#define U_YEARS         DATETIME_YEAR
+#define U_MONTHS        DATETIME_MONTH
+#define U_DAYS          DATETIME_DAY
+#define U_HOURS         DATETIME_HOUR
+#define U_MINUTES       DATETIME_MINUTE
+#define U_SECONDS       DATETIME_SECOND
 
 /*! \brief Projection code - XY coordinate system (unreferenced data) */
 #define PROJECTION_XY     0
@@ -302,10 +302,11 @@ typedef enum
     G_OPT_M_DBASE,              /*!< dbase */
     G_OPT_M_COORDS,             /*!< coordinates */
     G_OPT_M_COLR,               /*!< color rules */
-    G_OPT_M_DIR,                /*!< directory input */    
+    G_OPT_M_DIR,                /*!< directory input */
     G_OPT_M_REGION,             /*!< saved region */
     G_OPT_M_NULL_VALUE,         /*!< null value string */
-    
+    G_OPT_M_NPROCS,             /*!< number of threads for parallel computing */
+
     G_OPT_STDS_INPUT,           /*!< old input space time dataset of type strds, str3ds or stvds */
     G_OPT_STDS_INPUTS,          /*!< old input space time datasets */
     G_OPT_STDS_OUTPUT,          /*!< new output space time dataset */
@@ -321,7 +322,7 @@ typedef enum
     G_OPT_STVDS_OUTPUT,         /*!< new output space time vector dataset */
     G_OPT_MAP_INPUT,            /*!< old input map of type raster, vector or raster3d  */
     G_OPT_MAP_INPUTS,           /*!< old input maps of type raster, vector or raster3d  */
-    G_OPT_STDS_TYPE,            /*!< the type of a space time dataset: strds, str3ds, stvds */ 
+    G_OPT_STDS_TYPE,            /*!< the type of a space time dataset: strds, str3ds, stvds */
     G_OPT_MAP_TYPE,             /*!< The type of an input map: raster, vect, rast3d */
     G_OPT_T_TYPE,               /*!< The temporal type of a space time dataset */
     G_OPT_T_WHERE,              /*!< A temporal GIS framework SQL WHERE statement */
@@ -447,15 +448,15 @@ struct Cell_head
     /*! \brief Resolution - east to west cell size for 2D data */
     double ew_res;
     /*! \brief Resolution - east to west cell size for 3D data */
-    double ew_res3;   
+    double ew_res3;
     /*! \brief Resolution - north to south cell size for 2D data */
-    double ns_res;     
+    double ns_res;
     /*! \brief Resolution - north to south cell size for 3D data */
-    double ns_res3;   
+    double ns_res3;
     /*! \brief Resolution - top to bottom cell size for 3D data */
-    double tb_res;    
+    double tb_res;
     /*! \brief Extent coordinates (north) */
-    double north;     
+    double north;
     /*! \brief Extent coordinates (south) */
     double south;
     /*! \brief Extent coordinates (east) */


=====================================
lib/cairodriver/text.c
=====================================
@@ -17,8 +17,13 @@
 
 #if CAIRO_HAS_FT_FONT
 #include <cairo-ft.h>
+#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1,10,0) || defined(CAIRO_HAS_FC_FONT)
+#define USE_FONTCONFIG 1
 #include <fontconfig/fontconfig.h>
+#else
+#define USE_FONTCONFIG 0
 #endif
+#endif /* CAIRO_HAS_FT_FONT */
 
 #ifdef HAVE_ICONV_H
 #include <iconv.h>
@@ -170,7 +175,7 @@ static void set_font_toy(const char *name)
     G_free(font);
 }
 
-#if CAIRO_HAS_FT_FONT
+#if USE_FONTCONFIG
 
 static void fc_init(void)
 {
@@ -281,7 +286,7 @@ static int is_toy_font(const char *name)
 */
 void Cairo_set_font(const char *name)
 {
-#if CAIRO_HAS_FT_FONT
+#if USE_FONTCONFIG
     if (is_toy_font(name))
 	set_font_toy(name);
     else
@@ -322,7 +327,7 @@ static void font_list_toy(char ***list, int *count, int verbose)
 void Cairo_font_list(char ***list, int *count)
 {
     font_list_toy(list, count, 0);
-#if CAIRO_HAS_FT_FONT
+#if USE_FONTCONFIG
     font_list_fc(list, count, 0);
 #endif
 }
@@ -336,7 +341,7 @@ void Cairo_font_list(char ***list, int *count)
 void Cairo_font_info(char ***list, int *count)
 {
     font_list_toy(list, count, 1);
-#if CAIRO_HAS_FT_FONT
+#if USE_FONTCONFIG
     font_list_fc(list, count, 1);
 #endif
 }


=====================================
lib/gis/parser_standard_options.c
=====================================
@@ -109,6 +109,7 @@
    - G_OPT_M_COLR
    - G_OPT_M_REGION
    - G_OPT_M_NULL_VALUE
+   - G_OPT_M_NPROCS
 
   - temporal GIS framework
    - G_OPT_STDS_INPUT
@@ -755,6 +756,15 @@ struct Option *G_define_standard_option(int opt)
         Opt->description = _("Name of saved region");
         break;
 
+    case G_OPT_M_NPROCS:
+        Opt->key = "nprocs";
+        Opt->type = TYPE_INTEGER;
+        Opt->required = NO;
+        Opt->multiple = NO;
+        Opt->answer = "1";
+        Opt->description = _("Number of threads for parallel computing");
+        break;
+
     /* Spatio-temporal modules of the temporal GIS framework */
     case G_OPT_STDS_INPUT:
 	Opt->key = "input";


=====================================
lib/python/ctypes/ctypesgencore/parser/lex.py
=====================================
@@ -348,7 +348,7 @@ class Lexer:
                     break
 
                 # if func not callable, it means it's an ignored token
-                if not isinstance(func, collections.Callable):
+                if not isinstance(func, collections.abc.Callable):
                     break
 
                 # If token is processed by a function, call it
@@ -720,7 +720,7 @@ def lex(module=None, object=None, debug=0, optimize=0,
         states, tokname = _statetoken(f, stateinfo)
         toknames[f] = tokname
 
-        if isinstance(t, collections.Callable):
+        if isinstance(t, collections.abc.Callable):
             for s in states:
                 funcsym[s].append((f, t))
         elif (isinstance(t, bytes) or isinstance(t, str)):


=====================================
lib/python/docs/src/pygrass_modules.rst
=====================================
@@ -168,32 +168,16 @@ User or developer can check which parameters have been set, with: ::
         print "Aspect is not computed"
 
 
-After we set the parameters and run the module, the execution of the module
-instantiate a popen attribute to the class. The `Popen`_ class allow user
-to kill/wait/ the process. ::
+After we set the parameters, we can run the module in the background with
+`finish_=False`. Then call `wait()` and retrieve the returncode. ::
 
     >>> slope_aspect = Module('r.slope.aspect')
     >>> slope_aspect(elevation='elevation', slope='slp', aspect='asp', overwrite=True, finish_=False)
-    >>> slope_aspect.popen.wait() # *.kill(), *.terminate()
+    >>> slope_aspect.wait()
+    Module('r.slope.aspect')
+    >>> slope_aspect.returncode
     0
-    >>> out, err = slope_aspect.popen.communicate()
-    >>> print err #doctest: +NORMALIZE_WHITESPACE
-     100%
-    Aspect raster map <asp> complete
-    Slope raster map <slp> complete
-    <BLANKLINE>
 
-On the above example we use a new parameter `finish_`, if is set to True, the
-run method, automatically store the stdout and stderr to stdout and stderr
-attributes of the class: ::
-
-    >>> slope_aspect = Module('r.slope.aspect')
-    >>> slope_aspect(elevation='elevation', slope='slp', aspect='asp', overwrite=True, finish_=True)
-    >>> print slope_aspect.stderr #doctest: +NORMALIZE_WHITESPACE
-     100%
-    Aspect raster map <asp> complete
-    Slope raster map <slp> complete
-    <BLANKLINE>
 
 Another example of use: ::
 
@@ -202,9 +186,7 @@ Another example of use: ::
     >>> from grass.script.utils import parse_key_val
     >>> parse_key_val(info.outputs.stdout)
     {'max': '156.3299', 'min': '55.57879'}
-    >>> info = Module("r.info", map="elevation", flags="r", finish_=False)
-    >>> category = Module("r.category", map="elevation",
-    ...                   stdin_=info.popen.stdout, finish_=True)
+
 
 Launching GRASS GIS modules in parallel
 ---------------------------------------


=====================================
lib/python/gunittest/case.py
=====================================
@@ -1093,9 +1093,9 @@ class TestCase(unittest.TestCase):
                 errors += "\nSee available vector maps:\n"
                 errors += call_module('g.list', type='vector')
             # TODO: message format, parameters
-            raise CalledModuleError(module.popen.returncode, module.name,
-                                    module.get_python(),
-                                    errors=errors)
+            raise CalledModuleError(
+                module.returncode, module.name, module.get_python(), errors=errors
+            )
         # TODO: use this also in assert and apply when appropriate
         if expecting_stdout and not module.outputs.stdout.strip():
 
@@ -1153,14 +1153,15 @@ class TestCase(unittest.TestCase):
             print(text_to_string(module.outputs.stderr))
             # TODO: message format
             # TODO: stderr?
-            stdmsg = ('Running <{m.name}> module ended'
-                      ' with non-zero return code ({m.popen.returncode})\n'
-                      'Called: {code}\n'
-                      'See the following errors:\n'
-                      '{errors}'.format(
-                          m=module, code=module.get_python(),
-                          errors=module.outputs.stderr
-                      ))
+            stdmsg = (
+                "Running <{m.name}> module ended"
+                " with non-zero return code ({m.returncode})\n"
+                "Called: {code}\n"
+                "See the following errors:\n"
+                "{errors}".format(
+                    m=module, code=module.get_python(), errors=module.outputs.stderr
+                )
+            )
             self.fail(self._formatMessage(msg, stdmsg))
         print(text_to_string(module.outputs.stdout))
         print(text_to_string(module.outputs.stderr))


=====================================
lib/python/gunittest/gmodules.py
=====================================
@@ -26,14 +26,14 @@ class SimpleModule(Module):
     ...                        overwrite=True)
     >>> mapcalc.run()
     Module('r.mapcalc')
-    >>> mapcalc.popen.returncode
+    >>> mapcalc.returncode
     0
 
     >>> colors = SimpleModule('r.colors',
     ...                       map='test_a', rules='-', stdin_='1 red')
     >>> colors.run()
     Module('r.colors')
-    >>> colors.popen.returncode
+    >>> colors.returncode
     0
     >>> str(colors.inputs.stdin)
     '1 red'


=====================================
lib/python/pygrass/modules/interface/module.py
=====================================
@@ -53,7 +53,7 @@ class ParallelModuleQueue(object):
     Check with a queue size of 3 and 5 processes
 
     >>> import copy
-    >>> from grass.pygrass.modules import Module, ParallelModuleQueue
+    >>> from grass.pygrass.modules import Module, MultiModule, ParallelModuleQueue
     >>> mapcalc_list = []
 
     Setting run_ to False is important, otherwise a parallel processing is not possible
@@ -72,7 +72,7 @@ class ParallelModuleQueue(object):
     >>> queue.get_max_num_procs()
     3
     >>> for mapcalc in mapcalc_list:
-    ...     print(mapcalc.popen.returncode)
+    ...     print(mapcalc.returncode)
     0
     0
     0
@@ -95,7 +95,7 @@ class ParallelModuleQueue(object):
     >>> queue.get_max_num_procs()
     8
     >>> for mapcalc in mapcalc_list:
-    ...     print(mapcalc.popen.returncode)
+    ...     print(mapcalc.returncode)
     0
     0
     0
@@ -122,7 +122,7 @@ class ParallelModuleQueue(object):
     >>> queue.get_max_num_procs()
     3
     >>> for proc in proc_list:
-    ...     print(proc.popen.returncode)
+    ...     print(proc.returncode)
     0
     0
     0
@@ -165,7 +165,7 @@ class ParallelModuleQueue(object):
     >>> queue.get_max_num_procs()
     8
     >>> for mapcalc in mapcalc_list:
-    ...     print(mapcalc.popen.returncode)
+    ...     print(mapcalc.returncode)
     0
     0
     0
@@ -206,7 +206,7 @@ class ParallelModuleQueue(object):
     >>> queue.get_max_num_procs()
     3
     >>> for mapcalc in mapcalc_list:
-    ...     print(mapcalc.popen.returncode)
+    ...     print(mapcalc.returncode)
     0
     0
     0
@@ -366,7 +366,7 @@ class Module(object):
     ...                  overwrite=True, run_=False)
     >>> mapcalc.run()
     Module('r.mapcalc')
-    >>> mapcalc.popen.returncode
+    >>> mapcalc.returncode
     0
 
     >>> mapcalc = Module("r.mapcalc", expression="test_a = 1",
@@ -374,12 +374,12 @@ class Module(object):
     >>> mapcalc.run()
     Module('r.mapcalc')
     >>> p = mapcalc.wait()
-    >>> p.popen.returncode
+    >>> p.returncode
     0
     >>> mapcalc.run()
     Module('r.mapcalc')
     >>> p = mapcalc.wait()
-    >>> p.popen.returncode
+    >>> p.returncode
     0
 
     >>> colors = Module("r.colors", map="test_a", rules="-",
@@ -388,7 +388,7 @@ class Module(object):
     >>> colors.run()
     Module('r.colors')
     >>> p = mapcalc.wait()
-    >>> p.popen.returncode
+    >>> p.returncode
     0
     >>> colors.inputs["stdin"].value
     '1 red'
@@ -399,43 +399,47 @@ class Module(object):
 
     >>> colors = Module("r.colors", map="test_a", rules="-",
     ...                 run_=False, finish_=False, stdin_=PIPE)
+    >>> colors.inputs["stdin"].value = "1 red"
     >>> colors.run()
     Module('r.colors')
-    >>> stdout, stderr = colors.popen.communicate(input=b"1 red")
-    >>> colors.popen.returncode
+    >>> colors.wait()
+    Module('r.colors')
+    >>> colors.returncode
     0
-    >>> stdout
-    >>> stderr
 
     >>> colors = Module("r.colors", map="test_a", rules="-",
     ...                 run_=False, finish_=False,
     ...                 stdin_=PIPE, stderr_=PIPE)
+    >>> colors.inputs["stdin"].value = "1 red"
     >>> colors.run()
     Module('r.colors')
-    >>> stdout, stderr = colors.popen.communicate(input=b"1 red")
-    >>> colors.popen.returncode
+    >>> colors.wait()
+    Module('r.colors')
+    >>> colors.outputs["stderr"].value.strip()
+    "Color table for raster map <test_a> set to 'rules'"
+
+    >>> colors.returncode
     0
-    >>> stdout
-    >>> stderr.strip()
-    b"Color table for raster map <test_a> set to 'rules'"
 
     Run a second time
 
+    >>> colors.inputs["stdin"].value = "1 red"
     >>> colors.run()
     Module('r.colors')
-    >>> stdout, stderr = colors.popen.communicate(input=b"1 blue")
-    >>> colors.popen.returncode
+    >>> colors.wait()
+    Module('r.colors')
+    >>> colors.outputs["stderr"].value.strip()
+    "Color table for raster map <test_a> set to 'rules'"
+
+    >>> colors.returncode
     0
-    >>> stdout
-    >>> stderr.strip()
-    b"Color table for raster map <test_a> set to 'rules'"
 
     Multiple run test
 
     >>> colors = Module("r.colors", map="test_a",
     ...                                            color="ryb", run_=False)
     >>> colors.get_bash()
-    'r.colors map=test_a color=ryb'
+    'r.colors map=test_a color=ryb offset=0.0 scale=1.0'
     >>> colors.run()
     Module('r.colors')
     >>> colors(color="gyr")
@@ -575,18 +579,24 @@ class Module(object):
         self.stdin = None
         self.stdout_ = None
         self.stderr_ = None
-        diz = {'name': 'stdin', 'required': False,
-               'multiple': False, 'type': 'all',
-               'value': None}
-        self.inputs['stdin'] = Parameter(diz=diz)
-        diz['name'] = 'stdout'
-        self.outputs['stdout'] = Parameter(diz=diz)
-        diz['name'] = 'stderr'
-        self.outputs['stderr'] = Parameter(diz=diz)
-        self.popen = None
+        diz = {
+            "name": "stdin",
+            "required": False,
+            "multiple": False,
+            "type": "all",
+            "value": None,
+        }
+        self.inputs["stdin"] = Parameter(diz=diz)
+        diz["name"] = "stdout"
+        self.outputs["stdout"] = Parameter(diz=diz)
+        diz["name"] = "stderr"
+        self.outputs["stderr"] = Parameter(diz=diz)
+        self._popen = None
         self.time = None
-        self.start_time = None            # This variable will be set in the run() function
-        self._finished = False            # This variable is set True if wait() was successfully called
+        self.start_time = None  # This variable will be set in the run() function
+        # This variable is set True if wait() was successfully called
+        self._finished = False
+        self.returncode = None
 
         if args or kargs:
             self.__call__(*args, **kargs)
@@ -761,11 +771,13 @@ class Module(object):
 
         cmd = self.make_cmd()
         self.start_time = time.time()
-        self.popen = Popen(cmd,
-                           stdin=self.stdin_,
-                           stdout=self.stdout_,
-                           stderr=self.stderr_,
-                           env=self.env_)
+        self._popen = Popen(
+            cmd,
+            stdin=self.stdin_,
+            stdout=self.stdout_,
+            stderr=self.stderr_,
+            env=self.env_,
+        )
 
         if self.finish_ is True:
             self.wait()
@@ -781,17 +793,21 @@ class Module(object):
         if self._finished is False:
             if self.stdin:
                 self.stdin = encode(self.stdin)
-            stdout, stderr = self.popen.communicate(input=self.stdin)
-            self.outputs['stdout'].value = decode(stdout) if stdout else ''
-            self.outputs['stderr'].value = decode(stderr) if stderr else ''
+            stdout, stderr = self._popen.communicate(input=self.stdin)
+            self.outputs["stdout"].value = decode(stdout) if stdout else ""
+            self.outputs["stderr"].value = decode(stderr) if stderr else ""
             self.time = time.time() - self.start_time
-
+            self.returncode = self._popen.returncode
             self._finished = True
 
-            if self.popen.poll():
-                raise CalledModuleError(returncode=self.popen.returncode,
-                                        code=self.get_bash(),
-                                        module=self.name, errors=stderr)
+            if self._popen.poll():
+                raise CalledModuleError(
+                    returncode=self._popen.returncode,
+                    code=self.get_bash(),
+                    module=self.name,
+                    errors=stderr,
+                )
+        self._popen = None
 
         return self
 
@@ -836,38 +852,38 @@ class MultiModule(object):
     >>> mm = MultiModule(module_list=[region_1, region_2])
     >>> mm.run()
     >>> m_list = mm.get_modules()
-    >>> m_list[0].popen.returncode
+    >>> m_list[0].returncode
     0
-    >>> m_list[1].popen.returncode
+    >>> m_list[1].returncode
     0
 
     Asynchronous module run, setting finish = False
 
-    >>> region_1 = Module("g.region", run_=False)  # doctest: +SKIP
-    >>> region_1.flags.p = True  # doctest: +SKIP
-    >>> region_2 = copy.deepcopy(region_1)  # doctest: +SKIP
-    >>> region_2.flags.p = True  # doctest: +SKIP
-    >>> region_3 = copy.deepcopy(region_1)  # doctest: +SKIP
-    >>> region_3.flags.p = True  # doctest: +SKIP
-    >>> region_4 = copy.deepcopy(region_1)  # doctest: +SKIP
-    >>> region_4.flags.p = True  # doctest: +SKIP
-    >>> region_5 = copy.deepcopy(region_1)  # doctest: +SKIP
-    >>> region_5.flags.p = True  # doctest: +SKIP
+    >>> region_1 = Module("g.region", run_=False)
+    >>> region_1.flags.p = True
+    >>> region_2 = copy.deepcopy(region_1)
+    >>> region_2.flags.p = True
+    >>> region_3 = copy.deepcopy(region_1)
+    >>> region_3.flags.p = True
+    >>> region_4 = copy.deepcopy(region_1)
+    >>> region_4.flags.p = True
+    >>> region_5 = copy.deepcopy(region_1)
+    >>> region_5.flags.p = True
     >>> mm = MultiModule(module_list=[region_1, region_2, region_3, region_4, region_5],
-    ...                  sync=False)  # doctest: +SKIP
-    >>> t = mm.run()  # doctest: +SKIP
-    >>> isinstance(t, Process)  # doctest: +SKIP
+    ...                  sync=False)
+    >>> t = mm.run()
+    >>> isinstance(t, Process)
     True
-    >>> m_list = mm.wait()  # doctest: +SKIP
-    >>> m_list[0].popen.returncode  # doctest: +SKIP
+    >>> m_list = mm.wait()
+    >>> m_list[0].returncode
     0
-    >>> m_list[1].popen.returncode  # doctest: +SKIP
+    >>> m_list[1].returncode
     0
-    >>> m_list[2].popen.returncode  # doctest: +SKIP
+    >>> m_list[2].returncode
     0
-    >>> m_list[3].popen.returncode  # doctest: +SKIP
+    >>> m_list[3].returncode
     0
-    >>> m_list[4].popen.returncode  # doctest: +SKIP
+    >>> m_list[4].returncode
     0
 
     Asynchronous module run, setting finish = False and using temporary region
@@ -880,15 +896,15 @@ class MultiModule(object):
     >>> isinstance(t, Process)
     True
     >>> m_list = mm.wait()
-    >>> m_list[0].popen.returncode
+    >>> m_list[0].returncode
     0
-    >>> m_list[1].popen.returncode
+    >>> m_list[1].returncode
     0
-    >>> m_list[2].popen.returncode
+    >>> m_list[2].returncode
     0
-    >>> m_list[3].popen.returncode
+    >>> m_list[3].returncode
     0
-    >>> m_list[4].popen.returncode
+    >>> m_list[4].returncode
     0
 
     """


=====================================
lib/python/pygrass/modules/interface/typedict.py
=====================================
@@ -55,6 +55,18 @@ class TypeDict(OrderedDict):
             obj[k] = deepcopy(v)
         return obj
 
+    def __reduce__(self):
+        inst_dict = vars(self).copy()
+        for k in vars(TypeDict(self._type)):
+            inst_dict.pop(k, None)
+        return (
+            self.__class__,
+            (self._type,),
+            inst_dict or None,
+            None,
+            iter(self.items()),
+        )
+
     def used(self):
         key_dict = {}
         for key in self:


=====================================
lib/python/temporal/temporal_vector_algebra.py
=====================================
@@ -416,11 +416,12 @@ class TemporalVectorAlgebraParser(TemporalAlgebraParser):
                             self.msgr.message("Run command:\n" + cmd.get_bash())
                             cmd.run()
 
-                            if cmd.popen.returncode != 0:
-                                self.msgr.fatal(_("Error starting %s : \n%s")
-                                                  %(cmd.get_bash(),
-                                                  cmd.popen.stderr))
-                            mapname = cmd.outputs['output'].value
+                            if cmd.returncode != 0:
+                                self.msgr.fatal(
+                                    _("Error starting %s : \n%s")
+                                    % (cmd.get_bash(), cmd.outputs.stderr)
+                                )
+                            mapname = cmd.outputs["output"].value
                             if mapname.find("@") >= 0:
                                 map_test = map_i.get_new_instance(mapname)
                             else:
@@ -490,9 +491,13 @@ class TemporalVectorAlgebraParser(TemporalAlgebraParser):
                                 map_i.update_all(dbif=dbif)
                             elif map_i.is_in_db(dbif=dbif) and self.overwrite == False:
                                 # Raise error if map exists and no overwrite flag is given.
-                                self.msgr.fatal(_("Error vector map %s exist in temporal database. "
-                                                  "Use overwrite flag.  : \n%s") \
-                                                  %(map_i.get_map_id(), cmd.popen.stderr))
+                                self.msgr.fatal(
+                                    _(
+                                        "Error vector map %s exist in temporal database. "
+                                        "Use overwrite flag.  : \n%s"
+                                    )
+                                    % (map_i.get_map_id(), cmd.outputs.stderr)
+                                )
                             else:
                                 # Insert map into temporal database.
                                 map_i.insert(dbif=dbif)


=====================================
lib/raster/put_row.c
=====================================
@@ -388,14 +388,14 @@ static void put_data(int fd, char *null_buf, const CELL * cell,
 	    nwrite++;
 
 	    if (write(fcb->data_fd, compressed_buf, nwrite) != nwrite)
-		G_fatal_error(_("Error writing compressed data for row %d of <%s>"),
-			      row, fcb->name);
+		G_fatal_error(_("Error writing compressed data for row %d of <%s>: %s"),
+			      row, fcb->name, strerror(errno));
 	}
 	else {
 	    nwrite = nbytes * n + 1;
 	    if (write(fcb->data_fd, work_buf, nwrite) != nwrite)
-		G_fatal_error(_("Error writing compressed data for row %d of <%s>"),
-			      row, fcb->name);
+		G_fatal_error(_("Error writing compressed data for row %d of <%s>: %s"),
+			      row, fcb->name, strerror(errno));
 	}
 
 	G_free(compressed_buf);
@@ -404,8 +404,8 @@ static void put_data(int fd, char *null_buf, const CELL * cell,
 	nwrite = fcb->nbytes * n;
 
 	if (write(fcb->data_fd, work_buf, nwrite) != nwrite)
-	    G_fatal_error(_("Error writing uncompressed data for row %d of <%s>"),
-			  row, fcb->name);
+	    G_fatal_error(_("Error writing uncompressed data for row %d of <%s>: %s"),
+			  row, fcb->name, strerror(errno));
     }
 
     G_free(work_buf);
@@ -518,13 +518,13 @@ static void write_null_bits_compressed(const unsigned char *flags,
 
     if (nwrite > 0 && nwrite < size) {
 	if (write(fcb->null_fd, compressed_buf, nwrite) != nwrite)
-	    G_fatal_error(_("Error writing compressed null data for row %d of <%s>"),
-			  row, fcb->name);
+	    G_fatal_error(_("Error writing compressed null data for row %d of <%s>: %s"),
+			  row, fcb->name, strerror(errno));
     }
     else {
 	if (write(fcb->null_fd, flags, size) != size)
-	    G_fatal_error(_("Error writing compressed null data for row %d of <%s>"),
-			  row, fcb->name);
+	    G_fatal_error(_("Error writing compressed null data for row %d of <%s>: %s"),
+			  row, fcb->name, strerror(errno));
     }
 
     G_free(compressed_buf);
@@ -557,10 +557,12 @@ void Rast__write_null_bits(int fd, const unsigned char *flags)
     offset = (off_t) size * row;
 
     if (lseek(fcb->null_fd, offset, SEEK_SET) < 0)
-	G_fatal_error(_("Error writing null row %d of <%s>"), row, fcb->name);
+	G_fatal_error(_("Error writing null row %d of <%s>"),
+	              row, fcb->name);
 
     if (write(fcb->null_fd, flags, size) != size)
-	G_fatal_error(_("Error writing null row %d of <%s>"), row, fcb->name);
+	G_fatal_error(_("Error writing null row %d of <%s>: %s"),
+	              row, fcb->name, strerror(errno));
 }
 
 static void convert_and_write_if(int fd, const void *vbuf)


=====================================
locale/po/grasslibs_fr.po
=====================================
@@ -85,7 +85,7 @@ msgstr "afficher les paramètres de configuration GRASS"
 
 #: ../lib/init/grass.py:337
 msgid "options: arch,build,compiler,date,path,revision,svn_revision,version"
-msgstr ""
+msgstr "options: arch,build,compiler,date,path,revision,svn_revision,version"
 
 #: ../lib/init/grass.py:338 ../lib/gis/parser_rest.c:179
 #: ../lib/gis/parser_html.c:189
@@ -380,6 +380,12 @@ msgid ""
 "\n"
 "Check the <{file}> file."
 msgstr ""
+"Erreur de lecture des informations du chemin des données depuis g.gisenv.\n"
+"GISDBASE={gisbase}\n"
+"LOCATION_NAME={location}\n"
+"MAPSET={mapset}\n"
+"\n"
+"Vérifier le fichier <{file}>."
 
 #: ../lib/init/grass.py:1409
 #, python-format
@@ -1361,7 +1367,7 @@ msgstr "Seulement %d%% de la fenêtre sauvegardée dans \"%s\" chevauche(nt) la
 #: ../lib/gis/seek.c:54 ../lib/gis/seek.c:60
 #, c-format
 msgid "Unable to seek: %s"
-msgstr ""
+msgstr "Recherche de : %s impossible"
 
 #: ../lib/gis/seek.c:58
 msgid "Seek offset out of range"
@@ -2459,7 +2465,7 @@ msgstr "Maximum de mémoire à utiliser (en MB)"
 
 #: ../lib/gis/parser_standard_options.c:264
 msgid "Cache size for raster rows"
-msgstr ""
+msgstr "Taille de cache pour les lignes du raster"
 
 #: ../lib/gis/parser_standard_options.c:272
 msgid "Name of input raster map"
@@ -2760,7 +2766,7 @@ msgstr "Nom du jeu de données raster temporel en sortie"
 
 #: ../lib/gis/parser_standard_options.c:816
 msgid "Name of the output space time raster datasets"
-msgstr ""
+msgstr "Nom du jeu de données raster temporel en sortie"
 
 #: ../lib/gis/parser_standard_options.c:824
 msgid "Name of the input space time vector dataset"
@@ -5052,7 +5058,7 @@ msgstr "Impossible de sélectionner les attributs pour cat = %d"
 
 #: ../lib/vector/Vlib/ascii.c:724
 msgid "Unable to fetch data from table"
-msgstr "Impossible de récupérer les données depis la table"
+msgstr "Impossible de récupérer les données depuis la table"
 
 #: ../lib/vector/Vlib/ascii.c:756 ../lib/vector/Vlib/net_build.c:216
 #: ../lib/vector/Vlib/net_build.c:501 ../lib/vector/Vlib/net_build.c:519
@@ -8379,11 +8385,11 @@ msgstr ". Voir les erreus dans la sortie (d'erreurs)."
 #: ../lib/python/imaging/operations.py:128
 #: ../lib/python/imaging/operations.py:160
 msgid "Install PIL or Pillow to use this function"
-msgstr ""
+msgstr "Installer PIL ou Pillow pour utiliser cette fonction"
 
 #: ../lib/python/imaging/operations.py:162
 msgid "Install a newer version of PIL or Pillow to use this function (missing ImageOps module)"
-msgstr ""
+msgstr "Installer une version plus récente de PIL ou Pillow pour utiliser cette fonction (Module ImageOps manquant)"
 
 #: ../lib/python/script/task.py:58
 msgid "unknown"
@@ -8415,6 +8421,9 @@ msgid ""
 "\n"
 "Details: <{det}>"
 msgstr ""
+"Impossible de récupérer la description de l'interface pour la commande '<{cmd}>'.\n"
+"\n"
+"Détails: <{det}>"
 
 #: ../lib/python/script/task.py:524
 #, python-brace-format
@@ -9833,12 +9842,12 @@ msgstr "proj_create() a échoué pour '%s'"
 #: ../lib/proj/do_proj.c:284
 #, c-format
 msgid "Unrecognized SRID '%s'"
-msgstr ""
+msgstr "SRID '%s' non reconnu"
 
 #: ../lib/proj/do_proj.c:298
 #, c-format
 msgid "Unrecognized WKT '%s'"
-msgstr ""
+msgstr "WKT '%s' non reconnu"
 
 #: ../lib/proj/do_proj.c:316
 msgid "Unable to create PROJ object"
@@ -9874,22 +9883,22 @@ msgstr ""
 #: ../lib/proj/do_proj.c:573
 #, c-format
 msgid "Found %d possible transformations"
-msgstr ""
+msgstr "%d Possibles transformations trouvées"
 
 #: ../lib/proj/do_proj.c:585
 #, c-format
 msgid "proj_normalize_for_visualization() failed for operation %d"
-msgstr ""
+msgstr "proj_normalize_for_visualization() a échoué pour %d"
 
 #: ../lib/proj/do_proj.c:596
 #, c-format
 msgid "Operation %d:"
-msgstr ""
+msgstr "Operation %d:"
 
 #: ../lib/proj/do_proj.c:598
 #, c-format
 msgid "Description: %s"
-msgstr ""
+msgstr "Description: %s"
 
 #: ../lib/proj/do_proj.c:603
 #, c-format
@@ -9904,7 +9913,7 @@ msgstr ""
 #: ../lib/proj/do_proj.c:615
 #, c-format
 msgid "Remarks: %s"
-msgstr ""
+msgstr "Remarques: %s"
 
 #: ../lib/proj/do_proj.c:620
 #, c-format
@@ -9917,7 +9926,7 @@ msgstr ""
 
 #: ../lib/proj/do_proj.c:635
 msgid "See also output of:"
-msgstr ""
+msgstr "Voir aussi la sortie de:"
 
 #: ../lib/proj/do_proj.c:638
 #, c-format
@@ -9927,7 +9936,7 @@ msgstr ""
 #: ../lib/proj/do_proj.c:740
 #, c-format
 msgid "proj_normalize_for_visualization() failed for '%s'"
-msgstr ""
+msgstr "proj_normalize_for_visualization() a échoué pour '%s'"
 
 #: ../lib/proj/do_proj.c:753
 #, c-format
@@ -10535,3 +10544,6 @@ msgstr ""
 
 msgid "Unable to seek"
 msgstr "Recherche impossible"
+
+#~ msgid "Invalid value <%s> for parameter <%s>"
+#~ msgstr "Valeur <%s> manquante pour le paramètre <%s>"


=====================================
locale/po/grasslibs_pt_BR.po
=====================================
@@ -424,7 +424,7 @@ msgstr ""
 
 #: ../lib/init/grass.py:1469
 msgid "Building user fontcap..."
-msgstr ""
+msgstr "Construindo o fontcap do usuário..."
 
 #: ../lib/init/grass.py:1530
 msgid "The SHELL variable is not set"
@@ -736,7 +736,7 @@ msgstr "Registro no banco de dados para categoria %d não encontrado"
 
 #: ../lib/rst/interp_float/vinput2d.c:192
 msgid "Negative value of smoothing detected: sm must be >= 0"
-msgstr ""
+msgstr "Valor negativo de suavização detectado: sm deve ser >= 0"
 
 #: ../lib/rst/interp_float/vinput2d.c:256
 msgid "Strip exists with insufficient data"
@@ -764,7 +764,7 @@ msgstr "Não existem pontos na região atual"
 #: ../lib/rst/interp_float/vinput2d.c:287
 #, c-format
 msgid "Segmentation parameters set to invalid values: npmin= %d, segmax= %d for smooth connection of segments, npmin > segmax (see manual)"
-msgstr ""
+msgstr "Parâmetros de segmentação definidos com valores inválidos: npmin= %d, segmax= %dpara conexão suave de segmentos, npmin > segmax (consulte o manual)"
 
 #: ../lib/rst/interp_float/vinput2d.c:293
 #, c-format
@@ -1311,7 +1311,7 @@ msgstr "Não foi possível abrir arquivo de saída <%s>: %s"
 #: ../lib/gis/key_value3.c:39
 #, c-format
 msgid "Error closing output file <%s>: %s"
-msgstr "Erro fechando arquivo de saída <%s>: %s"
+msgstr "Erro ao fechar arquivo de saída <%s>: %s"
 
 #: ../lib/gis/get_ellipse.c:263 ../lib/proj/ellipse.c:243
 #, c-format
@@ -1962,7 +1962,7 @@ msgstr "Erro lendo arquivo <%s>: %s"
 #: ../lib/gis/key_value3.c:67
 #, c-format
 msgid "Error closing input file <%s>: %s"
-msgstr "Erro fechando arquivo de entrada <%s>: %s"
+msgstr "Erro ao fechar arquivo de entrada <%s>: %s"
 
 #: ../lib/gis/cmprbzip.c:97 ../lib/gis/cmprbzip.c:176
 msgid "GRASS needs to be compiled with BZIP2 for BZIP2 compression"
@@ -2021,7 +2021,7 @@ msgstr "Não foi possível escrever %d bytes: %s"
 
 #: ../lib/gis/token.c:169
 msgid "parse error"
-msgstr "erro de análise"
+msgstr "erro de interpretação"
 
 #: ../lib/gis/env.c:311
 msgid "GISRC - variable not set"
@@ -2584,7 +2584,7 @@ msgstr "O método de compactação usado no mapa raster3d de saída"
 
 #: ../lib/gis/parser_standard_options.c:474
 msgid "The dimensions of the tiles used in the output raster3d map (XxYxZ or default: 16x16x8)"
-msgstr "As dimensões dos tiles usados ​​no mapa raster3d de saída (XxYxZ ou padrão: 16x16x8)"
+msgstr "As dimensões dos mosaicos usados ​​no mapa raster3d de saída (XxYxZ ou padrão: 16x16x8)"
 
 #: ../lib/gis/parser_standard_options.c:484
 msgid "Name of input vector map"
@@ -4106,17 +4106,17 @@ msgstr ""
 #: ../lib/vector/vedit/delete.c:72
 #, c-format
 msgid "No area found for centroid %d"
-msgstr "Nenhuma área encontrada para o centróide %d"
+msgstr "Nenhuma área encontrada para o centroide %d"
 
 #: ../lib/vector/vedit/delete.c:76
 #, c-format
 msgid "Duplicate centroid %d, unable to delete area"
-msgstr "Centróide %d duplicado, não é possível excluir área"
+msgstr "Centroide %d duplicado, não é possível excluir área"
 
 #: ../lib/vector/vedit/delete.c:105
 #, c-format
 msgid "Area %d without centroid"
-msgstr "Área %d sem centróide"
+msgstr "Área %d sem centroide"
 
 #: ../lib/vector/vedit/delete.c:121
 #, c-format
@@ -4130,11 +4130,11 @@ msgstr "Não é possível definir a categoria %d para (ID da feição %d)"
 
 #: ../lib/vector/vedit/select.c:231
 msgid "Unknown query tool"
-msgstr ""
+msgstr "Ferramenta de consulta desconhecida"
 
 #: ../lib/vector/neta/spanningtree.c:109
 msgid "Computing minimum spanning tree..."
-msgstr ""
+msgstr "Calculando a árvore de recobrimento mínima..."
 
 #: ../lib/vector/neta/timetables.c:46 ../lib/vector/neta/timetables.c:203
 #: ../lib/vector/neta/timetables.c:244 ../lib/vector/neta/timetables.c:286
@@ -4267,7 +4267,7 @@ msgstr "Não é possível construir o centroide para a área %d. Ignorado."
 #: ../lib/vector/Vlib/read_sfa.c:95
 #, c-format
 msgid "Centroid %d: invalid area %d"
-msgstr "Centróide%d: área inválida%d"
+msgstr "Centroide%d: área inválida%d"
 
 #: ../lib/vector/Vlib/read_sfa.c:117 ../lib/vector/Vlib/read_pg.c:192
 #, c-format
@@ -4401,7 +4401,7 @@ msgstr "Número de fronteiras: %d"
 #: ../lib/vector/Vlib/build.c:906
 #, c-format
 msgid "Number of centroids: %d"
-msgstr "Número de centróides: %d"
+msgstr "Número de centroides: %d"
 
 #: ../lib/vector/Vlib/build.c:909
 #, c-format
@@ -4426,12 +4426,12 @@ msgstr "Número de ilhas: %d"
 #: ../lib/vector/Vlib/build.c:964
 #, c-format
 msgid "Number of areas without centroid: %d"
-msgstr "Número de áreas sem centróide: %d"
+msgstr "Número de áreas sem centroide: %d"
 
 #: ../lib/vector/Vlib/build.c:969
 #, c-format
 msgid "Number of centroids exceeds number of areas: %d > %d"
-msgstr "O número de centróides excede o número de áreas: %d > %d"
+msgstr "O número de centroides excede o número de áreas: %d > %d"
 
 #: ../lib/vector/Vlib/build.c:973
 #, c-format
@@ -4441,12 +4441,12 @@ msgstr "Número de fronteiras incorretas: %d"
 #: ../lib/vector/Vlib/build.c:977
 #, c-format
 msgid "Number of centroids outside area: %d"
-msgstr "Número de centróides fora da área: %d"
+msgstr "Número de centroides fora da área: %d"
 
 #: ../lib/vector/Vlib/build.c:981
 #, c-format
 msgid "Number of duplicate centroids: %d"
-msgstr "Número de centróides duplicados: %d"
+msgstr "Número de centroides duplicados: %d"
 
 #: ../lib/vector/Vlib/build.c:985
 msgid "Number of areas: -"
@@ -4818,7 +4818,7 @@ msgstr "Linha ignorada (tipo de feição de saída: %s)"
 #: ../lib/vector/Vlib/write_pg.c:1266
 #, c-format
 msgid "Centroid skipped (output feature type: %s)"
-msgstr "Centróide ignorado (tipo de feição de saída: %s)"
+msgstr "Centroide ignorado (tipo de feição de saída: %s)"
 
 #: ../lib/vector/Vlib/write_pg.c:1273
 #, c-format
@@ -5326,7 +5326,7 @@ msgstr "Extensão PostGIS Topology não encontrada no banco de dados <%s>"
 
 #: ../lib/vector/Vlib/open_pg.c:667
 msgid "Empty bounding box"
-msgstr ""
+msgstr "Retângulo envolvente vazio"
 
 #: ../lib/vector/Vlib/open_pg.c:768
 #, c-format
@@ -5350,7 +5350,7 @@ msgstr "Ilha %d sem fronteira detectada"
 
 #: ../lib/vector/Vlib/open_pg.c:1128
 msgid "Unable to get map bounding box from topology"
-msgstr ""
+msgstr "Não foi possível obter o retângulo envolvente do mapa da topologia"
 
 #: ../lib/vector/Vlib/open_pg.c:1135
 #, c-format
@@ -5358,6 +5358,8 @@ msgid ""
 "Unable to parse map bounding box:\n"
 "%s"
 msgstr ""
+"Não é possível analisar o retângulo envolvente do mapa:\n"
+"%s"
 
 #: ../lib/vector/Vlib/open_pg.c:1159
 #, c-format
@@ -5391,7 +5393,7 @@ msgstr "Inconsistência na topologia: número de linhas %d (deveria ser %d)"
 #: ../lib/vector/Vlib/open_pg.c:1668
 #, c-format
 msgid "Inconsistency in topology: number of centroids %d (should be %d)"
-msgstr "Inconsistência na topologia: número de centróides %d (deveria ser %d)"
+msgstr "Inconsistência na topologia: número de centroides %d (deveria ser %d)"
 
 #: ../lib/vector/Vlib/dangles.c:148
 msgid "Changed"
@@ -5447,7 +5449,7 @@ msgstr "%d novas linhas e fronteiras"
 
 #: ../lib/vector/Vlib/write_sfa.c:117
 msgid "Unable to calculate centroid for area"
-msgstr "Não foi possível calcular centróide para área"
+msgstr "Não foi possível calcular centroide para área"
 
 #: ../lib/vector/Vlib/write_sfa.c:219
 msgid "Attempt to delete dead feature"
@@ -5582,16 +5584,16 @@ msgstr "Não foi possível ler o mapa vetorial"
 #, c-format
 msgid "One primitive registered"
 msgid_plural "%d primitives registered"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Uma primitiva registrada"
+msgstr[1] "%d primitivas registradas"
 
 #: ../lib/vector/Vlib/build_nat.c:119 ../lib/vector/Vlib/build_sfa.c:386
 #: ../lib/vector/Vlib/build_sfa.c:665
 #, c-format
 msgid "One vertex registered"
 msgid_plural "%jd vertices registered"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Um vértice registrado"
+msgstr[1] "%jd vértices registrados"
 
 #: ../lib/vector/Vlib/build_nat.c:132
 msgid "Building areas..."
@@ -5601,15 +5603,15 @@ msgstr "Construindo áreas..."
 #, c-format
 msgid "One area built"
 msgid_plural "%d areas built"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Uma área construída"
+msgstr[1] "%d áreas construídas"
 
 #: ../lib/vector/Vlib/build_nat.c:157
 #, c-format
 msgid "One isle built"
 msgid_plural "%d isles built"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Uma ilha construída"
+msgstr[1] "%d ilhas construídas"
 
 #: ../lib/vector/Vlib/build_nat.c:168
 msgid "Attaching islands..."
@@ -5617,7 +5619,7 @@ msgstr "Anexando ilhas..."
 
 #: ../lib/vector/Vlib/build_nat.c:188
 msgid "Attaching centroids..."
-msgstr "Anexando centróides..."
+msgstr "Anexando centroides..."
 
 #: ../lib/vector/Vlib/graph.c:138
 msgid "Unable to add network arc"
@@ -5983,7 +5985,7 @@ msgstr "Acesso aleatório não suportado. Chave primária não definida."
 
 #: ../lib/vector/Vlib/read_pg.c:652
 msgid "Inconsistency in topology: detected centroid (should be point)"
-msgstr "Inconsistência na topologia: centróide detectado (deveria ser ponto)"
+msgstr "Inconsistência na topologia: centroide detectado (deveria ser ponto)"
 
 #: ../lib/vector/Vlib/read_pg.c:662
 msgid "Inconsistency in topology: detected boundary (should be line)"
@@ -6020,7 +6022,7 @@ msgstr "Dados corrompidos"
 #: ../lib/vector/Vlib/read_pg.c:1252
 #, c-format
 msgid "Unable to parse '%s'"
-msgstr "Não foi possível analisar '%s'"
+msgstr "Não foi possível interpretar '%s'"
 
 #: ../lib/vector/Vlib/read_pg.c:1420 ../lib/vector/Vlib/close_pg.c:66
 #, c-format
@@ -6091,8 +6093,8 @@ msgstr "Não é possível selecionar registros da tabela <%s>"
 #, c-format
 msgid "One category loaded"
 msgid_plural "%d categories loaded"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Uma categoria carregada"
+msgstr[1] "%d categorias carregadas"
 
 #: ../lib/vector/Vlib/cats.c:627
 #, c-format
@@ -6107,7 +6109,7 @@ msgstr "%d erros na opção '%s'"
 #: ../lib/vector/Vlib/build_sfa.c:279 ../lib/vector/Vlib/build_sfa.c:546
 #, c-format
 msgid "Unable to calculate centroid for area %d"
-msgstr "Não foi possível calcular centróide para área %d"
+msgstr "Não foi possível calcular centroide para área %d"
 
 #: ../lib/vector/Vlib/build_sfa.c:305
 #, c-format
@@ -6124,8 +6126,8 @@ msgstr "Tipo de feição OGR %d não é suportada"
 #, c-format
 msgid "One feature without geometry skipped"
 msgid_plural "%d features without geometry skipped"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Uma feição sem geometria ignorada"
+msgstr[1] "%d feições sem geometria ignoradas"
 
 #: ../lib/vector/Vlib/build_sfa.c:725
 #, c-format
@@ -6134,7 +6136,7 @@ msgstr "%s: Formato nativo não suportado"
 
 #: ../lib/vector/Vlib/build_sfa.c:751
 msgid "Feature index is built only for non-native formats. Nothing to dump."
-msgstr ""
+msgstr "O índice de feições é criado apenas para formatos não nativos. Nada para descartar."
 
 #: ../lib/vector/Vlib/box.c:264
 #, c-format
@@ -6164,12 +6166,12 @@ msgstr "Topologia não disponível para mapa vetorial <%s>. Registrando primitiv
 #: ../lib/vector/Vlib/break_lines.c:574
 #, c-format
 msgid "Intersections: %d"
-msgstr ""
+msgstr "Intersecções: %d"
 
 #: ../lib/vector/Vlib/color_write.c:71 ../lib/raster/color_write.c:81
 #, c-format
 msgid "Qualified name <%s> doesn't match mapset <%s>"
-msgstr ""
+msgstr "O nome qualificado <%s> não corresponde ao mapset <%s>"
 
 #: ../lib/vector/Vlib/color_write.c:91 ../lib/raster/color_write.c:96
 #, c-format
@@ -6187,7 +6189,7 @@ msgstr "%d áreas de tamanho total %g removidas"
 
 #: ../lib/vector/Vlib/remove_areas.c:395
 msgid "Could not delete line from coor"
-msgstr ""
+msgstr "Não foi possível excluir linha de coor"
 
 #: ../lib/vector/Vlib/remove_areas.c:458
 msgid "dissolve_neighbour > 0, failed to build new area"
@@ -6339,7 +6341,7 @@ msgstr "Segmento fora da linha, nenhum segmento criado"
 
 #: ../lib/vector/Vlib/net_build.c:101 ../lib/vector/Vlib/net_build.c:727
 msgid "Building graph..."
-msgstr ""
+msgstr "Construindo gráfico..."
 
 #: ../lib/vector/Vlib/net_build.c:143 ../lib/vector/Vlib/net_build.c:775
 msgid "Unable to build network graph"
@@ -6364,11 +6366,11 @@ msgstr ""
 #: ../lib/vector/Vlib/net_build.c:988
 #, c-format
 msgid "Data type of column <%s> not supported (must be numeric)"
-msgstr ""
+msgstr "Tipo de dados da coluna <%s> não suportado (deve ser numérico)"
 
 #: ../lib/vector/Vlib/net_build.c:202 ../lib/vector/Vlib/net_build.c:967
 msgid "Setting node costs..."
-msgstr ""
+msgstr "Definindo custos de nó..."
 
 #: ../lib/vector/Vlib/net_build.c:277 ../lib/vector/Vlib/net_build.c:1032
 #, c-format
@@ -6377,7 +6379,7 @@ msgstr "Registro do banco de dados para o nó %d (cat = %d) não encontrado (cos
 
 #: ../lib/vector/Vlib/net_build.c:332 ../lib/vector/Vlib/net_build.c:352
 msgid "Cannot add network arc for virtual node connection."
-msgstr ""
+msgstr "Não é possível adicionar arco de rede para conexão de nó virtual."
 
 #: ../lib/vector/Vlib/net_build.c:364
 #, c-format
@@ -6385,6 +6387,8 @@ msgid ""
 "There exists more than one point of node <%d> with unique category field  <%d>.\n"
 "The unique categories layer is not valid therefore you will probably get incorrect results."
 msgstr ""
+"Existe mais de um ponto de nó <%d> com campo de categoria exclusivo <%d>.\n"
+"A camada de categorias exclusivas não é válida, portanto, você provavelmente obterá resultados incorretos."
 
 #: ../lib/vector/Vlib/net_build.c:393
 #, c-format
@@ -6393,6 +6397,9 @@ msgid ""
 "Cost for the intersection was set to 0.\n"
 "The unique categories layer is not valid therefore you will probably get incorrect results."
 msgstr ""
+"Não foi possível encontrar o ponto que representa a interseção <%d> no campo de categorias exclusivas <%d>.\n"
+"O custo da interseção foi definido como 0.\n"
+"A camada de categorias exclusivas não é válida, portanto, você provavelmente obterá resultados incorretos."
 
 #: ../lib/vector/Vlib/net_build.c:407
 #, c-format
@@ -6401,6 +6408,9 @@ msgid ""
 "Cost for the intersection was set to 0.\n"
 "The unique categories layer is not valid therefore you will probably get incorrect results."
 msgstr ""
+"Não foi possível encontrar o nó para o ponto que representa a interseção <%d> no campo de categorias exclusivas <%d>.\n"
+"O custo da interseção foi definido como 0.\n"
+"A camada de categorias exclusivas não é válida, portanto, você provavelmente obterá resultados incorretos."
 
 #: ../lib/vector/Vlib/net_build.c:432
 #, c-format
@@ -6480,7 +6490,7 @@ msgstr ""
 #: ../lib/vector/Vlib/snap.c:527 ../lib/vector/Vlib/snap.c:890
 #, c-format
 msgid "New vertices: %d"
-msgstr ""
+msgstr "Novos vértices: %d"
 
 #: ../lib/vector/Vlib/snap.c:917
 msgid "Reading features..."
@@ -6557,7 +6567,7 @@ msgstr "Atualizando dados TopoGeometry..."
 #: ../lib/vector/Vlib/build_pg.c:464
 #, c-format
 msgid "Unsupported topo geometry type %d"
-msgstr ""
+msgstr "Tipo de geometria %d de topologia não suportado "
 
 #: ../lib/vector/Vlib/build_pg.c:493
 #, c-format
@@ -6566,7 +6576,7 @@ msgstr "Não foi possível criar <%s.%s>"
 
 #: ../lib/vector/Vlib/build_pg.c:644
 msgid "Unable to write nodes, offset array mismatch"
-msgstr ""
+msgstr "Não foi possível escrever nós, incompatibilidade de array de deslocamento"
 
 #: ../lib/vector/Vlib/build_pg.c:673
 msgid "Unable to write nodes"
@@ -6584,7 +6594,7 @@ msgstr "Não foi possível escrever linhas"
 #: ../lib/vector/Vlib/build_pg.c:792
 #, c-format
 msgid "Topology for centroid %d not available. Area %d skipped"
-msgstr "Topologia para centróide %d não disponível. Área %d ignorada"
+msgstr "Topologia para centroide %d não disponível. Área %d ignorada"
 
 #: ../lib/vector/Vlib/build_pg.c:1002
 msgid "Create simple features topology from topogeometry data..."
@@ -6757,7 +6767,7 @@ msgstr "Não foi possível ler arquivo de entrada <%s>"
 #: ../lib/cairodriver/read_xid.c:20
 #, c-format
 msgid "Unable to parse input file <%s>"
-msgstr "Não foi possível analisar o arquivo de entrada <%s>"
+msgstr "Não foi possível interpretar o arquivo de entrada <%s>"
 
 #: ../lib/cairodriver/raster.c:109
 msgid "Failed to create cairo surface"
@@ -7046,7 +7056,7 @@ msgstr "Leia o mapa g3d <%s> na memória"
 
 #: ../lib/gpde/n_arrays_io.c:364 ../lib/gpde/n_arrays_io.c:464
 msgid "Error closing g3d file"
-msgstr "Erro fechando arquivo g3d"
+msgstr "Erro ao fechar arquivo g3d"
 
 #: ../lib/gpde/n_arrays_io.c:420
 #, c-format
@@ -7609,7 +7619,7 @@ msgstr "Formato de granulação incorreto: %s"
 #: ../lib/python/temporal/abstract_space_time_dataset.py:1777
 #, python-format
 msgid "Unable to snap dataset <%(ds)s> of type %(type)s in the temporal database. The mapset of the dataset does not match the current mapset"
-msgstr "Não foi possível ajustar o conjunto de dados <%(ds)s> do tipo %(type)s no banco de dados temporal. O mapset do conjunto de dados não corresponde ao  mapset atual"
+msgstr "Não foi possível ligar o conjunto de dados <%(ds)s> do tipo %(type)s ao banco de dados temporal. O mapset do conjunto de dados não corresponde ao  mapset atual"
 
 #: ../lib/python/temporal/abstract_space_time_dataset.py:1879
 #, python-format
@@ -8022,7 +8032,7 @@ msgstr "Fusos horários não são suportados"
 #: ../lib/python/temporal/datetime_math.py:811
 #, python-format
 msgid "Unable to parse time string: %s"
-msgstr "Não foi possível analisar string <%s>"
+msgstr "Não foi possível interpretar string <%s>"
 
 #: ../lib/python/temporal/sampling.py:93 ../lib/python/temporal/sampling.py:98
 #, python-format
@@ -8391,7 +8401,7 @@ msgstr ""
 #: ../lib/python/script/task.py:524
 #, python-brace-format
 msgid "Cannot parse interface description of<{name}> module: {error}"
-msgstr "Não é possível analisar a descrição da interface do módulo <{name}>: {error}"
+msgstr "Não é possível interpretar a descrição da interface do módulo <{name}>: {error}"
 
 #: ../lib/python/script/raster3d.py:92
 #, python-format
@@ -8578,12 +8588,12 @@ msgstr ""
 #: ../lib/raster3d/range.c:88
 #, c-format
 msgid "Error reading range file for [%s in %s]"
-msgstr ""
+msgstr "Erro ao ler arquivo de intervalo para [%s in %s]"
 
 #: ../lib/raster3d/range.c:150
 #, c-format
 msgid "Unable to open range file for <%s>"
-msgstr ""
+msgstr "Não foi possível abrir o arquivo de intervalo para <%s>"
 
 #: ../lib/raster3d/close.c:47
 #, c-format
@@ -8597,11 +8607,11 @@ msgstr "Não foi possível mover o mapa raster temporário <%s> para o mapa rast
 
 #: ../lib/raster3d/close.c:79
 msgid "Unable to flush all tiles"
-msgstr ""
+msgstr "Não foi possível limpar todos os mosaicos"
 
 #: ../lib/raster3d/close.c:84
 msgid "Unable to flush index"
-msgstr ""
+msgstr "Não foi possível limpar o índice"
 
 #: ../lib/raster3d/close.c:94
 msgid "Unable to position file"
@@ -9172,7 +9182,7 @@ msgstr "Erro ao ler linha nula %d para <%s>"
 #: ../lib/raster/close.c:56
 #, c-format
 msgid "Unable to flush file %s for raster map %s: %s"
-msgstr ""
+msgstr "Não foi possível limpar o arquivo %s para o mapa raster %s: %s"
 
 #: ../lib/raster/close.c:64
 #, c-format
@@ -9224,11 +9234,11 @@ msgstr "não consegui escrever as informações de categoria para [%s] em [%s]"
 #: ../lib/raster/get_cellhd.c:66
 #, c-format
 msgid "Unable to read header file for raster map <%s@%s>. It is a reclass of raster map <%s@%s> %s"
-msgstr ""
+msgstr "Não foi possível ler o arquivo de cabeçalho do mapa raster <%s@%s>. É uma reclassificação do mapa raster <%s@%s> %s"
 
 #: ../lib/raster/get_cellhd.c:70
 msgid "which is missing."
-msgstr ""
+msgstr "que está faltando."
 
 #: ../lib/raster/get_cellhd.c:71
 msgid "whose header file can't be opened."
@@ -9303,12 +9313,12 @@ msgstr "Suporte de cor para <%s@%s> %s"
 #: ../lib/raster/open.c:192
 #, c-format
 msgid "Unable to open raster map <%s@%s> since it is a reclass of raster map <%s@%s> which does not exist"
-msgstr ""
+msgstr "Não foi possível abrir o mapa raster <%s@%s> uma vez que é uma reclassificaçãao do mapa raster <%s@%s> que não existe"
 
 #: ../lib/raster/open.c:197
 #, c-format
 msgid "Error reading reclass file for raster map <%s>"
-msgstr ""
+msgstr "Erro ao ler arquivo de reclassificação para mapa raster <%s>"
 
 #: ../lib/raster/open.c:208
 #, c-format
@@ -9549,7 +9559,7 @@ msgstr "O driver <%s> não suporta escrita direta. Usando o driver MEM para um c
 
 #: ../lib/raster/gdal.c:513
 msgid "Unable to get in-memory raster driver"
-msgstr ""
+msgstr "Não foi possível obter o driver de raster na memória"
 
 #: ../lib/raster/gdal.c:520
 #, c-format
@@ -9672,12 +9682,12 @@ msgstr "Rast_set_null_value: tipo de dados errado!"
 #: ../lib/raster/reclass.c:168
 #, c-format
 msgid "Too many reclass categories for <%s@%s>"
-msgstr ""
+msgstr "Muitas categorias de reclassificação para <%s@%s>"
 
 #: ../lib/raster/reclass.c:171
 #, c-format
 msgid "Illegal reclass format in header file for <%s@%s>"
-msgstr ""
+msgstr "Formato de reclassificação ilegal no arquivo de cabeçalho para <%s@%s>"
 
 #: ../lib/raster/reclass.c:273
 msgid "Illegal reclass request"
@@ -9960,7 +9970,7 @@ msgstr "Erro no arquivo da tabela de dados <%s>, linha %d"
 
 #: ../lib/proj/convert.c:194
 msgid "Unable parse GRASS PROJ_INFO file"
-msgstr "Não foi possível analisar o arquivo GRASS PROJ_INFO"
+msgstr "Não foi possível interpretar o arquivo GRASS PROJ_INFO"
 
 #: ../lib/proj/convert.c:201
 msgid "Unable get PROJ.4-style parameter string"
@@ -9969,7 +9979,7 @@ msgstr "Não é possível obter string de parâmetro com estilo PROJ.4"
 #: ../lib/proj/convert.c:219
 #, c-format
 msgid "OGR can't parse PROJ.4-style parameter string: %s (OGR Error code was %d)"
-msgstr "O OGR não consegue analisar a string de parâmetros no estilo PROJ.4: %s (o código de erro OGR era %d)"
+msgstr "O OGR não consegue interpretar a string de parâmetros no estilo PROJ.4: %s (o código de erro OGR era %d)"
 
 #: ../lib/proj/convert.c:232
 #, c-format


=====================================
locale/po/grassmods_fr.po
=====================================
@@ -9277,7 +9277,7 @@ msgstr "Installer"
 
 #: ../locale/scriptstrings/g.extension_to_translate.c:26
 msgid "Download source code and exit"
-msgstr "Télecharger le code qource et quitter"
+msgstr "Télécharger le code source et quitter"
 
 #: ../locale/scriptstrings/g.extension_to_translate.c:28
 msgid "Do not install new extension, just compile it"
@@ -26441,7 +26441,7 @@ msgstr "Impossible d'ouvrir la carte vecteur <%s>"
 #: ../vector/v.colors/scan_attr.c:24 ../doc/vector/v.example/main.c:109
 #, c-format
 msgid "Database connection not defined for layer %d"
-msgstr ""
+msgstr "La connexion à la base de données n'a pas été définie pour la couche %d"
 
 #: ../display/d.vect.chart/plot.c:86 ../display/d.vect/attr.c:110
 #: ../vector/v.in.db/main.c:225
@@ -29502,7 +29502,7 @@ msgstr "Impossible de sélectionner les attributs pour cat = %d"
 #: ../vector/v.lrs/v.lrs.label/main.c:331 ../vector/v.out.ogr/attrb.c:53
 #: ../vector/v.overlay/main.c:503
 msgid "Unable to fetch data from table"
-msgstr ""
+msgstr "Impossible de récupérer les données depuis la table"
 
 #: ../vector/v.out.vtk/writeVTK.c:632
 msgid "Cannot export attribute table fields for layer < 1. Skipping export"
@@ -29525,7 +29525,7 @@ msgstr ""
 #: ../scripts/db.univar/db.univar.py:107
 #, c-format, python-format
 msgid "Unable to describe table <%s>"
-msgstr ""
+msgstr "Impossible de décrire la table <%s>"
 
 #: ../vector/v.out.vtk/writeVTK.c:673
 msgid "No numerical attributes found. Skipping export"
@@ -30061,7 +30061,7 @@ msgstr ""
 #: ../vector/v.out.ogr/list.c:116 ../vector/v.in.ogr/main.c:442
 #: ../raster/r.in.gdal/main.c:314 ../raster/r.external/list.c:23
 msgid "Supported formats:"
-msgstr ""
+msgstr "Formats supportés : "
 
 #: ../vector/v.external/list.c:111
 msgid "GRASS is not compiled with PostgreSQL support"
@@ -32716,7 +32716,7 @@ msgstr ""
 #: ../vector/v.surf.bspline/main.c:882 ../vector/v.out.postgis/main.c:178
 #: ../vector/v.net.components/main.c:213 ../raster/r.resamp.bspline/main.c:723
 msgid "Writing output..."
-msgstr ""
+msgstr "Écriture de la sortie ..."
 
 #: ../vector/v.kernel/main.c:68
 #, c-format
@@ -33704,7 +33704,7 @@ msgstr ""
 
 #: ../vector/v.out.postgis/args.c:66
 msgid "Do not export attribute table"
-msgstr ""
+msgstr "Ne pas exporter de table attributaire"
 
 #: ../vector/v.out.postgis/args.c:72
 msgid "Export PostGIS topology instead of simple features"
@@ -33755,26 +33755,26 @@ msgstr ""
 #: ../vector/v.out.postgis/main.c:111
 #, c-format
 msgid "Unable to open vector map <%s> on topological level"
-msgstr ""
+msgstr "Impossible d'ouvrir la carte vecteur <%s> au niveau topologique"
 
 #: ../vector/v.out.postgis/main.c:135
 #, c-format
 msgid "Unable to create PostGIS layer <%s>"
-msgstr ""
+msgstr "Impossible de créer la couche PostGIS <%s>"
 
 #: ../vector/v.out.postgis/main.c:160
 #, c-format
 msgid "Feature type '%s' not supported"
-msgstr ""
+msgstr "Type d'entité '%s' non supporté"
 
 #: ../vector/v.out.postgis/main.c:165
 #, c-format
 msgid "Feature type %d is not supported"
-msgstr ""
+msgstr "Type d'entité %d non supporté"
 
 #: ../vector/v.out.postgis/main.c:172
 msgid "Copying features failed"
-msgstr ""
+msgstr "Échec de la copie des objets"
 
 #: ../vector/v.out.postgis/main.c:185
 #, c-format
@@ -33808,7 +33808,7 @@ msgstr ""
 #: ../vector/v.out.postgis/table.c:27
 #, c-format
 msgid "No database connection for layer <%s>"
-msgstr ""
+msgstr "Connexion à la base de données non définie pour la couche <%s>"
 
 #: ../vector/v.out.postgis/table.c:50
 #, c-format
@@ -35123,7 +35123,7 @@ msgstr "Position de référence"
 #: ../vector/v.label/main.c:132 ../vector/v.label/main.c:140
 #: ../vector/v.label/main.c:148
 msgid "Font"
-msgstr ""
+msgstr "Police"
 
 #: ../vector/v.lrs/v.lrs.label/main.c:187 ../vector/v.label.sa/main.c:77
 #: ../vector/v.label/main.c:129
@@ -43118,17 +43118,17 @@ msgstr ""
 
 #: ../doc/vector/v.example/main.c:53
 msgid "My first vector module"
-msgstr ""
+msgstr "Mon module vecteur de départ."
 
 #: ../doc/vector/v.example/main.c:175
 #, c-format
 msgid "Unable to get attribute data for cat %d"
-msgstr ""
+msgstr "Impossible de sélectionner les attributs pour cat = %d"
 
 #: ../doc/vector/v.example/main.c:184
 #, c-format
 msgid "Error while retrieving database record for cat %d"
-msgstr ""
+msgstr "Aucun enregistrement pour la catégorie cat = %d"
 
 #: ../doc/vector/v.example/main.c:217
 #, c-format
@@ -43137,7 +43137,7 @@ msgstr "Impossible de copier la table d'attributs vers la carte vecteur <%s>"
 
 #: ../doc/raster/r.example/main.c:86
 msgid "My first raster module"
-msgstr ""
+msgstr "Mon module raster de départ"
 
 #: ../db/db.select/main.c:53 ../db/db.columns/main.c:44
 #: ../db/db.describe/main.c:50 ../db/db.copy/main.c:100


=====================================
locale/po/grassmods_pt_BR.po
=====================================
The diff for this file was not included because it is too large.

=====================================
locale/po/grasswxpy_pt_BR.po
=====================================
The diff for this file was not included because it is too large.

=====================================
scripts/.flake8
=====================================
@@ -1,5 +1,6 @@
 [flake8]
 ignore =
+    E203, # white space before ':'
     E265, # block comment should start with '# '
     E266, # too many leading '#' for block comment
     E402, # module level import not at top of file


=====================================
scripts/g.extension/g.extension.py
=====================================
@@ -8,13 +8,15 @@
 #               Vaclav Petras <wenzeslaus gmail com> (support for general sources)
 # PURPOSE:      Tool to download and install extensions into local installation
 #
-# COPYRIGHT:    (C) 2009-2019 by Markus Neteler, and the GRASS Development Team
+# COPYRIGHT:    (C) 2009-2021 by Markus Neteler, and 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.
 #
-# TODO:         - add sudo support where needed (i.e. check first permission to write into
+# TODO:         - update temporary workaround of using grass7 subdir of addon-repo, see
+#                 https://github.com/OSGeo/grass-addons/issues/528
+#               - add sudo support where needed (i.e. check first permission to write into
 #                 $GISBASE directory)
 #               - fix toolbox support in install_private_extension_xml()
 #############################################################################
@@ -75,7 +77,6 @@
 #% description: Specific branch to fetch addon from (only used when fetching from git)
 #% required: no
 #% multiple: no
-#% answer: main
 #%end
 
 #%flag
@@ -128,6 +129,10 @@
 #% suppress_required: yes
 #%end
 #%flag
+#% key: o
+#% description: url refers to a fork of the official extension repository
+#%end
+#%flag
 #% key: j
 #% description: Generates JSON file containing the download URLs of the official Addons
 #% guisection: Install
@@ -139,6 +144,8 @@
 #% required: extension, -l, -c, -g, -a, -j
 #% exclusive: extension, -l, -c, -g
 #% exclusive: extension, -l, -c, -a
+#% requires: -o, url
+#% requires: branch, url
 #%end
 
 # TODO: solve addon-extension(-module) confusion
@@ -161,14 +168,16 @@ from distutils.dir_util import copy_tree
 
 from six.moves.urllib import request as urlrequest
 from six.moves.urllib.error import HTTPError, URLError
+from six.moves.urllib.parse import urlparse
 
 # Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
 # and ElementTree 1.3.
 from xml.parsers import expat  # TODO: works for any Python?
-if hasattr(etree, 'ParseError'):
+
+if hasattr(etree, "ParseError"):
     ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
 else:
-    ETREE_EXCEPTIONS = (expat.ExpatError)
+    ETREE_EXCEPTIONS = expat.ExpatError
 
 import grass.script as gscript
 from grass.script.utils import try_rmdir
@@ -182,6 +191,7 @@ HEADERS = {
     "User-Agent": "Mozilla/5.0",
 }
 HTTP_STATUS_CODES = list(http.HTTPStatus)
+GIT_URL = "https://github.com/OSGeo/grass-addons"
 
 
 def replace_shebang_win(python_file):
@@ -213,7 +223,7 @@ def urlretrieve(url, filename, *args, **kwargs):
     """
     request = urlrequest.Request(url, headers=HEADERS)
     response = urlrequest.urlopen(request, *args, **kwargs)
-    with open(filename, 'wb') as f:
+    with open(filename, "wb") as f:
         f.write(response.read())
 
 
@@ -225,8 +235,89 @@ def urlopen(url, *args, **kwargs):
     return urlrequest.urlopen(request, *args, **kwargs)
 
 
-def download_addons_paths_file(
-        url, response_format, *args, **kwargs):
+def get_version_branch(major_version):
+    """Check if version branch for the current GRASS version exists,
+    if not, take branch for the previous version
+    For the official repo we assume that at least one version branch is present"""
+    version_branch = "grass{}".format(major_version)
+    try:
+        urlrequest.urlopen(
+            "{GIT_URL}/tree/{version_branch}/src".format(
+                GIT_URL=GIT_URL, version_branch=version_branch
+            )
+        )
+    except URLError:
+        version_branch = "grass{}".format(int(major_version) - 1)
+    return version_branch
+
+
+def get_github_branches(
+    github_api_url="https://api.github.com/repos/OSGeo/grass-addons/branches",
+    version_only=True,
+):
+    """Get ordered list of branch names in repo using github API
+    For the official repo we assume that at least one version branch is present
+    Due to strict rate limits in the github API (60 calls per hour) this function
+    is currently not used."""
+    req = urlrequest.urlopen(github_api_url)
+    content = json.loads(req.read())
+    branches = [repo_branch["name"] for repo_branch in content]
+    if version_only:
+        branches = [
+            version_branch
+            for version_branch in branches
+            if version_branch.startswith("grass")
+        ]
+    branches.sort()
+    return branches
+
+
+def get_default_branch(full_url):
+    """Get default branch for repository in known hosting services
+    (currently only implemented for github, gitlab and bitbucket API)
+    In all other cases "main" is used as default"""
+    # Parse URL
+    url_parts = urlparse(full_url)
+    # Get organization and repository component
+    try:
+        organization, repository = url_parts.path.split("/")[1:3]
+    except URLError:
+        gscript.fatal(
+            _(
+                "Cannot retrieve organization and repository from URL: <{}>.".format(
+                    full_url
+                )
+            )
+        )
+    # Construct API call and retrieve default branch
+    api_calls = {
+        "github.com": "https://api.github.com/repos/{organization}/{repository}".format(
+            organization=organization, repository=repository
+        ),
+        "gitlab.com": "https://gitlab.com/api/v4/projects/{organization}%2F{repository}".format(
+            organization=organization, repository=repository
+        ),
+        "bitbucket.org": "https://api.bitbucket.org/2.0/repositories/{organization}/{repository}/branching-model?".format(
+            organization=organization, repository=repository
+        ),
+    }
+    # Try to get default branch via API. The API call is known to fail a) if the full_url
+    # does not belong to an implemented hosting service or b) if the rate limit of the
+    # API is exceeded
+    try:
+        req = urlrequest.urlopen(api_calls.get(url_parts.netloc))
+        content = json.loads(req.read())
+        # For github and gitlab
+        default_branch = content.get("default_branch")
+        # For bitbucket
+        if not default_branch:
+            default_branch = content.get("development").get("name")
+    except URLError:
+        default_branch = "main"
+    return default_branch
+
+
+def download_addons_paths_file(url, response_format, *args, **kwargs):
     """Generates JSON file containing the download URLs of the official
     Addons
 
@@ -252,7 +343,7 @@ def download_addons_paths_file(
                     ),
                 ),
             )
-        if response_format not in response.getheader('Content-Type'):
+        if response_format not in response.getheader("Content-Type"):
             gscript.fatal(
                 _(
                     "Wrong downloaded file format. "
@@ -266,10 +357,7 @@ def download_addons_paths_file(
         return response
 
     except HTTPError as err:
-        if (
-                err.code == 403 and
-                err.msg == 'rate limit exceeded'
-        ):
+        if err.code == 403 and err.msg == "rate limit exceeded":
             gscript.warning(
                 _(
                     "The download of the json file with add-ons paths "
@@ -280,7 +368,7 @@ def download_addons_paths_file(
             )
         else:
             return download_addons_paths_file(
-                url=url.replace('main', 'master'),
+                url=url.replace("main", "master"),
                 response_format=response_format,
             )
     except URLError:
@@ -296,7 +384,7 @@ def download_addons_paths_file(
 
 def etree_fromfile(filename):
     """Create XML element tree from a given file name"""
-    with open(filename, 'r') as file_:
+    with open(filename, "r") as file_:
         return etree.fromstring(file_.read())
 
 
@@ -308,12 +396,11 @@ def etree_fromurl(url):
 
 def check_progs():
     """Check if the necessary programs are available"""
-    # TODO: we need svn for the Subversion repo downloads
-    # also git would be tested once supported
-    for prog in ('make', 'gcc'):
-        if not grass.find_program(prog, '--help'):
-            grass.fatal(_("'%s' required. Please install '%s' first.")
-                        % (prog, prog))
+    # git to be tested once supported instead of `svn`
+    for prog in ("make", "gcc", "svn"):
+        if not grass.find_program(prog, "--help"):
+            grass.fatal(_("'%s' required. Please install '%s' first.") % (prog, prog))
+
 
 # expand prefix to class name
 
@@ -330,19 +417,19 @@ def expand_module_class_name(class_letters):
     'vector'
     """
     name = {
-        'd': 'display',
-        'db': 'db',
-        'g': 'general',
-        'i': 'imagery',
-        'm': 'misc',
-        'ps': 'postscript',
-        'p': 'paint',
-        'r': 'raster',
-        'r3': 'raster3d',
-        's': 'sites',
-        't': 'temporal',
-        'v': 'vector',
-        'wx': 'gui/wxpython'
+        "d": "display",
+        "db": "db",
+        "g": "general",
+        "i": "imagery",
+        "m": "misc",
+        "ps": "postscript",
+        "p": "paint",
+        "r": "raster",
+        "r3": "raster3d",
+        "s": "sites",
+        "t": "temporal",
+        "v": "vector",
+        "wx": "gui/wxpython",
     }
 
     return name.get(class_letters, class_letters)
@@ -358,13 +445,13 @@ def get_module_class_name(module_name):
     >>> get_module_class_name('v.to.rast')
     'vector'
     """
-    classchar = module_name.split('.', 1)[0]
+    classchar = module_name.split(".", 1)[0]
     return expand_module_class_name(classchar)
 
 
 def get_installed_extensions(force=False):
     """Get list of installed extensions or toolboxes (if -t is set)"""
-    if flags['t']:
+    if flags["t"]:
         return get_installed_toolboxes(force)
 
     # TODO: extension != module
@@ -379,8 +466,8 @@ def list_installed_extensions(toolboxes=False):
             grass.message(_("List of installed extensions (toolboxes):"))
         else:
             grass.message(_("List of installed extensions (modules):"))
-        sys.stdout.write('\n'.join(elist))
-        sys.stdout.write('\n')
+        sys.stdout.write("\n".join(elist))
+        sys.stdout.write("\n")
     else:
         if toolboxes:
             grass.info(_("No extension (toolbox) installed"))
@@ -395,7 +482,7 @@ def get_installed_toolboxes(force=False):
     Creates a new toolboxes file if it is not possible
     to read the current one.
     """
-    xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
+    xml_file = os.path.join(options["prefix"], "toolboxes.xml")
     if not os.path.exists(xml_file):
         write_xml_toolboxes(xml_file)
     # read XML file
@@ -406,8 +493,8 @@ def get_installed_toolboxes(force=False):
         write_xml_toolboxes(xml_file)
         return []
     ret = list()
-    for tnode in tree.findall('toolbox'):
-        ret.append(tnode.get('code'))
+    for tnode in tree.findall("toolbox"):
+        ret.append(tnode.get("code"))
     return ret
 
 
@@ -418,7 +505,7 @@ def get_installed_modules(force=False):
     Creates a new modules file if it is not possible
     to read the current one.
     """
-    xml_file = os.path.join(options['prefix'], 'modules.xml')
+    xml_file = os.path.join(options["prefix"], "modules.xml")
     if not os.path.exists(xml_file):
         if force:
             write_xml_modules(xml_file)
@@ -433,20 +520,21 @@ def get_installed_modules(force=False):
         write_xml_modules(xml_file)
         return []
     ret = list()
-    for tnode in tree.findall('task'):
-        if flags['g']:
+    for tnode in tree.findall("task"):
+        if flags["g"]:
             desc, keyw = get_optional_params(tnode)
-            ret.append('name={0}'.format(tnode.get('name').strip()))
-            ret.append('description={0}'.format(desc))
-            ret.append('keywords={0}'.format(keyw))
-            ret.append('executables={0}'.format(','.join(
-                get_module_executables(tnode))
-            ))
+            ret.append("name={0}".format(tnode.get("name").strip()))
+            ret.append("description={0}".format(desc))
+            ret.append("keywords={0}".format(keyw))
+            ret.append(
+                "executables={0}".format(",".join(get_module_executables(tnode)))
+            )
         else:
-            ret.append(tnode.get('name').strip())
+            ret.append(tnode.get("name").strip())
 
     return ret
 
+
 # list extensions (read XML file from grass.osgeo.org/addons)
 
 
@@ -456,22 +544,22 @@ def list_available_extensions(url):
     For toolboxes it lists also all modules.
     """
     gscript.debug("list_available_extensions(url={0})".format(url))
-    if flags['t']:
+    if flags["t"]:
         grass.message(_("List of available extensions (toolboxes):"))
         tlist = get_available_toolboxes(url)
         tkeys = sorted(tlist.keys())
         for toolbox_code in tkeys:
             toolbox_data = tlist[toolbox_code]
-            if flags['g']:
-                print('toolbox_name=' + toolbox_data['name'])
-                print('toolbox_code=' + toolbox_code)
+            if flags["g"]:
+                print("toolbox_name=" + toolbox_data["name"])
+                print("toolbox_code=" + toolbox_code)
             else:
-                print('%s (%s)' % (toolbox_data['name'], toolbox_code))
-            if flags['c'] or flags['g']:
-                list_available_modules(url, toolbox_data['modules'])
+                print("%s (%s)" % (toolbox_data["name"], toolbox_code))
+            if flags["c"] or flags["g"]:
+                list_available_modules(url, toolbox_data["modules"])
             else:
-                if toolbox_data['modules']:
-                    print(os.linesep.join(['* ' + x for x in toolbox_data['modules']]))
+                if toolbox_data["modules"]:
+                    print(os.linesep.join(["* " + x for x in toolbox_data["modules"]]))
     else:
         grass.message(_("List of available extensions (modules):"))
         # TODO: extensions with several modules + lib
@@ -484,18 +572,20 @@ def get_available_toolboxes(url):
     url = url + "toolboxes.xml"
     try:
         tree = etree_fromurl(url)
-        for tnode in tree.findall('toolbox'):
+        for tnode in tree.findall("toolbox"):
             mlist = list()
             clist = list()
-            tdict[tnode.get('code')] = {'name': tnode.get('name'),
-                                        'correlate': clist,
-                                        'modules': mlist}
+            tdict[tnode.get("code")] = {
+                "name": tnode.get("name"),
+                "correlate": clist,
+                "modules": mlist,
+            }
 
-            for cnode in tnode.findall('correlate'):
-                clist.append(cnode.get('name'))
+            for cnode in tnode.findall("correlate"):
+                clist.append(cnode.get("name"))
 
-            for mnode in tnode.findall('task'):
-                mlist.append(mnode.get('name'))
+            for mnode in tnode.findall("task"):
+                mlist.append(mnode.get("name"))
     except (HTTPError, IOError, OSError):
         grass.fatal(_("Unable to fetch addons metadata file"))
 
@@ -515,16 +605,16 @@ def get_toolbox_extensions(url, name):
 
     try:
         tree = etree_fromurl(url)
-        for tnode in tree.findall('toolbox'):
-            if name == tnode.get('code'):
-                for enode in tnode.findall('task'):
+        for tnode in tree.findall("toolbox"):
+            if name == tnode.get("code"):
+                for enode in tnode.findall("task"):
                     # extension name
-                    ename = enode.get('name')
+                    ename = enode.get("name")
                     edict[ename] = dict()
                     # list of modules installed by this extension
-                    edict[ename]['mlist'] = list()
+                    edict[ename]["mlist"] = list()
                     # list of files installed by this extension
-                    edict[ename]['flist'] = list()
+                    edict[ename]["flist"] = list()
                 break
     except (HTTPError, IOError, OSError):
         grass.fatal(_("Unable to fetch addons metadata file"))
@@ -538,9 +628,9 @@ def get_module_files(mnode):
     :param mnode: XML node for a module
     """
     flist = []
-    if mnode.find('binary') is None:
+    if mnode.find("binary") is None:
         return flist
-    for file_node in mnode.find('binary').findall('file'):
+    for file_node in mnode.find("binary").findall("file"):
         filepath = file_node.text
         flist.append(filepath)
 
@@ -554,11 +644,12 @@ def get_module_executables(mnode):
     """
     flist = []
     for filepath in get_module_files(mnode):
-        if filepath.startswith(options['prefix'] + os.path.sep + 'bin') or \
-           (sys.platform != 'win32' and
-                filepath.startswith(options['prefix'] + os.path.sep + 'scripts')):
+        if filepath.startswith(options["prefix"] + os.path.sep + "bin") or (
+            sys.platform != "win32"
+            and filepath.startswith(options["prefix"] + os.path.sep + "scripts")
+        ):
             filename = os.path.basename(filepath)
-            if sys.platform == 'win32':
+            if sys.platform == "win32":
                 filename = os.path.splitext(filename)[0]
             flist.append(filename)
 
@@ -571,17 +662,17 @@ def get_optional_params(mnode):
     :param mnode: XML node for a module
     """
     try:
-        desc = mnode.find('description').text
+        desc = mnode.find("description").text
     except AttributeError:
-        desc = ''
+        desc = ""
     if desc is None:
-        desc = ''
+        desc = ""
     try:
-        keyw = mnode.find('keywords').text
+        keyw = mnode.find("keywords").text
     except AttributeError:
-        keyw = ''
+        keyw = ""
     if keyw is None:
-        keyw = ''
+        keyw = ""
 
     return desc, keyw
 
@@ -599,29 +690,34 @@ def list_available_modules(url, mlist=None):
     try:
         tree = etree_fromurl(file_url)
     except ETREE_EXCEPTIONS:
-        grass.warning(_("Unable to parse '%s'. Trying to scan"
-                        " SVN repository (may take some time)...") % file_url)
+        grass.warning(
+            _(
+                "Unable to parse '%s'. Trying to scan"
+                " SVN repository (may take some time)..."
+            )
+            % file_url
+        )
         list_available_extensions_svn(url)
         return
     except (HTTPError, URLError, IOError, OSError):
         list_available_extensions_svn(url)
         return
 
-    for mnode in tree.findall('task'):
-        name = mnode.get('name').strip()
+    for mnode in tree.findall("task"):
+        name = mnode.get("name").strip()
         if mlist and name not in mlist:
             continue
-        if flags['c'] or flags['g']:
+        if flags["c"] or flags["g"]:
             desc, keyw = get_optional_params(mnode)
 
-        if flags['g']:
-            print('name=' + name)
-            print('description=' + desc)
-            print('keywords=' + keyw)
-        elif flags['c']:
+        if flags["g"]:
+            print("name=" + name)
+            print("description=" + desc)
+            print("keywords=" + keyw)
+        elif flags["c"]:
             if mlist:
-                print('*', end='')
-            print(name + ' - ' + desc)
+                print("*", end="")
+            print(name + " - " + desc)
         else:
             print(name)
 
@@ -643,25 +739,26 @@ def list_available_extensions_svn(url):
     :param url: a directory URL (filename will be attached)
     """
     gscript.debug("list_available_extensions_svn(url=%s)" % url, 2)
-    grass.message(_('Fetching list of extensions from'
-                    ' GRASS-Addons SVN repository (be patient)...'))
+    grass.message(
+        _(
+            "Fetching list of extensions from"
+            " GRASS-Addons SVN repository (be patient)..."
+        )
+    )
     pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
 
-    if flags['c']:
-        grass.warning(
-            _("Flag 'c' ignored, addons metadata file not available"))
-    if flags['g']:
-        grass.warning(
-            _("Flag 'g' ignored, addons metadata file not available"))
+    if flags["c"]:
+        grass.warning(_("Flag 'c' ignored, addons metadata file not available"))
+    if flags["g"]:
+        grass.warning(_("Flag 'g' ignored, addons metadata file not available"))
 
-    prefixes = ['d', 'db', 'g', 'i', 'm', 'ps',
-                'p', 'r', 'r3', 's', 't', 'v']
+    prefixes = ["d", "db", "g", "i", "m", "ps", "p", "r", "r3", "s", "t", "v"]
     for prefix in prefixes:
         modclass = expand_module_class_name(prefix)
         grass.verbose(_("Checking for '%s' modules...") % modclass)
 
         # construct a full URL of a file
-        file_url = '%s/%s' % (url, modclass)
+        file_url = "%s/%s" % (url, modclass)
         grass.debug("url = %s" % file_url, debug=2)
         try:
             file_ = urlopen(url)
@@ -674,8 +771,8 @@ def list_available_extensions_svn(url):
             sline = pattern.search(line)
             if not sline:
                 continue
-            name = sline.group(2).rstrip('/')
-            if name.split('.', 1)[0] == prefix:
+            name = sline.group(2).rstrip("/")
+            if name.split(".", 1)[0] == prefix:
                 print(name)
 
     # get_wxgui_extensions(url)
@@ -688,26 +785,28 @@ def get_wxgui_extensions(url):
     :param url: a directory URL (filename will be attached)
     """
     mlist = list()
-    grass.debug('Fetching list of wxGUI extensions from '
-                'GRASS-Addons SVN repository (be patient)...')
+    grass.debug(
+        "Fetching list of wxGUI extensions from "
+        "GRASS-Addons SVN repository (be patient)..."
+    )
     pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
-    grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
+    grass.verbose(_("Checking for '%s' modules...") % "gui/wxpython")
 
     # construct a full URL of a file
-    url = '%s/%s' % (url, 'gui/wxpython')
+    url = "%s/%s" % (url, "gui/wxpython")
     grass.debug("url = %s" % url, debug=2)
     file_ = urlopen(url)
     if not file_:
         grass.warning(_("Unable to fetch '%s'") % url)
         return
 
-    for line in file.readlines():
+    for line in file_.readlines():
         # list extensions
         sline = pattern.search(line)
         if not sline:
             continue
-        name = sline.group(2).rstrip('/')
-        if name not in ('..', 'Makefile'):
+        name = sline.group(2).rstrip("/")
+        if name not in ("..", "Makefile"):
             mlist.append(name)
 
     return mlist
@@ -719,7 +818,7 @@ def cleanup():
         try_rmdir(TMPDIR)
     else:
         grass.message("\n%s\n" % _("Path to the source code:"))
-        sys.stderr.write('%s\n' % os.path.join(TMPDIR, options['extension']))
+        sys.stderr.write("%s\n" % os.path.join(TMPDIR, options["extension"]))
 
 
 def write_xml_modules(name, tree=None):
@@ -730,38 +829,41 @@ def write_xml_modules(name, tree=None):
     :param name: file name
     :param tree: XML element tree
     """
-    file_ = open(name, 'w')
+    file_ = open(name, "w")
     file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
     file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
     file_.write('<addons version="%s">\n' % version[0])
 
-    libgis_revison = grass.version()['libgis_revision']
+    libgis_revison = grass.version()["libgis_revision"]
     if tree is not None:
-        for tnode in tree.findall('task'):
+        for tnode in tree.findall("task"):
             indent = 4
-            file_.write('%s<task name="%s">\n' %
-                        (' ' * indent, tnode.get('name')))
+            file_.write('%s<task name="%s">\n' % (" " * indent, tnode.get("name")))
             indent += 4
-            file_.write('%s<description>%s</description>\n' %
-                        (' ' * indent, tnode.find('description').text))
-            file_.write('%s<keywords>%s</keywords>\n' %
-                        (' ' * indent, tnode.find('keywords').text))
-            bnode = tnode.find('binary')
+            file_.write(
+                "%s<description>%s</description>\n"
+                % (" " * indent, tnode.find("description").text)
+            )
+            file_.write(
+                "%s<keywords>%s</keywords>\n"
+                % (" " * indent, tnode.find("keywords").text)
+            )
+            bnode = tnode.find("binary")
             if bnode is not None:
-                file_.write('%s<binary>\n' % (' ' * indent))
+                file_.write("%s<binary>\n" % (" " * indent))
                 indent += 4
-                for fnode in bnode.findall('file'):
-                    file_.write('%s<file>%s</file>\n' %
-                                (' ' * indent, os.path.join(options['prefix'],
-                                                            fnode.text)))
+                for fnode in bnode.findall("file"):
+                    file_.write(
+                        "%s<file>%s</file>\n"
+                        % (" " * indent, os.path.join(options["prefix"], fnode.text))
+                    )
                 indent -= 4
-                file_.write('%s</binary>\n' % (' ' * indent))
-            file_.write('%s<libgis revision="%s" />\n' %
-                        (' ' * indent, libgis_revison))
+                file_.write("%s</binary>\n" % (" " * indent))
+            file_.write('%s<libgis revision="%s" />\n' % (" " * indent, libgis_revison))
             indent -= 4
-            file_.write('%s</task>\n' % (' ' * indent))
+            file_.write("%s</task>\n" % (" " * indent))
 
-    file_.write('</addons>\n')
+    file_.write("</addons>\n")
     file_.close()
 
 
@@ -773,18 +875,17 @@ def write_xml_extensions(name, tree=None):
     :param name: file name
     :param tree: XML element tree
     """
-    file_ = open(name, 'w')
+    file_ = open(name, "w")
     file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
     file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
     file_.write('<addons version="%s">\n' % version[0])
 
-    libgis_revison = grass.version()['libgis_revision']
+    libgis_revison = grass.version()["libgis_revision"]
     if tree is not None:
-        for tnode in tree.findall('task'):
+        for tnode in tree.findall("task"):
             indent = 4
             # extension name
-            file_.write('%s<task name="%s">\n' %
-                        (' ' * indent, tnode.get('name')))
+            file_.write('%s<task name="%s">\n' % (" " * indent, tnode.get("name")))
             indent += 4
             """
             file_.write('%s<description>%s</description>\n' %
@@ -793,33 +894,32 @@ def write_xml_extensions(name, tree=None):
                         (' ' * indent, tnode.find('keywords').text))
             """
             # extension files
-            bnode = tnode.find('binary')
+            bnode = tnode.find("binary")
             if bnode is not None:
-                file_.write('%s<binary>\n' % (' ' * indent))
+                file_.write("%s<binary>\n" % (" " * indent))
                 indent += 4
-                for fnode in bnode.findall('file'):
-                    file_.write('%s<file>%s</file>\n' %
-                                (' ' * indent, os.path.join(options['prefix'],
-                                                            fnode.text)))
+                for fnode in bnode.findall("file"):
+                    file_.write(
+                        "%s<file>%s</file>\n"
+                        % (" " * indent, os.path.join(options["prefix"], fnode.text))
+                    )
                 indent -= 4
-                file_.write('%s</binary>\n' % (' ' * indent))
+                file_.write("%s</binary>\n" % (" " * indent))
             # extension modules
-            mnode = tnode.find('modules')
+            mnode = tnode.find("modules")
             if mnode is not None:
-                file_.write('%s<modules>\n' % (' ' * indent))
+                file_.write("%s<modules>\n" % (" " * indent))
                 indent += 4
-                for fnode in mnode.findall('module'):
-                    file_.write('%s<module>%s</module>\n' %
-                                (' ' * indent, fnode.text))
+                for fnode in mnode.findall("module"):
+                    file_.write("%s<module>%s</module>\n" % (" " * indent, fnode.text))
                 indent -= 4
-                file_.write('%s</modules>\n' % (' ' * indent))
+                file_.write("%s</modules>\n" % (" " * indent))
 
-            file_.write('%s<libgis revision="%s" />\n' %
-                        (' ' * indent, libgis_revison))
+            file_.write('%s<libgis revision="%s" />\n' % (" " * indent, libgis_revison))
             indent -= 4
-            file_.write('%s</task>\n' % (' ' * indent))
+            file_.write("%s</task>\n" % (" " * indent))
 
-    file_.write('</addons>\n')
+    file_.write("</addons>\n")
     file_.close()
 
 
@@ -831,38 +931,44 @@ def write_xml_toolboxes(name, tree=None):
     :param name: file name
     :param tree: XML element tree
     """
-    file_ = open(name, 'w')
+    file_ = open(name, "w")
     file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
     file_.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
     file_.write('<addons version="%s">\n' % version[0])
     if tree is not None:
-        for tnode in tree.findall('toolbox'):
+        for tnode in tree.findall("toolbox"):
             indent = 4
-            file_.write('%s<toolbox name="%s" code="%s">\n' %
-                        (' ' * indent, tnode.get('name'), tnode.get('code')))
+            file_.write(
+                '%s<toolbox name="%s" code="%s">\n'
+                % (" " * indent, tnode.get("name"), tnode.get("code"))
+            )
             indent += 4
-            for cnode in tnode.findall('correlate'):
-                file_.write('%s<correlate code="%s" />\n' %
-                            (' ' * indent, tnode.get('code')))
-            for mnode in tnode.findall('task'):
-                file_.write('%s<task name="%s" />\n' %
-                            (' ' * indent, mnode.get('name')))
+            for cnode in tnode.findall("correlate"):
+                file_.write(
+                    '%s<correlate code="%s" />\n' % (" " * indent, tnode.get("code"))
+                )
+            for mnode in tnode.findall("task"):
+                file_.write(
+                    '%s<task name="%s" />\n' % (" " * indent, mnode.get("name"))
+                )
             indent -= 4
-            file_.write('%s</toolbox>\n' % (' ' * indent))
+            file_.write("%s</toolbox>\n" % (" " * indent))
 
-    file_.write('</addons>\n')
+    file_.write("</addons>\n")
     file_.close()
 
 
 def install_extension(source, url, xmlurl, branch):
     """Install extension (e.g. one module) or a toolbox (list of modules)"""
-    gisbase = os.getenv('GISBASE')
+    gisbase = os.getenv("GISBASE")
     if not gisbase:
-        grass.fatal(_('$GISBASE not defined'))
+        grass.fatal(_("$GISBASE not defined"))
 
-    if options['extension'] in get_installed_extensions(force=True):
-        grass.warning(_("Extension <%s> already installed. Re-installing...") %
-                      options['extension'])
+    if options["extension"] in get_installed_extensions(force=True):
+        grass.warning(
+            _("Extension <%s> already installed. Re-installing...")
+            % options["extension"]
+        )
 
     # create a dictionary of extensions
     # for each extension
@@ -870,16 +976,16 @@ def install_extension(source, url, xmlurl, branch):
     #   - a list of files installed by this extension
 
     edict = None
-    if flags['t']:
-        grass.message(_("Installing toolbox <%s>...") % options['extension'])
-        edict = get_toolbox_extensions(xmlurl, options['extension'])
+    if flags["t"]:
+        grass.message(_("Installing toolbox <%s>...") % options["extension"])
+        edict = get_toolbox_extensions(xmlurl, options["extension"])
     else:
         edict = dict()
-        edict[options['extension']] = dict()
+        edict[options["extension"]] = dict()
         # list of modules installed by this extension
-        edict[options['extension']]['mlist'] = list()
+        edict[options["extension"]]["mlist"] = list()
         # list of files installed by this extension
-        edict[options['extension']]['flist'] = list()
+        edict[options["extension"]]["flist"] = list()
     if not edict:
         grass.warning(_("Nothing to install"))
         return
@@ -894,22 +1000,29 @@ def install_extension(source, url, xmlurl, branch):
         if sys.platform == "win32":
             ret1, new_modules_ext, new_files_ext = install_extension_win(extension)
         else:
-            ret1, new_modules_ext, new_files_ext, tmp_dir = install_extension_std_platforms(extension,
-                                                            source=source, url=url, branch=branch)
-        if not flags['d'] and not flags['i']:
-            edict[extension]['mlist'].extend(new_modules_ext)
-            edict[extension]['flist'].extend(new_files_ext)
+            (
+                ret1,
+                new_modules_ext,
+                new_files_ext,
+                tmp_dir,
+            ) = install_extension_std_platforms(
+                extension, source=source, url=url, branch=branch
+            )
+        if not flags["d"] and not flags["i"]:
+            edict[extension]["mlist"].extend(new_modules_ext)
+            edict[extension]["flist"].extend(new_files_ext)
             new_modules.extend(new_modules_ext)
             ret += ret1
             if len(edict) > 1:
-                print('-' * 60)
+                print("-" * 60)
 
-    if flags['d'] or flags['i']:
+    if flags["d"] or flags["i"]:
         return
 
     if ret != 0:
-        grass.warning(_('Installation failed, sorry.'
-                        ' Please check above error messages.'))
+        grass.warning(
+            _("Installation failed, sorry." " Please check above error messages.")
+        )
     else:
         # update extensions metadata file
         grass.message(_("Updating extensions metadata file..."))
@@ -922,13 +1035,18 @@ def install_extension(source, url, xmlurl, branch):
         for module in new_modules:
             update_manual_page(module)
 
-        grass.message(_("Installation of <%s> successfully finished") %
-                      options['extension'])
+        grass.message(
+            _("Installation of <%s> successfully finished") % options["extension"]
+        )
 
-    if not os.getenv('GRASS_ADDON_BASE'):
-        grass.warning(_('This add-on module will not function until'
-                        ' you set the GRASS_ADDON_BASE environment'
-                        ' variable (see "g.manual variables")'))
+    if not os.getenv("GRASS_ADDON_BASE"):
+        grass.warning(
+            _(
+                "This add-on module will not function until"
+                " you set the GRASS_ADDON_BASE environment"
+                ' variable (see "g.manual variables")'
+            )
+        )
 
 
 def get_toolboxes_metadata(url):
@@ -943,24 +1061,23 @@ def get_toolboxes_metadata(url):
     data = dict()
     try:
         tree = etree_fromurl(url)
-        for tnode in tree.findall('toolbox'):
+        for tnode in tree.findall("toolbox"):
             clist = list()
-            for cnode in tnode.findall('correlate'):
-                clist.append(cnode.get('code'))
+            for cnode in tnode.findall("correlate"):
+                clist.append(cnode.get("code"))
 
             mlist = list()
-            for mnode in tnode.findall('task'):
-                mlist.append(mnode.get('name'))
+            for mnode in tnode.findall("task"):
+                mlist.append(mnode.get("name"))
 
-            code = tnode.get('code')
+            code = tnode.get("code")
             data[code] = {
-                'name': tnode.get('name'),
-                'correlate': clist,
-                'modules': mlist,
+                "name": tnode.get("name"),
+                "correlate": clist,
+                "modules": mlist,
             }
     except (HTTPError, IOError, OSError):
-        grass.error(_("Unable to read addons metadata file "
-                      "from the remote server"))
+        grass.error(_("Unable to read addons metadata file " "from the remote server"))
     return data
 
 
@@ -976,40 +1093,39 @@ def install_toolbox_xml(url, name):
         grass.warning(_("No addons metadata available for <%s>") % name)
         return
 
-    xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
+    xml_file = os.path.join(options["prefix"], "toolboxes.xml")
     # create an empty file if not exists
     if not os.path.exists(xml_file):
         write_xml_modules(xml_file)
 
     # read XML file
-    with open(xml_file, 'r') as xml:
+    with open(xml_file, "r") as xml:
         tree = etree.fromstring(xml.read())
 
     # update tree
     tnode = None
-    for node in tree.findall('toolbox'):
-        if node.get('code') == name:
+    for node in tree.findall("toolbox"):
+        if node.get("code") == name:
             tnode = node
             break
 
     tdata = data[name]
     if tnode is not None:
         # update existing node
-        for cnode in tnode.findall('correlate'):
+        for cnode in tnode.findall("correlate"):
             tnode.remove(cnode)
-        for mnode in tnode.findall('task'):
+        for mnode in tnode.findall("task"):
             tnode.remove(mnode)
     else:
         # create new node for task
-        tnode = etree.Element(
-            'toolbox', attrib={'name': tdata['name'], 'code': name})
+        tnode = etree.Element("toolbox", attrib={"name": tdata["name"], "code": name})
         tree.append(tnode)
 
-    for cname in tdata['correlate']:
-        cnode = etree.Element('correlate', attrib={'code': cname})
+    for cname in tdata["correlate"]:
+        cnode = etree.Element("correlate", attrib={"code": cname})
         tnode.append(cnode)
-    for tname in tdata['modules']:
-        mnode = etree.Element('task', attrib={'name': tname})
+    for tname in tdata["modules"]:
+        mnode = etree.Element("task", attrib={"name": tname})
         tnode.append(mnode)
 
     write_xml_toolboxes(xml_file, tree)
@@ -1031,36 +1147,39 @@ def get_addons_metadata(url, mlist):
     try:
         tree = etree_fromurl(url)
     except (HTTPError, URLError, IOError, OSError) as error:
-        grass.error(_("Unable to read addons metadata file"
-                      " from the remote server: {0}").format(error))
+        grass.error(
+            _(
+                "Unable to read addons metadata file" " from the remote server: {0}"
+            ).format(error)
+        )
         return data, bin_list
     except ETREE_EXCEPTIONS as error:
         grass.warning(_("Unable to parse '%s': {0}").format(error) % url)
         return data, bin_list
-    for mnode in tree.findall('task'):
-        name = mnode.get('name')
+    for mnode in tree.findall("task"):
+        name = mnode.get("name")
         if name not in mlist:
             continue
         file_list = list()
-        bnode = mnode.find('binary')
-        windows = sys.platform == 'win32'
+        bnode = mnode.find("binary")
+        windows = sys.platform == "win32"
         if bnode is not None:
-            for fnode in bnode.findall('file'):
-                path = fnode.text.split('/')
-                if path[0] == 'bin':
+            for fnode in bnode.findall("file"):
+                path = fnode.text.split("/")
+                if path[0] == "bin":
                     bin_list.append(path[-1])
                     if windows:
-                        path[-1] += '.exe'
-                elif path[0] == 'scripts':
+                        path[-1] += ".exe"
+                elif path[0] == "scripts":
                     bin_list.append(path[-1])
                     if windows:
-                        path[-1] += '.py'
+                        path[-1] += ".py"
                 file_list.append(os.path.sep.join(path))
         desc, keyw = get_optional_params(mnode)
         data[name] = {
-            'desc': desc,
-            'keyw': keyw,
-            'files': file_list,
+            "desc": desc,
+            "keyw": keyw,
+            "files": file_list,
         }
     return data, bin_list
 
@@ -1075,7 +1194,7 @@ def install_extension_xml(edict):
     #     # read metadata from remote server (toolboxes)
     #     install_toolbox_xml(url, options['extension'])
 
-    xml_file = os.path.join(options['prefix'], 'extensions.xml')
+    xml_file = os.path.join(options["prefix"], "extensions.xml")
     # create an empty file if not exists
     if not os.path.exists(xml_file):
         write_xml_extensions(xml_file)
@@ -1100,14 +1219,14 @@ def install_extension_xml(edict):
         """
 
         tnode = None
-        for node in tree.findall('task'):
-            if node.get('name') == name:
+        for node in tree.findall("task"):
+            if node.get("name") == name:
                 tnode = node
                 break
 
         if tnode is None:
             # create new node for task
-            tnode = etree.Element('task', attrib={'name': name})
+            tnode = etree.Element("task", attrib={"name": name})
             """
             dnode = etree.Element('description')
             dnode.text = desc
@@ -1118,25 +1237,27 @@ def install_extension_xml(edict):
             """
 
             # create binary
-            bnode = etree.Element('binary')
+            bnode = etree.Element("binary")
             # list of all installed files for this extension
-            for file_name in edict[name]['flist']:
-                fnode = etree.Element('file')
+            for file_name in edict[name]["flist"]:
+                fnode = etree.Element("file")
                 fnode.text = file_name
                 bnode.append(fnode)
             tnode.append(bnode)
 
             # create modules
-            msnode = etree.Element('modules')
+            msnode = etree.Element("modules")
             # list of all installed modules for this extension
-            for module_name in edict[name]['mlist']:
-                mnode = etree.Element('module')
+            for module_name in edict[name]["mlist"]:
+                mnode = etree.Element("module")
                 mnode.text = module_name
                 msnode.append(mnode)
             tnode.append(msnode)
             tree.append(tnode)
         else:
-            grass.verbose("Extension already listed in metadata file; metadata not updated!")
+            grass.verbose(
+                "Extension already listed in metadata file; metadata not updated!"
+            )
     write_xml_extensions(xml_file, tree)
 
     return None
@@ -1148,7 +1269,7 @@ def install_module_xml(mlist):
 
     """
 
-    xml_file = os.path.join(options['prefix'], 'modules.xml')
+    xml_file = os.path.join(options["prefix"], "modules.xml")
     # create an empty file if not exists
     if not os.path.exists(xml_file):
         write_xml_modules(xml_file)
@@ -1163,25 +1284,28 @@ def install_module_xml(mlist):
             desc = gtask.parse_interface(name).description
             # mname = gtask.parse_interface(name).name
             keywords = gtask.parse_interface(name).keywords
-        except Exception as e:
-            grass.warning(_("No metadata available for module '%s'.")
-                          % name)
+        except Exception as error:
+            grass.warning(
+                _("No metadata available for module '{name}': {error}").format(
+                    name=name, error=error
+                )
+            )
             continue
 
         tnode = None
-        for node in tree.findall('task'):
-            if node.get('name') == name:
+        for node in tree.findall("task"):
+            if node.get("name") == name:
                 tnode = node
                 break
 
         if tnode is None:
             # create new node for task
-            tnode = etree.Element('task', attrib={'name': name})
-            dnode = etree.Element('description')
+            tnode = etree.Element("task", attrib={"name": name})
+            dnode = etree.Element("description")
             dnode.text = desc
             tnode.append(dnode)
-            knode = etree.Element('keywords')
-            knode.text = (',').join(keywords)
+            knode = etree.Element("keywords")
+            knode.text = (",").join(keywords)
             tnode.append(knode)
 
             # binary files installed with an extension are now
@@ -1220,7 +1344,9 @@ def install_module_xml(mlist):
             """
             tree.append(tnode)
         else:
-            grass.verbose("Extension module already listed in metadata file; metadata not updated!")
+            grass.verbose(
+                "Extension module already listed in metadata file; metadata not updated!"
+            )
     write_xml_modules(xml_file, tree)
 
     return mlist
@@ -1228,35 +1354,47 @@ def install_module_xml(mlist):
 
 def install_extension_win(name):
     """Install extension on MS Windows"""
-    grass.message(_("Downloading precompiled GRASS Addons <%s>...") %
-                  options['extension'])
+    grass.message(
+        _("Downloading precompiled GRASS Addons <%s>...") % options["extension"]
+    )
 
     # build base URL
-    if build_platform == 'x86_64':
+    if build_platform == "x86_64":
         platform = build_platform
     else:
-        platform = 'x86'
-    base_url = "http://wingrass.fsv.cvut.cz/" \
-               "grass%(major)s%(minor)s/%(platform)s/addons/" \
-               "grass-%(major)s.%(minor)s.%(patch)s" % \
-               {'platform': platform,
-                'major': version[0], 'minor': version[1],
-                'patch': version[2]}
+        platform = "x86"
+    base_url = (
+        "http://wingrass.fsv.cvut.cz/"
+        "grass%(major)s%(minor)s/%(platform)s/addons/"
+        "grass-%(major)s.%(minor)s.%(patch)s"
+        % {
+            "platform": platform,
+            "major": version[0],
+            "minor": version[1],
+            "patch": version[2],
+        }
+    )
 
     # resolve ZIP URL
-    source, url = resolve_source_code(url='{0}/{1}.zip'.format(base_url, name))
+    source, url = resolve_source_code(url="{0}/{1}.zip".format(base_url, name))
 
     # to hide non-error messages from subprocesses
     if grass.verbosity() <= 2:
-        outdev = open(os.devnull, 'w')
+        outdev = open(os.devnull, "w")
     else:
         outdev = sys.stdout
 
     # download Addons ZIP file
     os.chdir(TMPDIR)  # this is just to not leave something behind
     srcdir = os.path.join(TMPDIR, name)
-    download_source_code(source=source, url=url, name=name,
-                         outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
+    download_source_code(
+        source=source,
+        url=url,
+        name=name,
+        outdev=outdev,
+        directory=srcdir,
+        tmpdir=TMPDIR,
+    )
 
     # collect module names and file names
     module_list = list()
@@ -1273,7 +1411,7 @@ def install_extension_win(name):
     pyfiles = []
     for r, d, f in os.walk(srcdir):
         for file in f:
-            if file.endswith('.py'):
+            if file.endswith(".py"):
                 pyfiles.append(os.path.join(r, file))
 
     for filename in pyfiles:
@@ -1281,18 +1419,19 @@ def install_extension_win(name):
 
     # collect old files
     old_file_list = list()
-    for r, d, f in os.walk(options['prefix']):
+    for r, d, f in os.walk(options["prefix"]):
         for filename in f:
             fullname = os.path.join(r, filename)
             old_file_list.append(fullname)
 
     # copy Addons copy tree to destination directory
-    move_extracted_files(extract_dir=srcdir, target_dir=options['prefix'],
-                         files=os.listdir(srcdir))
+    move_extracted_files(
+        extract_dir=srcdir, target_dir=options["prefix"], files=os.listdir(srcdir)
+    )
 
     # collect new files
     file_list = list()
-    for r, d, f in os.walk(options['prefix']):
+    for r, d, f in os.walk(options["prefix"]):
         for filename in f:
             fullname = os.path.join(r, filename)
             if fullname not in old_file_list:
@@ -1320,11 +1459,10 @@ def download_source_code_svn(url, name, outdev, directory=None):
     """
     if not directory:
         directory = os.path.join(os.getcwd, name)
-    classchar = name.split('.', 1)[0]
+    classchar = name.split(".", 1)[0]
     moduleclass = expand_module_class_name(classchar)
-    url = url + '/' + moduleclass + '/' + name
-    if grass.call(['svn', 'checkout',
-                   url, directory], stdout=outdev) != 0:
+    url = url + "/" + moduleclass + "/" + name
+    if grass.call(["svn", "checkout", url, directory], stdout=outdev) != 0:
         grass.fatal(_("GRASS Addons <%s> not found") % name)
     return directory
 
@@ -1348,10 +1486,7 @@ def download_source_code_official_github(url, name, outdev, directory=None):
     """
     if not directory:
         directory = os.path.join(os.getcwd, name)
-    classchar = name.split('.', 1)[0]
-    moduleclass = expand_module_class_name(classchar)
-    if grass.call(['svn', 'export',
-                   url, directory], stdout=outdev) != 0:
+    if grass.call(["svn", "export", url, directory], stdout=outdev) != 0:
         grass.fatal(_("GRASS Addons <%s> not found") % name)
     return directory
 
@@ -1375,8 +1510,7 @@ def move_extracted_files(extract_dir, target_dir, files):
             if os.path.isdir(actual_file):
                 # shutil.copytree() replaced by copy_tree() because
                 # shutil's copytree() fails when subdirectory exists
-                copy_tree(actual_file,
-                          os.path.join(target_dir, file_name))
+                copy_tree(actual_file, os.path.join(target_dir, file_name))
             else:
                 shutil.copy(actual_file, os.path.join(target_dir, file_name))
 
@@ -1393,7 +1527,7 @@ def fix_newlines(directory):
     """
     # skip binary files
     # see https://stackoverflow.com/a/7392391
-    textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
+    textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F})
 
     def is_binary_string(bytes):
         return bool(bytes.translate(None, textchars))
@@ -1401,40 +1535,42 @@ def fix_newlines(directory):
     for root, unused, files in os.walk(directory):
         for name in files:
             filename = os.path.join(root, name)
-            if is_binary_string(open(filename, 'rb').read(1024)):
+            if is_binary_string(open(filename, "rb").read(1024)):
                 continue  # ignore binary files
 
             # read content of text file
-            with open(filename, 'rb') as fd:
+            with open(filename, "rb") as fd:
                 data = fd.read()
 
             # we don't expect there would be CRLF file by
             # purpose if we want to allow CRLF files we would
             # have to whitelite .py etc
-            newdata = data.replace(b'\r\n', b'\n')
+            newdata = data.replace(b"\r\n", b"\n")
             if newdata != data:
-                with open(filename, 'wb') as newfile:
+                with open(filename, "wb") as newfile:
                     newfile.write(newdata)
 
 
 def extract_zip(name, directory, tmpdir):
     """Extract a ZIP file into a directory"""
-    gscript.debug("extract_zip(name={name}, directory={directory},"
-                  " tmpdir={tmpdir})".format(name=name, directory=directory,
-                                             tmpdir=tmpdir), 3)
+    gscript.debug(
+        "extract_zip(name={name}, directory={directory},"
+        " tmpdir={tmpdir})".format(name=name, directory=directory, tmpdir=tmpdir),
+        3,
+    )
     try:
-        zip_file = zipfile.ZipFile(name, mode='r')
+        zip_file = zipfile.ZipFile(name, mode="r")
         file_list = zip_file.namelist()
         # we suppose we can write to parent of the given dir
         # (supposing a tmp dir)
-        extract_dir = os.path.join(tmpdir, 'extract_dir')
+        extract_dir = os.path.join(tmpdir, "extract_dir")
         os.mkdir(extract_dir)
         for subfile in file_list:
-            # this should be safe in Python 2.7.4
+            if "__pycache__" in subfile:
+                continue
             zip_file.extract(subfile, extract_dir)
         files = os.listdir(extract_dir)
-        move_extracted_files(extract_dir=extract_dir,
-                             target_dir=directory, files=files)
+        move_extracted_files(extract_dir=extract_dir, target_dir=directory, files=files)
     except zipfile.BadZipfile as error:
         gscript.fatal(_("ZIP file is unreadable: {0}").format(error))
 
@@ -1442,119 +1578,154 @@ def extract_zip(name, directory, tmpdir):
 # TODO: solve the other related formats
 def extract_tar(name, directory, tmpdir):
     """Extract a TAR or a similar file into a directory"""
-    gscript.debug("extract_tar(name={name}, directory={directory},"
-                  " tmpdir={tmpdir})".format(name=name, directory=directory,
-                                             tmpdir=tmpdir), 3)
+    gscript.debug(
+        "extract_tar(name={name}, directory={directory},"
+        " tmpdir={tmpdir})".format(name=name, directory=directory, tmpdir=tmpdir),
+        3,
+    )
     try:
         import tarfile  # we don't need it anywhere else
+
         tar = tarfile.open(name)
-        extract_dir = os.path.join(tmpdir, 'extract_dir')
+        extract_dir = os.path.join(tmpdir, "extract_dir")
         os.mkdir(extract_dir)
         tar.extractall(path=extract_dir)
         files = os.listdir(extract_dir)
-        move_extracted_files(extract_dir=extract_dir,
-                             target_dir=directory, files=files)
+        move_extracted_files(extract_dir=extract_dir, target_dir=directory, files=files)
     except tarfile.TarError as error:
         gscript.fatal(_("Archive file is unreadable: {0}").format(error))
 
-extract_tar.supported_formats = ['tar.gz', 'gz', 'bz2', 'tar', 'gzip', 'targz']
+
+extract_tar.supported_formats = ["tar.gz", "gz", "bz2", "tar", "gzip", "targz"]
 
 
-def download_source_code(source, url, name, outdev,
-                         directory=None, tmpdir=None, branch=None):
+def download_source_code(
+    source, url, name, outdev, directory=None, tmpdir=None, branch=None
+):
     """Get source code to a local directory for compilation"""
-    gscript.verbose(_("Type of source identified as '{source}'.")
-                    .format(source=source))
-    if source == 'official':
-        gscript.message(_("Fetching <%s> from "
-                          "GRASS GIS Addons repository (be patient)...") % name)
+    gscript.verbose(_("Type of source identified as '{source}'.").format(source=source))
+    if source == "official":
+        gscript.message(
+            _("Fetching <%s> from " "GRASS GIS Addons repository (be patient)...")
+            % name
+        )
+        download_source_code_official_github(url, name, outdev, directory)
+    elif source == "official_fork":
+        gscript.message(
+            _("Fetching <{name}> from " "<{url}> (be patient)...").format(
+                name=name, url=url
+            )
+        )
         download_source_code_official_github(url, name, outdev, directory)
-    elif source == 'svn':
-        gscript.message(_("Fetching <{name}> from "
-                          "<{url}> (be patient)...").format(name=name, url=url))
+    elif source == "svn":
+        gscript.message(
+            _("Fetching <{name}> from " "<{url}> (be patient)...").format(
+                name=name, url=url
+            )
+        )
         download_source_code_svn(url, name, outdev, directory)
-    elif source in ['remote_zip']:  # , 'official'
-        gscript.message(_("Fetching <{name}> from "
-                          "<{url}> (be patient)...").format(name=name, url=url))
+    elif source in ["remote_zip"]:  # , 'official'
+        gscript.message(
+            _("Fetching <{name}> from " "<{url}> (be patient)...").format(
+                name=name, url=url
+            )
+        )
         # we expect that the module.zip file is not by chance in the archive
-        zip_name = os.path.join(tmpdir, 'extension.zip')
+        zip_name = os.path.join(tmpdir, "extension.zip")
         try:
             response = urlopen(url)
         except URLError:
             # Try download add-on from 'master' branch if default "main" fails
-            if branch == "main":
+            if not branch:
                 try:
-                    url = url.replace('main', 'master')
-                    gscript.message(_("Expected default branch not found. "
-                                    "Trying again from <{url}>...")
-                                    .format(url=url))
+                    url = url.replace("main", "master")
+                    gscript.message(
+                        _(
+                            "Expected default branch not found. "
+                            "Trying again from <{url}>..."
+                        ).format(url=url)
+                    )
                     response = urlopen(url)
                 except URLError:
-                    grass.fatal(_("Extension <{name}> not found. Please check "
-                                  "'url' and 'branch' options".format(name=name)))
+                    grass.fatal(
+                        _(
+                            "Extension <{name}> not found. Please check "
+                            "'url' and 'branch' options".format(name=name)
+                        )
+                    )
             else:
                 grass.fatal(_("Extension <%s> not found") % name)
 
-        with open(zip_name, 'wb') as out_file:
+        with open(zip_name, "wb") as out_file:
             shutil.copyfileobj(response, out_file)
         extract_zip(name=zip_name, directory=directory, tmpdir=tmpdir)
         fix_newlines(directory)
-    elif source.startswith('remote_') and \
-            source.split('_')[1] in extract_tar.supported_formats:
+    elif (
+        source.startswith("remote_")
+        and source.split("_")[1] in extract_tar.supported_formats
+    ):
         # we expect that the module.tar.gz file is not by chance in the archive
-        archive_name = os.path.join(tmpdir,
-                                    'extension.' + source.split('_')[1])
+        archive_name = os.path.join(tmpdir, "extension." + source.split("_")[1])
         urlretrieve(url, archive_name)
         extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
         fix_newlines(directory)
-    elif source == 'zip':
+    elif source == "zip":
         extract_zip(name=url, directory=directory, tmpdir=tmpdir)
         fix_newlines(directory)
     elif source in extract_tar.supported_formats:
         extract_tar(name=url, directory=directory, tmpdir=tmpdir)
         fix_newlines(directory)
-    elif source == 'dir':
+    elif source == "dir":
         shutil.copytree(url, directory)
         fix_newlines(directory)
     else:
         # probably programmer error
-        grass.fatal(_("Unknown extension (addon) source type '{0}'."
-                      " Please report this to the grass-user mailing list.")
-                    .format(source))
+        grass.fatal(
+            _(
+                "Unknown extension (addon) source type '{0}'."
+                " Please report this to the grass-user mailing list."
+            ).format(source)
+        )
     assert os.path.isdir(directory)
 
 
 def install_extension_std_platforms(name, source, url, branch):
     """Install extension on standard platforms"""
-    gisbase = os.getenv('GISBASE')
-    source_url = 'https://github.com/OSGeo/grass-addons/tree/master/grass7/'
+    gisbase = os.getenv("GISBASE")
 
     # to hide non-error messages from subprocesses
     if grass.verbosity() <= 2:
-        outdev = open(os.devnull, 'w')
+        outdev = open(os.devnull, "w")
     else:
         outdev = sys.stdout
 
     os.chdir(TMPDIR)  # this is just to not leave something behind
     srcdir = os.path.join(TMPDIR, name)
-    download_source_code(source=source, url=url, name=name,
-                         outdev=outdev, directory=srcdir, tmpdir=TMPDIR,
-                         branch=branch)
+    download_source_code(
+        source=source,
+        url=url,
+        name=name,
+        outdev=outdev,
+        directory=srcdir,
+        tmpdir=TMPDIR,
+        branch=branch,
+    )
     os.chdir(srcdir)
 
-    pgm_not_found_message = _('Module name not found.'
-                              ' Check module Makefile syntax (PGM variable).')
+    pgm_not_found_message = _(
+        "Module name not found." " Check module Makefile syntax (PGM variable)."
+    )
     # collect module names
     module_list = list()
     for r, d, f in os.walk(srcdir):
         for filename in f:
             if filename == "Makefile":
                 # get the module name: PGM = <module name>
-                with open(os.path.join(r, 'Makefile')) as fp:
+                with open(os.path.join(r, "Makefile")) as fp:
                     for line in fp.readlines():
-                        if re.match(r'PGM.*.=|PGM=', line):
+                        if re.match(r"PGM.*.=|PGM=", line):
                             try:
-                                modulename = line.split('=')[1].strip()
+                                modulename = line.split("=")[1].strip()
                                 if modulename:
                                     module_list.append(modulename)
                                 else:
@@ -1567,77 +1738,76 @@ def install_extension_std_platforms(name, source, url, branch):
     # r=root, d=directories, f = files
     for r, d, f in os.walk(srcdir):
         for file in f:
-            if file.endswith('.py'):
+            if file.endswith(".py"):
                 pyfiles.append(os.path.join(r, file))
 
     for filename in pyfiles:
         with fileinput.FileInput(filename, inplace=True) as file:
             for line in file:
-                print(line.replace(
-                    "#!/usr/bin/env python\n",
-                    "#!/usr/bin/env python3\n"
-                ), end='')
+                print(
+                    line.replace("#!/usr/bin/env python\n", "#!/usr/bin/env python3\n"),
+                    end="",
+                )
 
     dirs = {
-        'bin': os.path.join(TMPDIR, name, 'bin'),
-        'docs': os.path.join(TMPDIR, name, 'docs'),
-        'html': os.path.join(TMPDIR, name, 'docs', 'html'),
-        'rest': os.path.join(TMPDIR, name, 'docs', 'rest'),
-        'man': os.path.join(TMPDIR, name, 'docs', 'man'),
-        'script': os.path.join(TMPDIR, name, 'scripts'),
+        "bin": os.path.join(TMPDIR, name, "bin"),
+        "docs": os.path.join(TMPDIR, name, "docs"),
+        "html": os.path.join(TMPDIR, name, "docs", "html"),
+        "rest": os.path.join(TMPDIR, name, "docs", "rest"),
+        "man": os.path.join(TMPDIR, name, "docs", "man"),
+        "script": os.path.join(TMPDIR, name, "scripts"),
         # TODO: handle locales also for addons
         #             'string'  : os.path.join(TMPDIR, name, 'locale'),
-        'string': os.path.join(TMPDIR, name),
-        'etc': os.path.join(TMPDIR, name, 'etc'),
+        "string": os.path.join(TMPDIR, name),
+        "etc": os.path.join(TMPDIR, name, "etc"),
     }
 
     make_cmd = [
-        'make',
-        'MODULE_TOPDIR=%s' % gisbase.replace(' ', r'\ '),
-        'RUN_GISRC=%s' % os.environ['GISRC'],
-        'BIN=%s' % dirs['bin'],
-        'HTMLDIR=%s' % dirs['html'],
-        'RESTDIR=%s' % dirs['rest'],
-        'MANBASEDIR=%s' % dirs['man'],
-        'SCRIPTDIR=%s' % dirs['script'],
-        'STRINGDIR=%s' % dirs['string'],
-        'ETC=%s' % os.path.join(dirs['etc']),
-        'SOURCE_URL=%s' % source_url
+        "make",
+        "MODULE_TOPDIR=%s" % gisbase.replace(" ", r"\ "),
+        "RUN_GISRC=%s" % os.environ["GISRC"],
+        "BIN=%s" % dirs["bin"],
+        "HTMLDIR=%s" % dirs["html"],
+        "RESTDIR=%s" % dirs["rest"],
+        "MANBASEDIR=%s" % dirs["man"],
+        "SCRIPTDIR=%s" % dirs["script"],
+        "STRINGDIR=%s" % dirs["string"],
+        "ETC=%s" % os.path.join(dirs["etc"]),
+        "SOURCE_URL=%s" % url,
     ]
 
     install_cmd = [
-        'make',
-        'MODULE_TOPDIR=%s' % gisbase,
-        'ARCH_DISTDIR=%s' % os.path.join(TMPDIR, name),
-        'INST_DIR=%s' % options['prefix'],
-        'install'
+        "make",
+        "MODULE_TOPDIR=%s" % gisbase,
+        "ARCH_DISTDIR=%s" % os.path.join(TMPDIR, name),
+        "INST_DIR=%s" % options["prefix"],
+        "install",
     ]
 
-    if flags['d']:
+    if flags["d"]:
         grass.message("\n%s\n" % _("To compile run:"))
-        sys.stderr.write(' '.join(make_cmd) + '\n')
+        sys.stderr.write(" ".join(make_cmd) + "\n")
         grass.message("\n%s\n" % _("To install run:"))
-        sys.stderr.write(' '.join(install_cmd) + '\n')
+        sys.stderr.write(" ".join(install_cmd) + "\n")
         return 0, None, None, None
 
     os.chdir(os.path.join(TMPDIR, name))
 
     grass.message(_("Compiling..."))
-    if not os.path.exists(os.path.join(gisbase, 'include',
-                                       'Make', 'Module.make')):
+    if not os.path.exists(os.path.join(gisbase, "include", "Make", "Module.make")):
         grass.fatal(_("Please install GRASS development package"))
 
-    if 0 != grass.call(make_cmd,
-                       stdout=outdev):
-        grass.fatal(_('Compilation failed, sorry.'
-                      ' Please check above error messages.'))
+    if 0 != grass.call(make_cmd, stdout=outdev):
+        grass.fatal(
+            _("Compilation failed, sorry." " Please check above error messages.")
+        )
 
-    if flags['i']:
+    if flags["i"]:
         return 0, None, None, None
 
     # collect old files
     old_file_list = list()
-    for r, d, f in os.walk(options['prefix']):
+    for r, d, f in os.walk(options["prefix"]):
         for filename in f:
             fullname = os.path.join(r, filename)
             old_file_list.append(fullname)
@@ -1647,7 +1817,7 @@ def install_extension_std_platforms(name, source, url, branch):
 
     # collect new files
     file_list = list()
-    for r, d, f in os.walk(options['prefix']):
+    for r, d, f in os.walk(options["prefix"]):
         for filename in f:
             fullname = os.path.join(r, filename)
             if fullname not in old_file_list:
@@ -1658,20 +1828,20 @@ def install_extension_std_platforms(name, source, url, branch):
 
 def remove_extension(force=False):
     """Remove existing extension
-       extension or toolbox with extensions if -t is given)"""
-    if flags['t']:
-        edict = get_toolbox_extensions(options['prefix'], options['extension'])
+    extension or toolbox with extensions if -t is given)"""
+    if flags["t"]:
+        edict = get_toolbox_extensions(options["prefix"], options["extension"])
     else:
         edict = dict()
-        edict[options['extension']] = dict()
+        edict[options["extension"]] = dict()
         # list of modules installed by this extension
-        edict[options['extension']]['mlist'] = list()
+        edict[options["extension"]]["mlist"] = list()
         # list of files installed by this extension
-        edict[options['extension']]['flist'] = list()
+        edict[options["extension"]]["flist"] = list()
 
     # collect modules and files installed by these extensions
     mlist = list()
-    xml_file = os.path.join(options['prefix'], 'extensions.xml')
+    xml_file = os.path.join(options["prefix"], "extensions.xml")
     if os.path.exists(xml_file):
         # read XML file
         tree = None
@@ -1682,27 +1852,27 @@ def remove_extension(force=False):
             write_xml_extensions(xml_file)
 
         if tree is not None:
-            for tnode in tree.findall('task'):
-                ename = tnode.get('name').strip()
+            for tnode in tree.findall("task"):
+                ename = tnode.get("name").strip()
                 if ename in edict:
                     # modules installed by this extension
-                    mnode = tnode.find('modules')
+                    mnode = tnode.find("modules")
                     if mnode:
-                        for fnode in mnode.findall('module'):
+                        for fnode in mnode.findall("module"):
                             mname = fnode.text.strip()
-                            edict[ename]['mlist'].append(mname)
+                            edict[ename]["mlist"].append(mname)
                             mlist.append(mname)
                     # files installed by this extension
-                    bnode = tnode.find('binary')
+                    bnode = tnode.find("binary")
                     if bnode:
-                        for fnode in bnode.findall('file'):
+                        for fnode in bnode.findall("file"):
                             bname = fnode.text.strip()
-                            edict[ename]['flist'].append(bname)
+                            edict[ename]["flist"].append(bname)
     else:
         if force:
             write_xml_extensions(xml_file)
 
-        xml_file = os.path.join(options['prefix'], 'modules.xml')
+        xml_file = os.path.join(options["prefix"], "modules.xml")
         if not os.path.exists(xml_file):
             if force:
                 write_xml_modules(xml_file)
@@ -1719,18 +1889,18 @@ def remove_extension(force=False):
             return []
 
         if tree is not None:
-            for tnode in tree.findall('task'):
-                ename = tnode.get('name').strip()
+            for tnode in tree.findall("task"):
+                ename = tnode.get("name").strip()
                 if ename in edict:
                     # assume extension name == module name
-                    edict[ename]['mlist'].append(ename)
+                    edict[ename]["mlist"].append(ename)
                     mlist.append(ename)
                     # files installed by this extension
-                    bnode = tnode.find('binary')
+                    bnode = tnode.find("binary")
                     if bnode:
-                        for fnode in bnode.findall('file'):
+                        for fnode in bnode.findall("file"):
                             bname = fnode.text.strip()
-                            edict[ename]['flist'].append(bname)
+                            edict[ename]["flist"].append(bname)
 
     if force:
         grass.verbose(_("List of removed files:"))
@@ -1745,17 +1915,25 @@ def remove_extension(force=False):
             remove_extension_xml(mlist, edict)
             for ename in edict:
                 if ename in eremoved:
-                    grass.message(_("Extension <%s> successfully uninstalled.") %
-                                  ename)
+                    grass.message(_("Extension <%s> successfully uninstalled.") % ename)
     else:
-        if flags['t']:
-            grass.warning(_("Toolbox <%s> not removed. "
-                            "Re-run '%s' with '-f' flag to force removal")
-                          % (options['extension'], 'g.extension'))
+        if flags["t"]:
+            grass.warning(
+                _(
+                    "Toolbox <%s> not removed. "
+                    "Re-run '%s' with '-f' flag to force removal"
+                )
+                % (options["extension"], "g.extension")
+            )
         else:
-            grass.warning(_("Extension <%s> not removed. "
-                            "Re-run '%s' with '-f' flag to force removal")
-                          % (options['extension'], 'g.extension'))
+            grass.warning(
+                _(
+                    "Extension <%s> not removed. "
+                    "Re-run '%s' with '-f' flag to force removal"
+                )
+                % (options["extension"], "g.extension")
+            )
+
 
 # remove existing extension(s) (reading XML file)
 
@@ -1767,7 +1945,7 @@ def remove_extension_files(edict, force=False):
     Fallbacks to standard layout of files on prefix path on error.
     """
     # try to read XML metadata file first
-    xml_file = os.path.join(options['prefix'], 'extensions.xml')
+    xml_file = os.path.join(options["prefix"], "extensions.xml")
 
     einstalled = list()
     eremoved = list()
@@ -1775,17 +1953,17 @@ def remove_extension_files(edict, force=False):
     if os.path.exists(xml_file):
         tree = etree_fromfile(xml_file)
         if tree is not None:
-            for task in tree.findall('task'):
-                ename = task.get('name').strip()
+            for task in tree.findall("task"):
+                ename = task.get("name").strip()
                 einstalled.append(ename)
     else:
         tree = None
 
     for name in edict:
         removed = True
-        if len(edict[name]['flist']) > 0:
+        if len(edict[name]["flist"]) > 0:
             err = list()
-            for fpath in edict[name]['flist']:
+            for fpath in edict[name]["flist"]:
                 grass.verbose(fpath)
                 if force:
                     try:
@@ -1818,21 +1996,20 @@ def remove_extension_std(name, force=False):
     Any images for manuals or files installed in etc will not be
     removed
     """
-    for fpath in [os.path.join(options['prefix'], 'bin', name),
-                  os.path.join(options['prefix'], 'scripts', name),
-                  os.path.join(
-                      options['prefix'], 'docs', 'html', name + '.html'),
-                  os.path.join(
-                      options['prefix'], 'docs', 'rest', name + '.txt'),
-                  os.path.join(options['prefix'], 'docs', 'man', 'man1',
-                               name + '.1')]:
+    for fpath in [
+        os.path.join(options["prefix"], "bin", name),
+        os.path.join(options["prefix"], "scripts", name),
+        os.path.join(options["prefix"], "docs", "html", name + ".html"),
+        os.path.join(options["prefix"], "docs", "rest", name + ".txt"),
+        os.path.join(options["prefix"], "docs", "man", "man1", name + ".1"),
+    ]:
         if os.path.isfile(fpath):
             grass.verbose(fpath)
             if force:
                 os.remove(fpath)
 
     # remove module libraries under GRASS_ADDONS/etc/{name}/*
-    libpath = os.path.join(options['prefix'], 'etc', name)
+    libpath = os.path.join(options["prefix"], "etc", name)
     if os.path.isdir(libpath):
         grass.verbose(libpath)
         if force:
@@ -1841,13 +2018,13 @@ def remove_extension_std(name, force=False):
 
 def remove_from_toolbox_xml(name):
     """Update local meta-file when removing existing toolbox"""
-    xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
+    xml_file = os.path.join(options["prefix"], "toolboxes.xml")
     if not os.path.exists(xml_file):
         return
     # read XML file
     tree = etree_fromfile(xml_file)
-    for node in tree.findall('toolbox'):
-        if node.get('code') != name:
+    for node in tree.findall("toolbox"):
+        if node.get("code") != name:
             continue
         tree.remove(node)
 
@@ -1858,32 +2035,33 @@ def remove_extension_xml(mlist, edict):
     """Update local meta-file when removing existing extension"""
     if len(edict) > 1:
         # update also toolboxes metadata
-        remove_from_toolbox_xml(options['extension'])
+        remove_from_toolbox_xml(options["extension"])
 
     # modules
-    xml_file = os.path.join(options['prefix'], 'modules.xml')
+    xml_file = os.path.join(options["prefix"], "modules.xml")
     if os.path.exists(xml_file):
         # read XML file
         tree = etree_fromfile(xml_file)
         for name in mlist:
-            for node in tree.findall('task'):
-                if node.get('name') != name:
+            for node in tree.findall("task"):
+                if node.get("name") != name:
                     continue
                 tree.remove(node)
         write_xml_modules(xml_file, tree)
 
     # extensions
-    xml_file = os.path.join(options['prefix'], 'extensions.xml')
+    xml_file = os.path.join(options["prefix"], "extensions.xml")
     if os.path.exists(xml_file):
         # read XML file
         tree = etree_fromfile(xml_file)
         for name in edict:
-            for node in tree.findall('task'):
-                if node.get('name') != name:
+            for node in tree.findall("task"):
+                if node.get("name") != name:
                     continue
                 tree.remove(node)
         write_xml_extensions(xml_file, tree)
 
+
 # check links in CSS
 
 
@@ -1895,8 +2073,8 @@ def check_style_file(name):
 
     If the files are missing, a warning is issued.
     """
-    dist_file = os.path.join(os.getenv('GISBASE'), 'docs', 'html', name)
-    addons_file = os.path.join(options['prefix'], 'docs', 'html', name)
+    dist_file = os.path.join(os.getenv("GISBASE"), "docs", "html", name)
+    addons_file = os.path.join(options["prefix"], "docs", "html", name)
 
     if os.path.isfile(addons_file):
         return
@@ -1905,11 +2083,13 @@ def check_style_file(name):
         shutil.copyfile(dist_file, addons_file)
     except OSError as error:
         grass.warning(
-            _("Unable to create '{filename}': {error}."
-              " Is the GRASS GIS documentation package installed?"
-              " Installation continues,"
-              " but documentation may not look right.").format(
-              filename=addons_file, error=error))
+            _(
+                "Unable to create '{filename}': {error}."
+                " Is the GRASS GIS documentation package installed?"
+                " Installation continues,"
+                " but documentation may not look right."
+            ).format(filename=addons_file, error=error)
+        )
 
 
 def create_dir(path):
@@ -1930,28 +2110,28 @@ def create_dir(path):
 
 def check_dirs():
     """Ensure that the necessary directories in prefix path exist"""
-    create_dir(os.path.join(options['prefix'], 'bin'))
-    create_dir(os.path.join(options['prefix'], 'docs', 'html'))
-    create_dir(os.path.join(options['prefix'], 'docs', 'rest'))
-    check_style_file('grass_logo.png')
-    check_style_file('grassdocs.css')
-    create_dir(os.path.join(options['prefix'], 'etc'))
-    create_dir(os.path.join(options['prefix'], 'docs', 'man', 'man1'))
-    create_dir(os.path.join(options['prefix'], 'scripts'))
+    create_dir(os.path.join(options["prefix"], "bin"))
+    create_dir(os.path.join(options["prefix"], "docs", "html"))
+    create_dir(os.path.join(options["prefix"], "docs", "rest"))
+    check_style_file("grass_logo.png")
+    check_style_file("grassdocs.css")
+    create_dir(os.path.join(options["prefix"], "etc"))
+    create_dir(os.path.join(options["prefix"], "docs", "man", "man1"))
+    create_dir(os.path.join(options["prefix"], "scripts"))
+
 
 # fix file URI in manual page
 
 
 def update_manual_page(module):
     """Fix manual page for addons which are at different directory
-       than core modules"""
-    if module.split('.', 1)[0] == 'wx':
+    than core modules"""
+    if module.split(".", 1)[0] == "wx":
         return  # skip for GUI modules
 
     grass.verbose(_("Manual page for <%s> updated") % module)
     # read original html file
-    htmlfile = os.path.join(
-        options['prefix'], 'docs', 'html', module + '.html')
+    htmlfile = os.path.join(options["prefix"], "docs", "html", module + ".html")
     try:
         oldfile = open(htmlfile)
         shtml = oldfile.read()
@@ -1968,12 +2148,12 @@ def update_manual_page(module):
         pos.append(match.start(1))
 
     # find URIs
-    pattern = r'''<a href="([^"]+)">([^>]+)</a>'''
+    pattern = r"""<a href="([^"]+)">([^>]+)</a>"""
     addons = get_installed_extensions(force=True)
     for match in re.finditer(pattern, shtml):
-        if match.group(1)[:4] == 'http':
+        if match.group(1)[:4] == "http":
             continue
-        if match.group(1).replace('.html', '') in addons:
+        if match.group(1).replace(".html", "") in addons:
             continue
         pos.append(match.start(1))
 
@@ -1981,15 +2161,15 @@ def update_manual_page(module):
         return  # no match
 
     # replace file URIs
-    prefix = 'file://' + '/'.join([os.getenv('GISBASE'), 'docs', 'html'])
-    ohtml = shtml[:pos[0]]
+    prefix = "file://" + "/".join([os.getenv("GISBASE"), "docs", "html"])
+    ohtml = shtml[: pos[0]]
     for i in range(1, len(pos)):
-        ohtml += prefix + '/' + shtml[pos[i - 1]:pos[i]]
-    ohtml += prefix + '/' + shtml[pos[-1]:]
+        ohtml += prefix + "/" + shtml[pos[i - 1] : pos[i]]
+    ohtml += prefix + "/" + shtml[pos[-1] :]
 
     # write updated html file
     try:
-        newfile = open(htmlfile, 'w')
+        newfile = open(htmlfile, "w")
         newfile.write(ohtml)
     except IOError as error:
         gscript.fatal(_("Unable for write manual page: %s") % error)
@@ -2000,21 +2180,24 @@ def update_manual_page(module):
 def resolve_install_prefix(path, to_system):
     """Determine and check the path for installation"""
     if to_system:
-        path = os.environ['GISBASE']
-    if path == '$GRASS_ADDON_BASE':
-        if not os.getenv('GRASS_ADDON_BASE'):
-            grass.warning(_("GRASS_ADDON_BASE is not defined, "
-                            "installing to ~/.grass%s/addons") % version[0])
-            path = os.path.join(
-                os.environ['HOME'], '.grass%s' % version[0], 'addons')
+        path = os.environ["GISBASE"]
+    if path == "$GRASS_ADDON_BASE":
+        if not os.getenv("GRASS_ADDON_BASE"):
+            grass.warning(
+                _("GRASS_ADDON_BASE is not defined, " "installing to ~/.grass%s/addons")
+                % version[0]
+            )
+            path = os.path.join(os.environ["HOME"], ".grass%s" % version[0], "addons")
         else:
-            path = os.environ['GRASS_ADDON_BASE']
-    if os.path.exists(path) and \
-       not os.access(path, os.W_OK):
-        grass.fatal(_("You don't have permission to install extension to <{0}>."
-                      " Try to run {1} with administrator rights"
-                      " (su or sudo).")
-                    .format(path, 'g.extension'))
+            path = os.environ["GRASS_ADDON_BASE"]
+    if os.path.exists(path) and not os.access(path, os.W_OK):
+        grass.fatal(
+            _(
+                "You don't have permission to install extension to <{0}>."
+                " Try to run {1} with administrator rights"
+                " (su or sudo)."
+            ).format(path, "g.extension")
+        )
     # ensure dir sep at the end for cases where path is used as URL and pasted
     # together with file names
     if not path.endswith(os.path.sep):
@@ -2034,45 +2217,48 @@ def resolve_xmlurl_prefix(url, source=None):
     'https://grass.osgeo.org/addons/'
     """
     gscript.debug("resolve_xmlurl_prefix(url={0}, source={1})".format(url, source))
-    if source == 'official':
+    if source == "official":
         # use pregenerated modules XML file
-        url = 'https://grass.osgeo.org/addons/grass%s/' % version[0]
+        # Define branch to fetch from (latest or current version)
+        version_branch = get_version_branch(version[0])
+
+        url = "https://grass.osgeo.org/addons/{}/".format(version_branch)
     # else try to get extensions XMl from SVN repository (provided URL)
     # the exact action depends on subsequent code (somewhere)
 
-    if not url.endswith('/'):
-        url = url + '/'
+    if not url.endswith("/"):
+        url = url + "/"
     return url
 
 
 KNOWN_HOST_SERVICES_INFO = {
-    'OSGeo Trac': {
-        'domain': 'trac.osgeo.org',
-        'ignored_suffixes': ['format=zip'],
-        'possible_starts': ['', 'https://', 'http://'],
-        'url_start': 'https://',
-        'url_end': '?format=zip',
+    "OSGeo Trac": {
+        "domain": "trac.osgeo.org",
+        "ignored_suffixes": ["format=zip"],
+        "possible_starts": ["", "https://", "http://"],
+        "url_start": "https://",
+        "url_end": "?format=zip",
     },
-    'GitHub': {
-        'domain': 'github.com',
-        'ignored_suffixes': ['.zip', '.tar.gz'],
-        'possible_starts': ['', 'https://', 'http://'],
-        'url_start': 'https://',
-        'url_end': '/archive/{branch}.zip',
+    "GitHub": {
+        "domain": "github.com",
+        "ignored_suffixes": [".zip", ".tar.gz"],
+        "possible_starts": ["", "https://", "http://"],
+        "url_start": "https://",
+        "url_end": "/archive/{branch}.zip",
     },
-    'GitLab': {
-        'domain': 'gitlab.com',
-        'ignored_suffixes': ['.zip', '.tar.gz', '.tar.bz2', '.tar'],
-        'possible_starts': ['', 'https://', 'http://'],
-        'url_start': 'https://',
-        'url_end': '/-/archive/{branch}/{name}-{branch}.zip',
+    "GitLab": {
+        "domain": "gitlab.com",
+        "ignored_suffixes": [".zip", ".tar.gz", ".tar.bz2", ".tar"],
+        "possible_starts": ["", "https://", "http://"],
+        "url_start": "https://",
+        "url_end": "/-/archive/{branch}/{name}-{branch}.zip",
     },
-    'Bitbucket': {
-        'domain': 'bitbucket.org',
-        'ignored_suffixes': ['.zip', '.tar.gz', '.gz', '.bz2'],
-        'possible_starts': ['', 'https://', 'http://'],
-        'url_start': 'https://',
-        'url_end': '/get/{branch}.zip',
+    "Bitbucket": {
+        "domain": "bitbucket.org",
+        "ignored_suffixes": [".zip", ".tar.gz", ".gz", ".bz2"],
+        "possible_starts": ["", "https://", "http://"],
+        "url_start": "https://",
+        "url_end": "/get/{branch}.zip",
     },
 }
 
@@ -2092,40 +2278,46 @@ def resolve_known_host_service(url, name, branch):
     match = None
     actual_start = None
     for key, value in KNOWN_HOST_SERVICES_INFO.items():
-        for start in value['possible_starts']:
-            if url.startswith(start + value['domain']):
+        for start in value["possible_starts"]:
+            if url.startswith(start + value["domain"]):
                 match = value
                 actual_start = start
-                gscript.verbose(_("Identified {0} as known hosting service")
-                                .format(key))
-                for suffix in value['ignored_suffixes']:
+                gscript.verbose(
+                    _("Identified {0} as known hosting service").format(key)
+                )
+                for suffix in value["ignored_suffixes"]:
                     if url.endswith(suffix):
                         gscript.verbose(
-                            _("Not using {service} as known hosting service"
-                              " because the URL ends with '{suffix}'")
-                            .format(service=key, suffix=suffix))
+                            _(
+                                "Not using {service} as known hosting service"
+                                " because the URL ends with '{suffix}'"
+                            ).format(service=key, suffix=suffix)
+                        )
                         return None, None
     if match:
         if not actual_start:
-            actual_start = match['url_start']
+            actual_start = match["url_start"]
         else:
-            actual_start = ''
-        if 'branch' in  match['url_end']:
-            suffix = match['url_end'].format(name=name, branch=branch)
+            actual_start = ""
+        if "branch" in match["url_end"]:
+            suffix = match["url_end"].format(
+                name=name,
+                branch=branch if branch else get_default_branch(url),
+            )
         else:
-            suffix = match['url_end'].format(name=name)
-        url = '{prefix}{base}{suffix}'.format(prefix=actual_start,
-                                              base=url.rstrip('/'),
-                                              suffix=suffix)
-        gscript.verbose(_("Will use the following URL for download: {0}")
-                        .format(url))
-        return 'remote_zip', url
+            suffix = match["url_end"].format(name=name)
+        url = "{prefix}{base}{suffix}".format(
+            prefix=actual_start, base=url.rstrip("/"), suffix=suffix
+        )
+        gscript.verbose(_("Will use the following URL for download: {0}").format(url))
+        return "remote_zip", url
     else:
         return None, None
 
 
 # TODO: add also option to enforce the source type
-def resolve_source_code(url=None, name=None, branch=None):
+# TODO: workaround, https://github.com/OSGeo/grass-addons/issues/528
+def resolve_source_code(url=None, name=None, branch=None, fork=False):
     """Return type and URL or path of the source code
 
     Local paths are not presented as URLs to be usable in standard functions.
@@ -2192,57 +2384,84 @@ def resolve_source_code(url=None, name=None, branch=None):
     >>> resolve_source_code('https://bitbucket.org/joe-user/grass-module') # doctest: +SKIP
     ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
     """
-    if not url and name:
+    # Handle URL for the offical repo
+    if name and (not url or fork):
         module_class = get_module_class_name(name)
         # note: 'trunk' is required to make URL usable for 'svn export' call
-        git_url = 'https://github.com/OSGeo/grass-addons/trunk/' \
-                   'grass{version}/{module_class}/{module_name}' \
-                   .format(version=version[0],
-                           module_class=module_class, module_name=name)
-        # trac_url = 'https://trac.osgeo.org/grass/browser/grass-addons/' \
-        #            'grass{version}/{module_class}/{module_name}?format=zip' \
-        #            .format(version=version[0],
-        #                    module_class=module_class, module_name=name)
-        # return 'official', trac_url
-        return 'official', git_url
+        # and fetches the default branch
+        if not branch:
+            # Fetch from default branch
+            version_branch = get_version_branch(version[0])
+            try:
+                url = url.rstrip("/") if url else GIT_URL
+                urlrequest.urlopen(
+                    "{url}/tree/{version_branch}/src".format(
+                        url=url, version_branch=version_branch
+                    )
+                )
+                svn_reference = "branches/{}".format(version_branch)
+            except URLError:
+                svn_reference = "trunk"
+        else:
+            svn_reference = "branches/{}".format(branch)
+
+        if not url:
+            # Set URL for the given GRASS version
+            git_url = "{GIT_URL}/{svn_reference}/src/{module_class}/{name}".format(
+                GIT_URL=GIT_URL,
+                svn_reference=svn_reference,
+                module_class=module_class,
+                name=name,
+            )
+            return "official", git_url
+        else:
+            # Forks from the official repo should reflect the current structure
+            url = url.rstrip("/")
+            git_url = "{url}/{svn_reference}/src/{module_class}/{name}".format(
+                url=url,
+                svn_reference=svn_reference,
+                module_class=module_class,
+                name=name,
+            )
+            return "official_fork", git_url
 
     # Check if URL can be found
     # Catch corner case if local URL is given starting with file://
-    url = url[6:] if url.startswith('file://') else url
+    url = url[6:] if url.startswith("file://") else url
     if not os.path.exists(url):
         url_validated = False
-        if url.startswith('http'):
+        if url.startswith("http"):
             try:
                 open_url = urlopen(url)
                 open_url.close()
                 url_validated = True
-            except:
+            except URLError:
                 pass
         else:
             try:
-                open_url = urlopen('http://' + url)
+                open_url = urlopen("http://" + url)
                 open_url.close()
                 url_validated = True
-            except:
+            except URLError:
                 pass
             try:
-                open_url = urlopen('https://' + url)
+                open_url = urlopen("https://" + url)
                 open_url.close()
                 url_validated = True
-            except:
+            except URLError:
                 pass
 
         if not url_validated:
-            grass.fatal(_('Cannot open URL: {}'.format(url)))
+            grass.fatal(_("Cannot open URL: {}".format(url)))
 
     # Handle local URLs
     if os.path.isdir(url):
-        return 'dir', os.path.abspath(url)
+        return "dir", os.path.abspath(url)
     elif os.path.exists(url):
-        if url.endswith('.zip'):
-            return 'zip', os.path.abspath(url)
+        if url.endswith(".zip"):
+            return "zip", os.path.abspath(url)
         for suffix in extract_tar.supported_formats:
-            if url.endswith('.' + suffix):
+            if url.endswith("." + suffix):
                 return suffix, os.path.abspath(url)
     # Handle remote URLs
     else:
@@ -2252,13 +2471,13 @@ def resolve_source_code(url=None, name=None, branch=None):
         # we allow URL to end with =zip or ?zip and not only .zip
         # unfortunately format=zip&version=89612 would require something else
         # special option to force the source type would solve it
-        if url.endswith('zip'):
-            return 'remote_zip', url
+        if url.endswith("zip"):
+            return "remote_zip", url
         for suffix in extract_tar.supported_formats:
             if url.endswith(suffix):
-                return 'remote_' + suffix, url
+                return "remote_" + suffix, url
         # fallback to the classic behavior
-        return 'svn', url
+        return "svn", url
 
 
 def get_addons_paths(gg_addons_base_dir):
@@ -2266,94 +2485,103 @@ def get_addons_paths(gg_addons_base_dir):
     in the $GRASS_ADDON_BASE dir. The file serves as a list of all addons,
     and their paths (mkhmtl.py tool)
     """
-    get_addons_paths.json_file = 'addons_paths.json'
-
-    url = 'https://api.github.com/repos/OSGeo/grass-addons/git/trees/'\
-        'main?recursive=1'
+    get_addons_paths.json_file = "addons_paths.json"
+    # Define branch to fetch from (latest or current version)
+    addons_branch = get_version_branch(version[0])
+    url = "https://api.github.com/repos/OSGeo/grass-addons/git/trees/{}?recursive=1".format(
+        addons_branch
+    )
 
     response = download_addons_paths_file(
-        url=url, response_format='application/json',
+        url=url,
+        response_format="application/json",
     )
     if response:
         addons_paths = json.loads(gscript.decode(response.read()))
-        with open(os.path.join(gg_addons_base_dir, get_addons_paths.json_file),
-                  'w') as f:
+        with open(
+            os.path.join(gg_addons_base_dir, get_addons_paths.json_file), "w"
+        ) as f:
             json.dump(addons_paths, f)
 
 
 def main():
     # check dependencies
-    if not flags['a'] and sys.platform != "win32":
+    if not flags["a"] and sys.platform != "win32":
         check_progs()
 
-    original_url = options['url']
-    branch = options['branch']
+    original_url = options["url"]
+    branch = options["branch"]
 
     # manage proxies
     global PROXIES
-    if options['proxy']:
+    if options["proxy"]:
         PROXIES = {}
-        for ptype, purl in (p.split('=') for p in options['proxy'].split(',')):
+        for ptype, purl in (p.split("=") for p in options["proxy"].split(",")):
             PROXIES[ptype] = purl
         proxy = urlrequest.ProxyHandler(PROXIES)
         opener = urlrequest.build_opener(proxy)
         urlrequest.install_opener(opener)
 
     # define path
-    options['prefix'] = resolve_install_prefix(path=options['prefix'],
-                                               to_system=flags['s'])
+    options["prefix"] = resolve_install_prefix(
+        path=options["prefix"], to_system=flags["s"]
+    )
 
-    if flags['j']:
-        get_addons_paths(gg_addons_base_dir=options['prefix'])
+    if flags["j"]:
+        get_addons_paths(gg_addons_base_dir=options["prefix"])
         return 0
 
     # list available extensions
-    if flags['l'] or flags['c'] or (flags['g'] and not flags['a']):
+    if flags["l"] or flags["c"] or (flags["g"] and not flags["a"]):
         # using dummy extension, we don't need any extension URL now,
         # but will work only as long as the function does not check
         # if the URL is actually valid or something
-        source, url = resolve_source_code(name='dummy',
-                                          url=original_url,
-                                          branch=branch)
+        source, url = resolve_source_code(
+            name="dummy", url=original_url, branch=branch, fork=flags["o"]
+        )
         xmlurl = resolve_xmlurl_prefix(original_url, source=source)
         list_available_extensions(xmlurl)
         return 0
-    elif flags['a']:
-        list_installed_extensions(toolboxes=flags['t'])
+    elif flags["a"]:
+        list_installed_extensions(toolboxes=flags["t"])
         return 0
 
-    if flags['d'] or flags['i']:
-        flag = 'd' if flags['d'] else 'i'
-        if options['operation'] != 'add':
-            grass.warning(_("Flag '{}' is relevant only to"
-                            " 'operation=add'. Ignoring this flag.").format(
-                                flag))
+    if flags["d"] or flags["i"]:
+        flag = "d" if flags["d"] else "i"
+        if options["operation"] != "add":
+            grass.warning(
+                _(
+                    "Flag '{}' is relevant only to"
+                    " 'operation=add'. Ignoring this flag."
+                ).format(flag)
+            )
         else:
             global REMOVE_TMPDIR
             REMOVE_TMPDIR = False
 
-    if options['operation'] == 'add':
+    if options["operation"] == "add":
         check_dirs()
-        if original_url == '':
+        if original_url == "" or flags["o"]:
             """
             Query GitHub API only if extension will be downloaded
             from official GRASS GIS addon repository
             """
-            get_addons_paths(gg_addons_base_dir=options['prefix'])
-        source, url = resolve_source_code(name=options['extension'],
-                                          url=original_url,
-                                          branch=branch)
+            get_addons_paths(gg_addons_base_dir=options["prefix"])
+        source, url = resolve_source_code(
+            name=options["extension"], url=original_url, branch=branch, fork=flags["o"]
+        )
         xmlurl = resolve_xmlurl_prefix(original_url, source=source)
         install_extension(source=source, url=url, xmlurl=xmlurl, branch=branch)
     else:  # remove
-        remove_extension(force=flags['f'])
+        remove_extension(force=flags["f"])
 
     return 0
 
 
 if __name__ == "__main__":
-    if len(sys.argv) == 2 and sys.argv[1] == '--doctest':
+    if len(sys.argv) == 2 and sys.argv[1] == "--doctest":
         import doctest
+
         sys.exit(doctest.testmod().failed)
     options, flags = grass.parser()
     global TMPDIR
@@ -2361,7 +2589,8 @@ if __name__ == "__main__":
     atexit.register(cleanup)
 
     grass_version = grass.version()
-    version = grass_version['version'].split('.')
-    build_platform = grass_version['build_platform'].split('-', 1)[0]
+    version = grass_version["version"].split(".")
+
+    build_platform = grass_version["build_platform"].split("-", 1)[0]
 
     sys.exit(main())


=====================================
temporal/t.rast.accumulate/t.rast.accumulate.py
=====================================
@@ -469,7 +469,7 @@ def main():
             print(accmod)
             accmod.run()
 
-            if accmod.popen.returncode != 0:
+            if accmod.returncode != 0:
                 dbif.close()
                 grass.fatal(_("Error running r.series.accumulate"))
 


=====================================
temporal/t.rast.neighbors/t.rast.neighbors.py
=====================================
@@ -194,8 +194,11 @@ def main():
     # Check return status of all finished modules
     error = 0
     for proc in proc_list:
-        if proc.popen.returncode != 0:
-            grass.error(_("Error running module: %\n    stderr: %s") %(proc.get_bash(), proc.outputs.stderr))
+        if proc.returncode != 0:
+            grass.error(
+                _("Error running module: %\n    stderr: %s")
+                % (proc.get_bash(), proc.outputs.stderr)
+            )
             error += 1
 
     if error > 0:



View it on GitLab: https://salsa.debian.org/debian-gis-team/grass/-/commit/ba8139edb20123870e23cf111ec991c788cc41d6

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/grass/-/commit/ba8139edb20123870e23cf111ec991c788cc41d6
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210810/a332a261/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list