Browse Source

r.import and v.import: Use PID and node for tmp location (#653)

This adds several new functions to the library API for creating unique names
for temporary elements/objects in GRASS database. The one needed by r.import and v.import
is adding PID and node name to a name provided by the caller which is based on the current
practice of adding PID, but now adding the node name similarly to how mapset .tmp directory
is handled.

A completely new test is provided for basic functionality of v.import and the current r.import
is partially fixed, but still failing which is unrelated to this change.
A parallel test with reprojection is provided for both modules, although the v.import
is suboptimal because the attribute table import cannot be currently disabled.
The library functions are tested for expected results (but not for likelihood of collisions).

The library functions are based on the multiple needs in r.import and v.import which
create a temporary vector in the current mapset and existing functions in the library.
The goal is to provide a unified API as well as clear way how to fix user code in the
future if any of the methods will turn up to be wrong.

A new legalize/replace/escape vector name function is based on C equivalent Vect_legal_filename()
in terms of character limitations and keyword checks, but it is trying to be more flexible
since it is in Python and it is actually trying to create a legal name automatically
as opposed to being used for checking user input and giving an error. The keywords check
from Vect_legal_filename() is limited and left as future work.
Vaclav Petras 4 years ago
parent
commit
ff33a47ec7

+ 6 - 0
lib/python/script/core.py

@@ -940,6 +940,8 @@ def tempname(length, lowercase=False):
 
     >>> tempname(12)
     'tmp_MxMa1kAS13s9'
+
+    .. seealso:: functions :func:`append_uuid()`, :func:`append_random()`
     """
 
     chars = string.ascii_lowercase + string.digits
@@ -1762,6 +1764,10 @@ def debug_level(force=False):
     return _debug_level
 
 
+# TODO: Move legal_name() to utils or a new dedicated "name" module.
+# TODO: Remove the pygrass backwards compatibility version of it?
+
+
 def legal_name(s):
     """Checks if the string contains only allowed characters.
 

+ 245 - 0
lib/python/script/testsuite/test_names.py

@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+import platform
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+import grass.script as gs
+from grass.script import legal_name
+from grass.script import utils
+
+
+class TestUnique(TestCase):
+    """Tests functions generating unique names and suffixes"""
+
+    def test_append_node_pid(self):
+        base_name = "tmp_abc"
+        full_name = utils.append_node_pid(base_name)
+        self.assertIn(base_name, full_name)
+        self.assertGreater(len(full_name), len(base_name))
+        self.assertIn(str(os.getpid()), full_name)
+        self.assertIn(platform.node(), full_name)
+        self.assertTrue(legal_name(full_name))
+        # TODO: It should be also a valid vector name
+        # but we don't have a function for that (same for all)
+        full_name2 = utils.append_node_pid(base_name)
+        self.assertEqual(
+            full_name, full_name2, msg="There should be no randomness or change."
+        )
+
+    def test_append_uuid(self):
+        base_name = "tmp_abc"
+        full_name = utils.append_uuid(base_name)
+        self.assertIn(base_name, full_name)
+        self.assertGreater(len(full_name), len(base_name))
+        self.assertTrue(legal_name(full_name))
+        full_name2 = utils.append_uuid(base_name)
+        # There is a low chance of collision.
+        self.assertNotEqual(full_name, full_name2)
+
+    def test_append_random_suffix(self):
+        base_name = "tmp_abc"
+        size = 10
+        full_name = utils.append_random(base_name, suffix_length=size)
+        self.assertIn(base_name, full_name)
+        self.assertGreater(len(full_name), len(base_name))
+        self.assertGreaterEqual(len(full_name), len(base_name) + size)
+        self.assertTrue(legal_name(full_name))
+        full_name2 = utils.append_random(base_name, suffix_length=size)
+        # There is a low chance of collision.
+        self.assertNotEqual(full_name, full_name2)
+
+    def test_append_random_total(self):
+        base_name = "tmp_abc"
+        size = 10
+        full_name = utils.append_random(base_name, total_length=size)
+        self.assertIn(base_name, full_name)
+        self.assertGreater(len(full_name), len(base_name))
+        self.assertEqual(len(full_name), size)
+        self.assertTrue(legal_name(full_name))
+        full_name2 = utils.append_random(base_name, total_length=size)
+        self.assertNotEqual(full_name, full_name2)
+
+    def test_append_random_one_arg(self):
+        base_name = "tmp_abc"
+        size = 10
+        full_name = utils.append_random(base_name, size)
+        self.assertIn(base_name, full_name)
+        self.assertGreater(len(full_name), len(base_name))
+        self.assertGreaterEqual(len(full_name), len(base_name) + size)
+        self.assertTrue(legal_name(full_name))
+        full_name2 = utils.append_random(base_name, size)
+        self.assertNotEqual(full_name, full_name2)
+
+    def test_append_random_two_args(self):
+        base_name = "tmp_abc"
+        size = 10
+        self.assertRaises(ValueError, utils.append_random, base_name, size, size)
+
+    def test_append_random_total_name_too_long(self):
+        base_name = "tmp_abc"
+        size = 4
+        self.assertRaises(ValueError, utils.append_random, base_name, total_length=size)
+
+
+class TestLegalizeVectorName(TestCase):
+    """Tests legalize_vector_name() function"""
+
+    # Names for general tests (input, output)
+    # Not ideal as the only tests don't show which one failed for errors which
+    # are not assert failures.
+    # Good for adding new combinations.
+    names = [
+        ("a", "a"),
+        ("__abc__", "x__abc__"),
+        ("125", "x125"),
+        ("1a25g78g", "x1a25g78g"),
+        ("_X1", "x_X1"),
+        ("øaøbøcø", "x_a_b_c_"),
+    ]
+
+    def works_for_vector_table_column(self, name):
+        """Try to create vector, with attribute table and a column with name
+
+        Returns false when that fails. Does not report further errors.
+        Use together with other tests.
+        """
+        try:
+            gs.run_command("v.edit", map=name, tool="create")
+            gs.run_command("v.db.addtable", map=name)
+            gs.run_command("v.db.addcolumn", map=name, columns=name)
+            works = True
+        except gs.CalledModuleError:
+            works = False
+        finally:
+            gs.run_command("g.remove", name=name, type="vector", flags="f")
+        return works
+
+    def test_is_legal_name(self):
+        """Check that it is a G_legal_name()"""
+        for name, reference in self.names:
+            legalized = utils.legalize_vector_name(name)
+            self.assertTrue(legal_name(legalized))
+            self.assertEqual(legalized, reference)
+
+    def test_is_working_in_vector_table_column(self):
+        """Check that a vector and column can be created
+
+        This indirectly tests that it is Vect_legal_name().
+        """
+        for name, reference in self.names:
+            legalized = utils.legalize_vector_name(name)
+            self.assertTrue(self.works_for_vector_table_column(legalized))
+            self.assertEqual(legalized, reference)
+
+    def test_no_change(self):
+        """There should be no change if the name is valid already"""
+        name = "perfectly_valid_name"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, name)
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_has_dashes(self):
+        """Check behavior with dash (a typical and important case)"""
+        name = "abc-def-1"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "abc_def_1")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_has_spaces(self):
+        """Check behavior with a space"""
+        name = "abc def 1"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "abc_def_1")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_has_at_sign(self):
+        """Check behavior with @
+
+        This can happen, e.g., when a full map name is used, so testing
+        explicitly.
+        """
+        name = "abc@def"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "abc_def")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_has_dollar(self):
+        """Check with one invalid character"""
+        name = "abc_$def"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "abc__def")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_with_ascii_art(self):
+        """Check with a lot of invalid characters"""
+        name = "abc_>>>def<<<_!"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "abc____def_____")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_starts_with_digit(self):
+        """Check string starting with digit"""
+        name = "123456"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "x123456")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_starts_with_underscore(self):
+        """Check string starting with underscore"""
+        name = "_123456"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "x_123456")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_has_unicode(self):
+        """Check string with unicode"""
+        name = "abøc"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "ab_c")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_starts_with_unicode(self):
+        """Check string starting with unicode"""
+        name = "øabc"
+        legalized = utils.legalize_vector_name(name)
+        self.assertEqual(legalized, "x_abc")
+        self.assertTrue(legal_name(legalized))
+        self.assertTrue(self.works_for_vector_table_column(legalized))
+
+    def test_diff_only_first_character(self):
+        """Check two string with only the first character being different"""
+        name1 = "1800"
+        name2 = "2800"
+        legalized1 = utils.legalize_vector_name(name1)
+        legalized2 = utils.legalize_vector_name(name2)
+        self.assertNotEqual(legalized1, legalized2)
+
+    def test_custom_prefix(self):
+        """Check providing custom prefix"""
+        name = "1800"
+        prefix = "prefix_a1_"
+        legalized = utils.legalize_vector_name(name, fallback_prefix=prefix)
+        self.assertEqual(len(prefix) + len(name), len(legalized))
+        self.assertIn(prefix, legalized)
+
+    def test_no_prefix(self):
+        """Check providing custom prefix"""
+        name = "1800"
+        legalized = utils.legalize_vector_name(name, fallback_prefix="")
+        self.assertEqual(len(name), len(legalized))
+
+
+if __name__ == "__main__":
+    test()

+ 139 - 0
lib/python/script/utils.py

@@ -25,6 +25,10 @@ import locale
 import shlex
 import re
 import time
+import platform
+import uuid
+import random
+import string
 
 
 if sys.version_info.major >= 3:
@@ -486,3 +490,138 @@ def clock():
     if sys.version_info > (3,2):
         return time.perf_counter()
     return time.clock()
+
+
+def legalize_vector_name(name, fallback_prefix="x"):
+    """Make *name* usable for vectors, tables, and columns
+
+    The returned string is a name usable for vectors, tables, and columns,
+    i.e., it is a vector legal name which is a string containing only
+    lowercase and uppercase ASCII letters, digits, and underscores.
+
+    Invalid characters are replaced by underscores.
+    If the name starts with an invalid character, the name is prefixed with
+    *fallback_prefix*. This increases the length of the resulting name by the
+    length of the prefix.
+
+    The *fallback_prefix* can be empty which is useful when the *name* is later
+    used as a suffix for some other valid name.
+
+    ValueError is raised when provided *name* is empty or *fallback_prefix*
+    does not start with a valid character.
+    """
+    # The implementation is based on Vect_legal_filename().
+    if not name:
+        raise ValueError("name cannot be empty")
+    if fallback_prefix and re.match("[^A-Za-z]", fallback_prefix[0]):
+        raise ValueError("fallback_prefix must start with an ASCII letter")
+    if fallback_prefix and re.match("[^A-Za-z]", name[0], flags=re.ASCII):
+        # We prefix here rather than just replace, because in cases of unique
+        # identifiers, e.g., columns or node names, replacing the first
+        # character by the same replacement character increases chances of
+        # conflict (e.g. column names 10, 20, 30).
+        name = "{fallback_prefix}{name}".format(**locals())
+    name = re.sub("[^A-Za-z0-9_]", "_", name, flags=re.ASCII)
+    keywords = ["and", "or", "not"]
+    if name in keywords:
+        name = "{name}_".format(**locals())
+    return name
+
+
+def append_node_pid(name):
+    """Add node name and PID to a name (string)
+
+    For the result to be unique, the name needs to be unique within a process.
+    Given that, the result will be unique enough for use in temporary maps
+    and other elements on single machine or an HPC cluster.
+
+    The returned string is a name usable for vectors, tables, and columns
+    (vector legal name) as long as provided argument *name* is.
+
+    >>> append_node_pid("tmp_raster_1")
+
+    ..note::
+
+        Before you use this function for creating temporary files (i.e., normal
+        files on disk, not maps and other mapset elements), see functions
+        designed for it in the GRASS GIS or standard Python library. These
+        take care of collisions already on different levels.
+    """
+    # We are using this node as a suffix, so we don't need to make sure it
+    # is prefixed with additional character(s) since that's exactly what
+    # happens in this function.
+    # Note that this may still cause collisions when nodes are named in a way
+    # that they collapse into the same name after the replacements are done,
+    # but we consider that unlikely given that
+    # nodes will be likely already named as something close to what we need.
+    node = legalize_vector_name(platform.node(), fallback_prefix="")
+    pid = os.getpid()
+    return "{name}_{node}_{pid}".format(**locals())
+
+
+def append_uuid(name):
+    """Add UUID4 to a name (string)
+
+    To generate a name of an temporary mapset element which is unique in a
+    system, use :func:`append_node_pid()` in a combination with a name unique
+    within your process.
+
+    To avoid collisions, never shorten the name obtained from this function.
+    A shortened UUID does not have the collision guarantees the full UUID has.
+
+    For a random name of a given shorter size, see :func:`append_random()`.
+
+    >>> append_uuid("tmp")
+
+    ..note::
+
+        See the note about creating temporary files in the
+        :func:`append_node_pid()` description.
+    """
+    suffix = uuid.uuid4().hex
+    return "{name}_{suffix}".format(**locals())
+
+
+def append_random(name, suffix_length=None, total_length=None):
+    """Add a random part to of a specified length to a name (string)
+
+    >>> append_random("tmp", 8)
+    >>> append_random("tmp", total_length=16)
+
+    ..note::
+
+        Note that this will be influeced by the random seed set for the Python
+        random package.
+
+    ..note::
+
+        See the note about creating temporary files in the
+        :func:`append_node_pid()` description.
+    """
+    if suffix_length and total_length:
+        raise ValueError(
+            "Either suffix_length or total_length can be provided, not both"
+        )
+    if not suffix_length and not total_length:
+        raise ValueError(
+            "suffix_length or total_length has to be provided"
+        )
+    if total_length:
+        # remove len of name and one underscore
+        name_length = len(name)
+        suffix_length = total_length - name_length - 1
+        if suffix_length <= 0:
+            raise ValueError(
+                "No characters left for the suffix:"
+                " total_length <{total_length}> is too small"
+                " or name <{name}> ({name_length}) is too long".format(
+                    **locals()
+                )
+            )
+    # We don't do lower and upper case because that could cause conflicts in
+    # contexts which are case-insensitive.
+    # We use lowercase because that's what is in UUID4 hex string.
+    allowed_chars = string.ascii_lowercase + string.digits
+    # The following can be shorter with random.choices from Python 3.6.
+    suffix = ''.join(random.choice(allowed_chars) for _ in range(suffix_length))
+    return "{name}_{suffix}".format(**locals())

+ 2 - 2
scripts/r.import/r.import.py

@@ -207,8 +207,8 @@ def main():
     tgtmapset = grassenv['MAPSET']
     GISDBASE = grassenv['GISDBASE']
 
-    TMPLOC = 'temp_import_location_' + str(os.getpid())
-    TMP_REG_NAME = 'vreg_tmp_' + str(os.getpid())
+    TMPLOC = grass.append_node_pid("tmp_r_import_location")
+    TMP_REG_NAME = grass.append_node_pid("tmp_r_import_region")
 
     SRCGISRC, src_env = grass.create_environment(GISDBASE, TMPLOC, 'PERMANENT')
 

+ 81 - 0
scripts/r.import/testsuite/test_parallel.sh

@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+# Test parallel calls of r.import in Bash.
+# To test the reprojection, it needs to run in a location other than
+# WGS84 (that's the CRS of imported file used) or r.import may (should)
+# skip the reprojection part of the code.
+# Imported raster map presence or absence based on a search pattern is
+# used to evaluate if the expected result is created.
+
+# This test likely overwhelms the system for a while because all the
+# processes will start more or less at once, but this is exactly the
+# point of the test because we want increase a chance of a potential
+# race condition or name conflict.
+
+# Undefined variables are errors.
+set -u
+# More set commands later on.
+
+if [ $# -eq 0 ]
+then
+    NUM_SERIALS=3
+    NUM_PARALLELS=50
+elif [ $# -eq 2 ]
+then
+    # Allow user to set a large number of processes.
+    NUM_SERIALS=$3
+    NUM_PARALLELS=$4
+else
+    >&2 echo "Usage:"
+    >&2 echo "  $0"
+    >&2 echo "  $0 <nproc serial> <nproc parallel>"
+    >&2 echo "Example:"
+    >&2 echo "  $0 5 300"
+    >&2 echo "Use zero or two parameters, not $#."
+    exit 1
+fi
+
+# Remove maps at exit.
+cleanup () {
+    EXIT_CODE=$?
+    g.remove type=raster pattern="test_parallel_ser_*" -f --quiet
+    g.remove type=raster pattern="test_parallel_par_*" -f --quiet
+    exit $EXIT_CODE
+}
+
+trap cleanup EXIT
+
+DATA="data/data2.asc"
+
+# Fail fast and show commands
+set -e
+set -x
+
+# Serial
+# The parallel loop version won't fail even if command returns non-zero,
+# so we need to check the command ahead of time.
+# Since this is useful mostly for making sure the command works for this
+# script, so the command should be exactly the same.
+
+for i in `seq 1 $NUM_SERIALS`
+do
+    r.import input="$DATA" output="test_parallel_ser_$i"
+done
+
+# Parallel
+
+for i in `seq 1 $NUM_PARALLELS`
+do
+    r.import input="$DATA" output="test_parallel_par_$i" &
+done
+
+wait
+
+EXPECTED=$NUM_PARALLELS
+NUM=$(g.list type=raster pattern='test_parallel_par_*' mapset=. | wc -l)
+
+if [ ${NUM} -ne ${EXPECTED} ]
+then
+    echo "Parallel test: Got ${NUM} but expected ${EXPECTED} maps"
+    exit 1
+fi

+ 3 - 1
scripts/r.import/testsuite/test_r_import.py

@@ -1,3 +1,5 @@
+#!/usr/bin/env python3
+
 from grass.gunittest.case import TestCase
 from grass.gunittest.main import test
 
@@ -29,7 +31,7 @@ class TestRImportRegion(TestCase):
                           resample='bilinear')
         reference = dict(north=223490, south=223390, east=636820, west=636710,
                          nsres=10, ewres=10, datatype='FCELL')
-        self.assertRasterFitsInfo(raster=self.imported, reference=reference)
+        self.assertRasterFitsInfo(raster=self.imported, reference=reference, precision=1e-6)
 
     def test_import_asc_custom_res(self):
         """Import ASC in different projection, with specified resolution"""

BIN
scripts/v.import/testsuite/data/all_types.gpkg


BIN
scripts/v.import/testsuite/data/all_types_wgs84.gpkg


+ 88 - 0
scripts/v.import/testsuite/test_parallel.sh

@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+
+# Test parallel calls of v.import in Bash.
+# To test the reprojection, it needs to run in a location other than
+# WGS84 (that's the CRS of imported file used) or v.import may (should)
+# skip the reprojection part of the code.
+# Imported raster map presence or absence based on a search pattern is
+# used to evaluate if the expected result is created.
+
+# This test likely overwhelms the system for a while because all the
+# processes will start more or less at once, but this is exactly the
+# point of the test because we want increase a chance of a potential
+# race condition or name conflict.
+
+# TODO: This test should import without attribute table (in simple
+# SQLite case), but that's not possible with the current v.import
+# and also the v.out.ogr seems not to respect -s or it lacks
+# a "no attributes" flag.
+# However, the test runs fine because the maps are created and failure
+# to create an attribute table is not currently considered fatal.
+
+# Undefined variables are errors.
+set -u
+# More set commands later on.
+
+if [ $# -eq 0 ]
+then
+    NUM_SERIALS=3
+    NUM_PARALLELS=50
+elif [ $# -eq 2 ]
+then
+    # Allow user to set a large number of processes.
+    NUM_SERIALS=$3
+    NUM_PARALLELS=$4
+else
+    >&2 echo "Usage:"
+    >&2 echo "  $0"
+    >&2 echo "  $0 <nproc serial> <nproc parallel>"
+    >&2 echo "Example:"
+    >&2 echo "  $0 5 300"
+    >&2 echo "Use zero or two parameters, not $#."
+    exit 1
+fi
+
+# Remove maps at exit.
+cleanup () {
+    EXIT_CODE=$?
+    g.remove type=vector pattern="test_parallel_ser_*" -f --quiet
+    g.remove type=vector pattern="test_parallel_par_*" -f --quiet
+    exit $EXIT_CODE
+}
+
+trap cleanup EXIT
+
+DATA="data/all_types_wgs84.gpkg"
+
+# Fail fast and show commands
+set -e
+set -x
+
+# Serial
+# The parallel loop version won't fail even if command returns non-zero,
+# so we need to check the command ahead of time.
+# Since this is useful mostly for making sure the command works for this
+# script, so the command should be exactly the same.
+
+for i in `seq 1 $NUM_SERIALS`
+do
+    v.import input="$DATA" output="test_parallel_ser_$i"
+done
+
+# Parallel
+
+for i in `seq 1 $NUM_PARALLELS`
+do
+    v.import input="$DATA" output="test_parallel_par_$i" &
+done
+
+wait
+
+EXPECTED=$NUM_PARALLELS
+NUM=$(g.list type=vector pattern='test_parallel_par_*' mapset=. | wc -l)
+
+if [ ${NUM} -ne ${EXPECTED} ]
+then
+    echo "Parallel test: Got ${NUM} but expected ${EXPECTED} maps"
+    exit 1
+fi

+ 112 - 0
scripts/v.import/testsuite/test_v_import.py

@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+
+# Input data notes
+
+# Created in:
+# nc_spm/PERMANENT
+
+# all_types... files
+# File with 2 points, 1 line, and 3 areas created in wxGUI digitizer
+# v.out.ogr input=all_types output=data/all_types.gpkg format=GPKG
+# ogr2ogr data/all_types_wgs84.gpkg data/all_types.gpkg -f GPKG -t_srs EPSG:4326
+
+
+class TestVImport(TestCase):
+
+    imported = "test_v_import_imported"
+
+    def tearDown(cls):
+        """Remove imported map after each test method"""
+        cls.runModule("g.remove", flags="f", type="vector", name=cls.imported)
+
+    def test_import_same_proj_gpkg(self):
+        """Import GPKG in same proj, default params"""
+        self.assertModule("v.import", input="data/all_types.gpkg", output=self.imported)
+        self.assertVectorExists(self.imported)
+        self.assertVectorFitsExtendedInfo(
+            vector=self.imported,
+            reference=dict(
+                name=self.imported,
+                level=2,
+                num_dblinks=1,
+                attribute_table=self.imported,
+            ),
+        )
+        self.assertVectorFitsRegionInfo(
+            vector=self.imported,
+            # Values rounded to one decimal point.
+            reference=dict(
+                north=227744.8,
+                south=215259.6,
+                east=644450.6,
+                west=631257.4,
+                top=0,
+                bottom=0,
+            ),
+            precision=0.2,
+        )
+        self.assertVectorFitsTopoInfo(
+            vector=self.imported,
+            reference=dict(
+                nodes=5,
+                points=2,
+                lines=1,
+                boundaries=3,
+                centroids=3,
+                areas=3,
+                islands=3,
+                primitives=9,
+                map3d=0,
+            ),
+        )
+
+    def test_import_gpkg_wgs84(self):
+        """Import GPKG in same proj, default params"""
+        self.assertModule(
+            "v.import", input="data/all_types_wgs84.gpkg", output=self.imported
+        )
+        self.assertVectorExists(self.imported)
+        self.assertVectorFitsExtendedInfo(
+            vector=self.imported,
+            reference=dict(
+                name=self.imported,
+                level=2,
+                num_dblinks=1,
+                attribute_table=self.imported,
+            ),
+        )
+        self.assertVectorFitsRegionInfo(
+            vector=self.imported,
+            # Values rounded to one decimal point.
+            reference=dict(
+                north=227744.8,
+                south=215259.6,
+                east=644450.6,
+                west=631257.4,
+                top=0,
+                bottom=0,
+            ),
+            precision=0.2,
+        )
+        self.assertVectorFitsTopoInfo(
+            vector=self.imported,
+            reference=dict(
+                nodes=5,
+                points=2,
+                lines=1,
+                boundaries=3,
+                centroids=3,
+                areas=3,
+                islands=3,
+                primitives=9,
+                map3d=0,
+            ),
+        )
+
+
+if __name__ == "__main__":
+    test()

+ 3 - 2
scripts/v.import/v.import.py

@@ -241,7 +241,7 @@ def main():
     TGTGISRC = os.environ['GISRC']
     SRCGISRC = grass.tempfile()
 
-    TMPLOC = 'temp_import_location_' + str(os.getpid())
+    TMPLOC = grass.append_node_pid("tmp_v_import_location")
 
     f = open(SRCGISRC, 'w')
     f.write('MAPSET: PERMANENT\n')
@@ -289,7 +289,8 @@ def main():
         os.environ['GISRC'] = str(TGTGISRC)
 
         # v.in.region in tgt
-        vreg = 'vreg_' + str(os.getpid())
+        vreg = grass.append_node_pid("tmp_v_import_region")
+
         grass.run_command('v.in.region', output=vreg, quiet=True)
 
         # reproject to src