Repository: rasterio/rasterio

Issue:
Crashes on process exit when CPL_DEBUG is enabled
Reproducer:

```
$ CPL_DEBUG=ON gdb python
[...]
(gdb) run
[...]
>>> import rasterio
>>> exit()
Program received signal SIGSEGV, Segmentation fault.
0x000000000052b351 in PyErr_Format ()
(gdb) bt
#0  0x000000000052b351 in PyErr_Format ()
#1  0x00000000004b7dce in PyObject_GetIter ()
#2  0x00000000004914c0 in _PySequence_IterSearch ()
#3  0x0000000000421fbf in PySequence_Contains ()
#4  0x00007fffe0e6032c in __Pyx_PySequence_ContainsTF (eq=2, seq=<optimized out>, item=0x95b170) at rasterio/_drivers.c:677
#5  __pyx_f_8rasterio_8_drivers_errorHandler (__pyx_v_err_class=CE_Debug, __pyx_v_err_no=0, 
    __pyx_v_msg=0xb1eb50 "GDAL: In GDALDestroy - unloading GDAL shared library.") at rasterio/_drivers.c:1082
#6  0x00007ffff5208d50 in CPLDebug (pszCategory=<optimized out>, pszFormat=<optimized out>) at cpl_error.cpp:612
#7  0x00007ffff5199d42 in GDALDestroy () at gdaldllmain.cpp:71
#8  GDALDestructor () at gdaldllmain.cpp:122
#9  0x00007ffff7dea70a in _dl_fini () at dl-fini.c:252
#10 0x00007ffff78331a9 in __run_exit_handlers (status=0, listp=0x7ffff7bb56c8 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true) at exit.c:82
#11 0x00007ffff78331f5 in __GI_exit (status=<optimized out>) at exit.c:104
#12 0x00007ffff7818f4c in __libc_start_main (main=0x44f9c2 <main>, argc=1, argv=0x7fffffffe3a8, init=<optimized out>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffe398) at libc-start.c:321
#13 0x0000000000578c4e in _start ()
```

Perhaps rasterio could unset its GDAL error handler when it is unloaded ? But that assumes that GDAL isn't already unloaded... So not sure if that's fixable



Discussion:
Implementing #876 would solve this issue


Resolving patch:
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -65,7 +65,6 @@ def window_index(*args, **kwargs):
 log.addHandler(NullHandler())
 
 
-@ensure_env
 def open(path, mode='r', driver=None, width=None, height=None,
          count=None, crs=None, transform=None, dtype=None, nodata=None,
          **kwargs):
@@ -198,40 +197,44 @@ def open(path, mode='r', driver=None, width=None, height=None,
     if transform:
         transform = guard_transform(transform)
 
-    # Get AWS credentials if we're attempting to access a raster
-    # on S3.
-    pth, archive, scheme = parse_path(path)
-    if scheme == 's3':
-        Env().get_aws_credentials()
-        log.debug("AWS credentials have been obtained")
-
     # Check driver/mode blacklist.
     if driver and is_blacklisted(driver, mode):
         raise RasterioIOError(
             "Blacklisted: file cannot be opened by "
             "driver '{0}' in '{1}' mode".format(driver, mode))
 
-    # Create dataset instances and pass the given env, which will
-    # be taken over by the dataset's context manager if it is not
-    # None.
-    if mode == 'r':
-        s = DatasetReader(path)
-    elif mode == 'r-':
-        warnings.warn("'r-' mode is deprecated, use 'r'", DeprecationWarning)
-        s = DatasetReader(path)
-    elif mode == 'r+':
-        s = get_writer_for_path(path)(path, mode)
-    elif mode == 'w':
-        s = get_writer_for_driver(driver)(path, mode, driver=driver,
-                                          width=width, height=height,
-                                          count=count, crs=crs,
-                                          transform=transform, dtype=dtype,
-                                          nodata=nodata, **kwargs)
-    else:
-        raise ValueError(
-            "mode string must be one of 'r', 'r+', or 'w', not %s" % mode)
-    s.start()
-    return s
+    _, _, scheme = parse_path(path)
+
+    with Env() as env:
+        # Get AWS credentials only if we're attempting to access a
+        # raster using the S3 scheme.
+        if scheme == 's3':
+            env.get_aws_credentials()
+            log.debug("AWS credentials have been obtained")
+
+        # Create dataset instances and pass the given env, which will
+        # be taken over by the dataset's context manager if it is not
+        # None.
+        if mode == 'r':
+            s = DatasetReader(path)
+        elif mode == 'r-':
+            warnings.warn("'r-' mode is deprecated, use 'r'",
+                          DeprecationWarning)
+            s = DatasetReader(path)
+        elif mode == 'r+':
+            s = get_writer_for_path(path)(path, mode)
+        elif mode == 'w':
+            s = get_writer_for_driver(driver)(path, mode, driver=driver,
+                                              width=width, height=height,
+                                              count=count, crs=crs,
+                                              transform=transform,
+                                              dtype=dtype, nodata=nodata,
+                                              **kwargs)
+        else:
+            raise ValueError(
+                "mode string must be one of 'r', 'r+', or 'w', not %s" % mode)
+        s.start()
+        return s
 
 
 @ensure_env
diff --git a/rasterio/env.py b/rasterio/env.py
--- a/rasterio/env.py
+++ b/rasterio/env.py
@@ -1,5 +1,6 @@
 """Rasterio's GDAL/AWS environment"""
 
+from functools import wraps
 import logging
 
 from rasterio._drivers import (
@@ -19,7 +20,8 @@
 # Rasterio defaults
 default_options = {
     'CHECK_WITH_INVERT_PROJ': True,
-    'GTIFF_IMPLICIT_JPEG_OVR': False
+    'GTIFF_IMPLICIT_JPEG_OVR': False,
+    "I'M_ON_RASTERIO": True
 }
 
 class Env(object):
@@ -104,8 +106,7 @@ def __init__(self, aws_session=None, aws_access_key_id=None,
             self.aws_session._session.get_credentials()
             if self.aws_session else None)
         self.options = options.copy()
-        self.previous_options = {}
-        defenv()
+        self.context_options = {}
 
     def get_aws_credentials(self):
         """Get credentials and configure GDAL."""
@@ -131,7 +132,6 @@ def get_aws_credentials(self):
             options.update(aws_region=self.aws_session.region_name)
 
         # Pass these credentials to the GDAL environment.
-        defenv()
         global _env
         _env.update_config_options(**options)
 
@@ -141,14 +141,15 @@ def drivers(self):
         return _env.drivers()
 
     def __enter__(self):
-        self.previous_options = getenv()
+        defenv()
+        self.context_options = getenv()
         setenv(**self.options)
         log.debug("Entering env %r context", self)
         return self
 
     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
         delenv()
-        setenv(**self.previous_options)
+        setenv(**self.context_options)
         log.debug("Exiting env %r context", self)
 
 
@@ -197,5 +198,8 @@ def delenv():
 def ensure_env(f):
     """A decorator that ensures an env exists before a function
     calls any GDAL C functions."""
-    defenv()
-    return f
+    @wraps(f)
+    def wrapper(*args, **kwds):
+        with Env(WITH_RASTERIO_ENV=True):
+            return f(*args, **kwds)
+    return wrapper
diff --git a/rasterio/warp.py b/rasterio/warp.py
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -16,7 +16,6 @@
 from rasterio.transform import guard_transform
 
 
-@ensure_env
 def transform(src_crs, dst_crs, xs, ys, zs=None, **options):
     """Transform vectors from source to target coordinate reference system.
 
@@ -321,10 +320,6 @@ def calculate_default_transform(
     tuple
         Three elements: ``affine transform, width, and height``
 
-    Note
-    ----
-    Should be called within a rasterio.Env() context
-
     Some behavior of this function is determined by the
     CHECK_WITH_INVERT_PROJ environment variable
         YES: constrain output raster to extents that can be inverted


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -69,10 +69,11 @@ def test_env_accessors(gdalenv):
 
 
 def test_ensure_env_decorator(gdalenv):
-    def f(x):
-        return x
+    def f():
+        return getenv()['WITH_RASTERIO_ENV']
     wrapper = ensure_env(f)
-    assert wrapper == f
+    assert wrapper() is True
+    assert 'WITH_RASTERIO_ENV' not in getenv()
 
 
 def test_no_aws_gdal_config(gdalenv):
@@ -86,15 +87,12 @@ def test_no_aws_gdal_config(gdalenv):
 def test_env_options(gdalenv):
     """Test env options."""
     env = rasterio.Env(foo='x')
-    assert env.options == {'foo': 'x'}
-    assert not env.previous_options
-    expected = default_options.copy()
-    assert getenv() == rasterio.env._env.options == expected
-    expected.update({'foo': 'x'})
+    assert env.options['foo'] == 'x'
+    assert not env.context_options
     with env:
-        assert getenv() == rasterio.env._env.options == expected
-    del expected['foo']
-    assert getenv() == rasterio.env._env.options == expected
+        assert env.context_options['CHECK_WITH_INVERT_PROJ'] is True
+        assert env.context_options['GTIFF_IMPLICIT_JPEG_OVR'] is False
+        assert env.context_options["I'M_ON_RASTERIO"] is True
 
 
 def test_aws_session(gdalenv):
@@ -114,14 +112,12 @@ def test_aws_session_credentials(gdalenv):
     aws_session = boto3.Session(
         aws_access_key_id='id', aws_secret_access_key='key',
         aws_session_token='token', region_name='null-island-1')
-    s = rasterio.env.Env(aws_session=aws_session)
-    expected = default_options.copy()
-    assert getenv() == rasterio.env._env.options == expected
-    s.get_aws_credentials()
-    expected.update({
-        'aws_access_key_id': 'id', 'aws_region': 'null-island-1',
-        'aws_secret_access_key': 'key', 'aws_session_token': 'token'})
-    assert getenv() == rasterio.env._env.options == expected
+    with rasterio.env.Env(aws_session=aws_session) as s:
+        s.get_aws_credentials()
+        assert getenv()['aws_access_key_id'] == 'id'
+        assert getenv()['aws_region'] == 'null-island-1'
+        assert getenv()['aws_secret_access_key'] == 'key'
+        assert getenv()['aws_session_token'] == 'token'
 
 
 def test_with_aws_session_credentials(gdalenv):