浏览代码

grass.grassdb: New Python subpackage for working with locations and mapsets (#837)

New package under grass should be a place for all functionality related to manipulating locations and mapsets. The need for this comes specifically from sharing the code between init/grass.py and wxGUI data catalog (and startup window).

This should be the place where things like "locked mapset" should be defined for Python code (whatever the implementation is, the function to check that will be here). It also aspires to be a lightweight Python API equivalent of wxGUI data catalog. With that functions related to raster maps, vector maps, and other actual data manipulations can be included (i.e., manipulations, not computations).

Besides existing functions from from init/grass.py, startup/utils.py, and datacatalog/tree.py, this includes new functions for mapset checking from (currently) unmerged #767 which needs to know ahead of time if mapset is usable for running in it.

Currently, only standalone functions are used (i.e., no classes like trees, etc.). This can change if needed.

grassdb stands for GRASS GIS Spatial Database.

A check for PERMANENT added to rename mapset.

Note the lack of treatment of SKIP_MAPSET_OWN_CHK build flag in the pure Python implementation of ownership check.

The library currently complies with Black and most of Pylint.
Vaclav Petras 4 年之前
父节点
当前提交
0538cf8d98

+ 2 - 36
gui/wxpython/datacatalog/tree.py

@@ -48,6 +48,7 @@ from grass.pydispatch.signal import Signal
 
 import grass.script as gscript
 from grass.script import gisenv
+from grass.grassdb.data import map_exists
 from grass.exceptions import CalledModuleError
 
 
@@ -157,41 +158,6 @@ def getLocationTree(gisdbase, location, queue, mapsets=None):
     gscript.try_remove(tmp_gisrc_file)
 
 
-def map_exists(name, element, env, mapset=None):
-    """Check is map is present in the mapset given in the environment
-
-    :param name: name of the map
-    :param element: data type ('raster', 'raster_3d', and 'vector')
-    :param env environment created by function gscript.create_environment
-    """
-    if not mapset:
-        mapset = gscript.run_command('g.mapset', flags='p', env=env).strip()
-    # change type to element used by find file
-    if element == 'raster':
-        element = 'cell'
-    elif element == 'raster_3d':
-        element = 'grid3'
-    # g.findfile returns non-zero when file was not found
-    # se we ignore return code and just focus on stdout
-    process = gscript.start_command(
-        'g.findfile',
-        flags='n',
-        element=element,
-        file=name,
-        mapset=mapset,
-        stdout=gscript.PIPE,
-        stderr=gscript.PIPE,
-        env=env)
-    output, errors = process.communicate()
-    info = gscript.parse_key_val(output, sep='=')
-    # file is the key questioned in grass.script.core find_file()
-    # return code should be equivalent to checking the output
-    if info['file']:
-        return True
-    else:
-        return False
-
-
 class NameEntryDialog(TextEntryDialog):
 
     def __init__(self, element, mapset, env, **kwargs):
@@ -206,7 +172,7 @@ class NameEntryDialog(TextEntryDialog):
         new = self.GetValue()
         if not new:
             return
-        if map_exists(new, self._element, self._env, self._mapset):
+        if map_exists(new, self._element, env=self._env, mapset=self._mapset):
             dlg = wx.MessageDialog(
                 self,
                 message=_(

+ 3 - 1
gui/wxpython/gis_set.py

@@ -32,10 +32,12 @@ from core import globalvar
 import wx
 import wx.lib.mixins.listctrl as listmix
 
+from grass.grassdb.checks import get_lockfile_if_present
+
 from core.gcmd import GError, RunCommand
 from core.utils import GetListOfLocations, GetListOfMapsets
 from startup.utils import (
-    get_lockfile_if_present, get_possible_database_path,
+    get_possible_database_path,
     create_database_directory)
 from startup.guiutils import (SetSessionMapset,
                               create_mapset_interactively,

+ 1 - 1
gui/wxpython/location_wizard/wizard.py

@@ -58,8 +58,8 @@ from gui_core.widgets import GenericMultiValidator
 from gui_core.wrap import SpinCtrl, SearchCtrl, StaticText, \
     TextCtrl, Button, CheckBox, StaticBox, NewId, ListCtrl, HyperlinkCtrl
 from location_wizard.dialogs import SelectTransformDialog
-from startup.utils import location_exists
 
+from grass.grassdb.checks import location_exists
 from grass.script import decode
 from grass.script import core as grass
 from grass.exceptions import OpenError

+ 8 - 3
gui/wxpython/startup/guiutils.py

@@ -21,15 +21,20 @@ import wx
 
 import grass.script as gs
 from grass.script import gisenv
+from grass.grassdb.checks import mapset_exists, location_exists
+from grass.grassdb.create import create_mapset, get_default_mapset_name
+from grass.grassdb.manage import (
+    delete_mapset,
+    delete_location,
+    rename_mapset,
+    rename_location,
+)
 
 from core import globalvar
 from core.gcmd import GError, GMessage, DecodeString, RunCommand
 from gui_core.dialogs import TextEntryDialog
 from location_wizard.dialogs import RegionDef
 from gui_core.widgets import GenericMultiValidator
-from startup.utils import (create_mapset, delete_mapset, delete_location,
-                           rename_mapset, rename_location, mapset_exists,
-                           location_exists, get_default_mapset_name)
 
 
 def SetSessionMapset(database, location, mapset):

+ 0 - 87
gui/wxpython/startup/utils.py

@@ -17,7 +17,6 @@ solve the errors etc. in a general manner).
 
 
 import os
-import shutil
 import tempfile
 import getpass
 import sys
@@ -88,89 +87,3 @@ def create_database_directory():
         pass
 
     return None
-
-
-def get_lockfile_if_present(database, location, mapset):
-    """Return path to lock if present, None otherwise
-
-    Returns the path as a string or None if nothing was found, so the
-    return value can be used to test if the lock is present.
-    """
-    lock_name = ".gislock"
-    lockfile = os.path.join(database, location, mapset, lock_name)
-    if os.path.isfile(lockfile):
-        return lockfile
-    else:
-        return None
-
-
-def create_mapset(database, location, mapset):
-    """Creates a mapset in a specified location"""
-    location_path = os.path.join(database, location)
-    mapset_path = os.path.join(location_path, mapset)
-    # create an empty directory
-    os.mkdir(mapset_path)
-    # copy DEFAULT_WIND file and its permissions from PERMANENT
-    # to WIND in the new mapset
-    region_path1 = os.path.join(location_path, "PERMANENT", "DEFAULT_WIND")
-    region_path2 = os.path.join(location_path, mapset, "WIND")
-    shutil.copy(region_path1, region_path2)
-    # set permissions to u+rw,go+r (disabled; why?)
-    # os.chmod(os.path.join(database,location,mapset,'WIND'), 0644)
-
-
-def delete_mapset(database, location, mapset):
-    """Deletes a specified mapset"""
-    if mapset == "PERMANENT":
-        # TODO: translatable or not?
-        raise ValueError(
-            "Mapset PERMANENT cannot be deleted" " (whole location can be)"
-        )
-    shutil.rmtree(os.path.join(database, location, mapset))
-
-
-def delete_location(database, location):
-    """Deletes a specified location"""
-    shutil.rmtree(os.path.join(database, location))
-
-
-def rename_mapset(database, location, old_name, new_name):
-    """Rename mapset from *old_name* to *new_name*"""
-    location_path = os.path.join(database, location)
-    os.rename(
-        os.path.join(location_path, old_name), os.path.join(location_path, new_name)
-    )
-
-
-def rename_location(database, old_name, new_name):
-    """Rename location from *old_name* to *new_name*"""
-    os.rename(os.path.join(database, old_name), os.path.join(database, new_name))
-
-
-def mapset_exists(database, location, mapset):
-    """Returns True whether mapset path exists."""
-    location_path = os.path.join(database, location)
-    mapset_path = os.path.join(location_path, mapset)
-    if os.path.exists(mapset_path):
-        return True
-    return False
-
-
-def location_exists(database, location):
-    """Returns True whether location path exists."""
-    location_path = os.path.join(database, location)
-    if os.path.exists(location_path):
-        return True
-    return False
-
-
-def get_default_mapset_name():
-    """Returns default name for mapset."""
-    try:
-        defaultName = getpass.getuser()
-        defaultName.encode("ascii")
-    except UnicodeEncodeError:
-        # raise error if not ascii (not valid mapset name)
-        defaultName = "user"
-
-    return defaultName

+ 16 - 151
lib/init/grass.py

@@ -859,156 +859,6 @@ def create_location(gisdbase, location, geostring):
         fatal(err.value.strip('"').strip("'").replace('\\n', os.linesep))
 
 
-# TODO: distinguish between valid for getting maps and usable as current
-# https://lists.osgeo.org/pipermail/grass-dev/2016-September/082317.html
-# interface created according to the current usage
-def is_mapset_valid(full_mapset):
-    """Return True if GRASS Mapset is valid"""
-    # WIND is created from DEFAULT_WIND by `g.region -d` and functions
-    # or modules which create a new mapset. Most modules will fail if
-    # WIND doesn't exist (assuming that neither GRASS_REGION nor
-    # WIND_OVERRIDE environmental variables are set).
-    return os.access(os.path.join(full_mapset, "WIND"), os.R_OK)
-
-
-def is_location_valid(gisdbase, location):
-    """Return True if GRASS Location is valid
-
-    :param gisdbase: Path to GRASS GIS database directory
-    :param location: name of a Location
-    """
-    # DEFAULT_WIND file should not be required until you do something
-    # that actually uses them. The check is just a heuristic; a directory
-    # containing a PERMANENT/DEFAULT_WIND file is probably a GRASS
-    # location, while a directory lacking it probably isn't.
-    return os.access(os.path.join(gisdbase, location,
-                                  "PERMANENT", "DEFAULT_WIND"), os.F_OK)
-
-
-# basically checking location, possibly split into two functions
-# (mapset one can call location one)
-def get_mapset_invalid_reason(gisdbase, location, mapset):
-    """Returns a message describing what is wrong with the Mapset
-
-    The goal is to provide the most suitable error message
-    (rather than to do a quick check).
-
-    :param gisdbase: Path to GRASS GIS database directory
-    :param location: name of a Location
-    :param mapset: name of a Mapset
-    :returns: translated message
-    """
-    full_location = os.path.join(gisdbase, location)
-    full_mapset = os.path.join(full_location, mapset)
-    # first checking the location validity
-    # perhaps a special set of checks with different messages mentioning mapset
-    # will be needed instead of the same set of messages used for location
-    location_msg = get_location_invalid_reason(
-        gisdbase, location, none_for_no_reason=True
-    )
-    if location_msg:
-        return location_msg
-    # if location is valid, check mapset
-    elif mapset not in os.listdir(full_location):
-        return _("Mapset <{mapset}> doesn't exist in GRASS Location <{loc}>. "
-                 "A new mapset can be created by '-c' switch.").format(
-                     mapset=mapset, loc=location)
-    elif not os.path.isdir(full_mapset):
-        return _("<%s> is not a GRASS Mapset"
-                 " because it is not a directory") % mapset
-    elif not os.path.isfile(os.path.join(full_mapset, 'WIND')):
-        return _("<%s> is not a valid GRASS Mapset"
-                 " because it does not have a WIND file") % mapset
-    # based on the is_mapset_valid() function
-    elif not os.access(os.path.join(full_mapset, "WIND"), os.R_OK):
-        return _("<%s> is not a valid GRASS Mapset"
-                 " because its WIND file is not readable") % mapset
-    else:
-        return _("Mapset <{mapset}> or Location <{location}> is"
-                 " invalid for an unknown reason").format(
-                     mapset=mapset, location=location)
-
-
-def get_location_invalid_reason(gisdbase, location, none_for_no_reason=False):
-    """Returns a message describing what is wrong with the Location
-
-    The goal is to provide the most suitable error message
-    (rather than to do a quick check).
-
-    By default, when no reason is found, a message about unknown reason is
-    returned. This applies also to the case when this function is called on
-    a valid location (e.g. as a part of larger investigation).
-    ``none_for_no_reason=True`` allows the function to be used as part of other
-    diagnostic. When this function fails to find reason for invalidity, other
-    the caller can continue the investigation in their context.
-
-    :param gisdbase: Path to GRASS GIS database directory
-    :param location: name of a Location
-    :param none_for_no_reason: When True, return None when reason is unknown
-    :returns: translated message or None
-    """
-    full_location = os.path.join(gisdbase, location)
-    full_permanent = os.path.join(full_location, 'PERMANENT')
-
-    # directory
-    if not os.path.exists(full_location):
-        return _("Location <%s> doesn't exist") % full_location
-    # permament mapset
-    elif 'PERMANENT' not in os.listdir(full_location):
-        return _("<%s> is not a valid GRASS Location"
-                 " because PERMANENT Mapset is missing") % full_location
-    elif not os.path.isdir(full_permanent):
-        return _("<%s> is not a valid GRASS Location"
-                 " because PERMANENT is not a directory") % full_location
-    # partially based on the is_location_valid() function
-    elif not os.path.isfile(os.path.join(full_permanent,
-                                         'DEFAULT_WIND')):
-        return _("<%s> is not a valid GRASS Location"
-                 " because PERMANENT Mapset does not have a DEFAULT_WIND file"
-                 " (default computational region)") % full_location
-    # no reason for invalidity found (might be valid)
-    if none_for_no_reason:
-        return None
-    else:
-        return _("Location <{location}> is"
-                 " invalid for an unknown reason").format(location=full_location)
-
-
-def dir_contains_location(path):
-    """Return True if directory *path* contains a valid location"""
-    if not os.path.isdir(path):
-        return False
-    for name in os.listdir(path):
-        if os.path.isdir(os.path.join(path, name)):
-            if is_location_valid(path, name):
-                return True
-    return False
-
-
-def get_location_invalid_suggestion(gisdbase, location_name):
-    """Return suggestion what to do when specified location is not valid
-
-    It gives suggestion when:
-     * A mapset was specified instead of a location.
-     * A GRASS database was specified instead of a location.
-    """
-    full_path = os.path.join(gisdbase, location_name)
-    # a common error is to use mapset instead of location,
-    # if that's the case, include that info into the message
-    if is_mapset_valid(full_path):
-        return _(
-            "<{loc}> looks like a mapset, not a location."
-            " Did you mean just <{one_dir_up}>?").format(
-                loc=location_name, one_dir_up=gisdbase)
-    # confusion about what is database and what is location
-    elif dir_contains_location(full_path):
-        return _(
-            "It looks like <{loc}> contains locations."
-            " Did you mean to specify one of them?").format(
-                loc=location_name)
-    return None
-
-
 def can_create_location(gisdbase, location):
     """Checks if location can be created"""
     path = os.path.join(gisdbase, location)
@@ -1027,6 +877,8 @@ def cannot_create_location_reason(gisdbase, location):
     :param location: name of a Location
     :returns: translated message
     """
+    from grass.grassdb.checks import is_location_valid
+
     path = os.path.join(gisdbase, location)
     if is_location_valid(gisdbase, location):
         return _("Unable to create new location because"
@@ -1054,6 +906,14 @@ def set_mapset(gisrc, arg=None, geofile=None, create_new=False,
 
     tmp_location requires tmpdir (which is used as gisdbase)
     """
+    from grass.grassdb.checks import (
+        is_mapset_valid,
+        is_location_valid,
+        get_mapset_invalid_reason,
+        get_location_invalid_reason,
+        get_location_invalid_suggestion,
+        mapset_exists,
+    )
     # TODO: arg param seems to be always the mapset parameter (or a dash
     # in a distant past), refactor
     l = arg
@@ -1116,7 +976,12 @@ def set_mapset(gisrc, arg=None, geofile=None, create_new=False,
             if not create_new:
                 # 'path' is not a valid mapset and user does not
                 # want to create anything new
-                fatal(get_mapset_invalid_reason(gisdbase, location_name, mapset))
+                reason = get_mapset_invalid_reason(gisdbase, location_name, mapset)
+                if not mapset_exists(gisdbase, location_name, mapset):
+                    suggestion = _("A new mapset can be created using '-c' flag.")
+                else:
+                    suggestion = _("Maybe you meant a different directory.")
+                fatal("{reason}\n{suggestion}".format(**locals()))
             else:
                 # 'path' is not valid and the user wants to create
                 # mapset on the fly

+ 1 - 1
lib/python/Makefile

@@ -5,7 +5,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make
 
 PYDIR = $(ETC)/python/grass
 
-SUBDIRS = exceptions script ctypes temporal pygrass pydispatch imaging gunittest bandref
+SUBDIRS = exceptions script ctypes grassdb temporal pygrass pydispatch imaging gunittest bandref
 
 default: $(PYDIR)/__init__.py
 	$(MAKE) subdirs

+ 19 - 0
lib/python/grassdb/Makefile

@@ -0,0 +1,19 @@
+MODULE_TOPDIR = ../../..
+
+include $(MODULE_TOPDIR)/include/Make/Other.make
+include $(MODULE_TOPDIR)/include/Make/Python.make
+
+DSTDIR = $(ETC)/python/grass/grassdb
+
+MODULES = checks create data manage
+
+PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
+PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
+
+default: $(PYFILES) $(PYCFILES)
+
+$(DSTDIR):
+	$(MKDIR) $@
+
+$(DSTDIR)/%: % | $(DSTDIR)
+	$(INSTALL_DATA) $< $@

+ 0 - 0
lib/python/grassdb/__init__.py


+ 255 - 0
lib/python/grassdb/checks.py

@@ -0,0 +1,255 @@
+"""
+Checking objects in a GRASS GIS Spatial Database
+
+(C) 2020 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.
+
+.. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
+"""
+
+
+import os
+
+
+def mapset_exists(database, location, mapset):
+    """Returns True whether mapset path exists."""
+    location_path = os.path.join(database, location)
+    mapset_path = os.path.join(location_path, mapset)
+    if os.path.exists(mapset_path):
+        return True
+    return False
+
+
+def location_exists(database, location):
+    """Returns True whether location path exists."""
+    location_path = os.path.join(database, location)
+    if os.path.exists(location_path):
+        return True
+    return False
+
+
+# TODO: distinguish between valid for getting maps and usable as current
+# https://lists.osgeo.org/pipermail/grass-dev/2016-September/082317.html
+# interface created according to the current usage
+def is_mapset_valid(mapset_path):
+    """Return True if GRASS Mapset is valid"""
+    # WIND is created from DEFAULT_WIND by `g.region -d` and functions
+    # or modules which create a new mapset. Most modules will fail if
+    # WIND doesn't exist (assuming that neither GRASS_REGION nor
+    # WIND_OVERRIDE environmental variables are set).
+    return os.access(os.path.join(mapset_path, "WIND"), os.R_OK)
+
+
+def is_location_valid(database, location):
+    """Return True if GRASS Location is valid
+
+    :param database: Path to GRASS GIS database directory
+    :param location: name of a Location
+    """
+    # DEFAULT_WIND file should not be required until you do something
+    # that actually uses them. The check is just a heuristic; a directory
+    # containing a PERMANENT/DEFAULT_WIND file is probably a GRASS
+    # location, while a directory lacking it probably isn't.
+    return os.access(
+        os.path.join(database, location, "PERMANENT", "DEFAULT_WIND"), os.F_OK
+    )
+
+
+def is_mapset_users(mapset_path):
+    """Check if the mapset belongs to the user"""
+    # Note that this does account for libgis built with SKIP_MAPSET_OWN_CHK
+    # which disables the ownerships check, i.e., even if it was build with the
+    # skip, it still needs the env variable.
+    if os.environ.get("GRASS_SKIP_MAPSET_OWNER_CHECK", None):
+        # Mapset just needs to be accessible for writing.
+        return os.access(mapset_path, os.W_OK)
+    # Mapset needs to be owned by user.
+    stat_info = os.stat(mapset_path)
+    mapset_uid = stat_info.st_uid
+    return mapset_uid == os.getuid()
+
+
+def is_mapset_locked(mapset_path):
+    """Check if the mapset is locked"""
+    lock_name = ".gislock"
+    lockfile = os.path.join(mapset_path, lock_name)
+    return os.path.exists(lockfile)
+
+
+def get_lockfile_if_present(database, location, mapset):
+    """Return path to lock if present, None otherwise
+
+    Returns the path as a string or None if nothing was found, so the
+    return value can be used to test if the lock is present.
+    """
+    lock_name = ".gislock"
+    lockfile = os.path.join(database, location, mapset, lock_name)
+    if os.path.isfile(lockfile):
+        return lockfile
+    return None
+
+
+def can_start_in_mapset(mapset_path, ignore_lock=False):
+    """Check if a mapset from a gisrc file is usable for new session"""
+    if not is_mapset_valid(mapset_path):
+        return False
+    if not is_mapset_users(mapset_path):
+        return False
+    if not ignore_lock and is_mapset_locked(mapset_path):
+        return False
+    return True
+
+
+def dir_contains_location(path):
+    """Return True if directory *path* contains a valid location"""
+    if not os.path.isdir(path):
+        return False
+    for name in os.listdir(path):
+        if os.path.isdir(os.path.join(path, name)):
+            if is_location_valid(path, name):
+                return True
+    return False
+
+
+# basically checking location, possibly split into two functions
+# (mapset one can call location one)
+def get_mapset_invalid_reason(database, location, mapset, none_for_no_reason=False):
+    """Returns a message describing what is wrong with the Mapset
+
+    The goal is to provide the most suitable error message
+    (rather than to do a quick check).
+
+    :param database: Path to GRASS GIS database directory
+    :param location: name of a Location
+    :param mapset: name of a Mapset
+    :returns: translated message
+    """
+    # Since we are trying to get the one most likely message, we need all
+    # those return statements here.
+    # pylint: disable=too-many-return-statements
+    location_path = os.path.join(database, location)
+    mapset_path = os.path.join(location_path, mapset)
+    # first checking the location validity
+    # perhaps a special set of checks with different messages mentioning mapset
+    # will be needed instead of the same set of messages used for location
+    location_msg = get_location_invalid_reason(
+        database, location, none_for_no_reason=True
+    )
+    if location_msg:
+        return location_msg
+    # if location is valid, check mapset
+    if mapset not in os.listdir(location_path):
+        # TODO: remove the grass.py specific wording
+        return _(
+            "Mapset <{mapset}> doesn't exist in GRASS Location <{location}>"
+        ).format(mapset=mapset, location=location)
+    if not os.path.isdir(mapset_path):
+        return _("<%s> is not a GRASS Mapset because it is not a directory") % mapset
+    if not os.path.isfile(os.path.join(mapset_path, "WIND")):
+        return (
+            _(
+                "<%s> is not a valid GRASS Mapset"
+                " because it does not have a WIND file"
+            )
+            % mapset
+        )
+    # based on the is_mapset_valid() function
+    if not os.access(os.path.join(mapset_path, "WIND"), os.R_OK):
+        return (
+            _(
+                "<%s> is not a valid GRASS Mapset"
+                " because its WIND file is not readable"
+            )
+            % mapset
+        )
+    # no reason for invalidity found (might be valid)
+    if none_for_no_reason:
+        return None
+    return _(
+        "Mapset <{mapset}> or Location <{location}> is invalid for an unknown reason"
+    ).format(mapset=mapset, location=location)
+
+
+def get_location_invalid_reason(database, location, none_for_no_reason=False):
+    """Returns a message describing what is wrong with the Location
+
+    The goal is to provide the most suitable error message
+    (rather than to do a quick check).
+
+    By default, when no reason is found, a message about unknown reason is
+    returned. This applies also to the case when this function is called on
+    a valid location (e.g. as a part of larger investigation).
+    ``none_for_no_reason=True`` allows the function to be used as part of other
+    diagnostic. When this function fails to find reason for invalidity, other
+    the caller can continue the investigation in their context.
+
+    :param database: Path to GRASS GIS database directory
+    :param location: name of a Location
+    :param none_for_no_reason: When True, return None when reason is unknown
+    :returns: translated message or None
+    """
+    location_path = os.path.join(database, location)
+    permanent_path = os.path.join(location_path, "PERMANENT")
+
+    # directory
+    if not os.path.exists(location_path):
+        return _("Location <%s> doesn't exist") % location_path
+    # permament mapset
+    if "PERMANENT" not in os.listdir(location_path):
+        return (
+            _(
+                "<%s> is not a valid GRASS Location"
+                " because PERMANENT Mapset is missing"
+            )
+            % location_path
+        )
+    if not os.path.isdir(permanent_path):
+        return (
+            _(
+                "<%s> is not a valid GRASS Location"
+                " because PERMANENT is not a directory"
+            )
+            % location_path
+        )
+    # partially based on the is_location_valid() function
+    if not os.path.isfile(os.path.join(permanent_path, "DEFAULT_WIND")):
+        return (
+            _(
+                "<%s> is not a valid GRASS Location"
+                " because PERMANENT Mapset does not have a DEFAULT_WIND file"
+                " (default computational region)"
+            )
+            % location_path
+        )
+    # no reason for invalidity found (might be valid)
+    if none_for_no_reason:
+        return None
+    return _("Location <{location_path}> is invalid for an unknown reason").format(
+        location_path=location_path
+    )
+
+
+def get_location_invalid_suggestion(database, location):
+    """Return suggestion what to do when specified location is not valid
+
+    It gives suggestion when:
+     * A mapset was specified instead of a location.
+     * A GRASS database was specified instead of a location.
+    """
+    location_path = os.path.join(database, location)
+    # a common error is to use mapset instead of location,
+    # if that's the case, include that info into the message
+    if is_mapset_valid(location_path):
+        return _(
+            "<{location}> looks like a mapset, not a location."
+            " Did you mean just <{one_dir_up}>?"
+        ).format(location=location, one_dir_up=database)
+    # confusion about what is database and what is location
+    if dir_contains_location(location_path):
+        return _(
+            "It looks like <{location}> contains locations."
+            " Did you mean to specify one of them?"
+        ).format(location=location)
+    return None

+ 43 - 0
lib/python/grassdb/create.py

@@ -0,0 +1,43 @@
+"""
+Create objects in GRASS GIS Spatial Database
+
+(C) 2020 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.
+
+.. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
+"""
+
+
+import os
+import shutil
+import getpass
+
+
+def create_mapset(database, location, mapset):
+    """Creates a mapset in a specified location"""
+    location_path = os.path.join(database, location)
+    mapset_path = os.path.join(location_path, mapset)
+    # create an empty directory
+    os.mkdir(mapset_path)
+    # copy DEFAULT_WIND file and its permissions from PERMANENT
+    # to WIND in the new mapset
+    region_path1 = os.path.join(location_path, "PERMANENT", "DEFAULT_WIND")
+    region_path2 = os.path.join(location_path, mapset, "WIND")
+    shutil.copy(region_path1, region_path2)
+    # set permissions to u+rw,go+r (disabled; why?)
+    # os.chmod(os.path.join(database,location,mapset,'WIND'), 0644)
+
+
+def get_default_mapset_name():
+    """Returns default name for mapset."""
+    try:
+        result = getpass.getuser()
+        # Raise error if not ascii (not valid mapset name).
+        result.encode("ascii")
+    except UnicodeEncodeError:
+        # Fall back to fixed name.
+        result = "user"
+
+    return result

+ 47 - 0
lib/python/grassdb/data.py

@@ -0,0 +1,47 @@
+"""
+Manipulate data in mapsets in GRASS GIS Spatial Database
+
+(C) 2020 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.
+
+.. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
+"""
+
+import grass.script as gs
+
+
+def map_exists(name, element, mapset=None, env=None):
+    """Check is map is present in the mapset given in the environment
+
+    :param name: Name of the map
+    :param element: Data type ('raster', 'raster_3d', and 'vector')
+    :param env: Environment created by function grass.script.create_environment
+    :param mapset: Mapset name, "." for current mapset only,
+                   None for all mapsets in the search path
+    """
+    # change type to element used by find file
+    if element == "raster":
+        element = "cell"
+    elif element == "raster_3d":
+        element = "grid3"
+    # g.findfile returns non-zero when file was not found
+    # se we ignore return code and just focus on stdout
+    process = gs.start_command(
+        "g.findfile",
+        flags="n",
+        element=element,
+        file=name,
+        mapset=mapset,
+        stdout=gs.PIPE,
+        stderr=gs.PIPE,
+        env=env,
+    )
+    output, unused_errors = process.communicate()
+    info = gs.parse_key_val(output, sep="=")
+    # file is the key questioned in grass.script.core find_file()
+    # return code should be equivalent to checking the output
+    if info["file"]:
+        return True
+    return False

+ 43 - 0
lib/python/grassdb/manage.py

@@ -0,0 +1,43 @@
+"""
+Managing existing objects in a GRASS GIS Spatial Database
+
+(C) 2020 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.
+
+.. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
+"""
+
+
+import os
+import shutil
+
+
+def delete_mapset(database, location, mapset):
+    """Deletes a specified mapset"""
+    if mapset == "PERMANENT":
+        raise ValueError(
+            _("Mapset PERMANENT cannot be deleted (a whole location can be)")
+        )
+    shutil.rmtree(os.path.join(database, location, mapset))
+
+
+def delete_location(database, location):
+    """Deletes a specified location"""
+    shutil.rmtree(os.path.join(database, location))
+
+
+def rename_mapset(database, location, old_name, new_name):
+    """Rename mapset from *old_name* to *new_name*"""
+    if old_name == "PERMANENT":
+        raise ValueError(_("Mapset PERMANENT cannot be renamed"))
+    location_path = os.path.join(database, location)
+    os.rename(
+        os.path.join(location_path, old_name), os.path.join(location_path, new_name)
+    )
+
+
+def rename_location(database, old_name, new_name):
+    """Rename location from *old_name* to *new_name*"""
+    os.rename(os.path.join(database, old_name), os.path.join(database, new_name))