Fixed #25938 -- Factored out CPointerBase base class for GEOSBase/GDALBase.

This commit is contained in:
Sergey Fedoseev 2016-12-16 03:59:08 +06:00 committed by Tim Graham
parent 4884472447
commit b01ceae843
16 changed files with 131 additions and 194 deletions

View File

@ -1,38 +1,6 @@
from ctypes import c_void_p
from django.contrib.gis.gdal.error import GDALException from django.contrib.gis.gdal.error import GDALException
from django.utils import six from django.contrib.gis.ptr import CPointerBase
class GDALBase(object): class GDALBase(CPointerBase):
""" null_ptr_exception_class = GDALException
Base object for GDAL objects that has a pointer access property
that controls access to the underlying C pointer.
"""
# Initially the pointer is NULL.
_ptr = None
# Default allowed pointer type.
ptr_type = c_void_p
# Pointer access property.
def _get_ptr(self):
# Raise an exception if the pointer isn't valid don't
# want to be passing NULL pointers to routines --
# that's very bad.
if self._ptr:
return self._ptr
else:
raise GDALException('GDAL %s pointer no longer valid.' % self.__class__.__name__)
def _set_ptr(self, ptr):
# Only allow the pointer to be set with pointers of the
# compatible type or None (NULL).
if isinstance(ptr, six.integer_types):
self._ptr = self.ptr_type(ptr)
elif ptr is None or isinstance(ptr, self.ptr_type):
self._ptr = ptr
else:
raise TypeError('Incompatible pointer type')
ptr = property(_get_ptr, _set_ptr)

View File

@ -51,6 +51,7 @@ from django.utils.six.moves import range
# The OGR_DS_* routines are relevant here. # The OGR_DS_* routines are relevant here.
class DataSource(GDALBase): class DataSource(GDALBase):
"Wraps an OGR Data Source object." "Wraps an OGR Data Source object."
destructor = capi.destroy_ds
def __init__(self, ds_input, ds_driver=False, write=False, encoding='utf-8'): def __init__(self, ds_input, ds_driver=False, write=False, encoding='utf-8'):
# The write flag. # The write flag.
@ -85,13 +86,6 @@ class DataSource(GDALBase):
# Raise an exception if the returned pointer is NULL # Raise an exception if the returned pointer is NULL
raise GDALException('Invalid data source file "%s"' % ds_input) raise GDALException('Invalid data source file "%s"' % ds_input)
def __del__(self):
"Destroys this DataStructure object."
try:
capi.destroy_ds(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def __iter__(self): def __iter__(self):
"Allows for iteration over the layers in a data source." "Allows for iteration over the layers in a data source."
for i in range(self.layer_count): for i in range(self.layer_count):

View File

@ -49,7 +49,7 @@ class Driver(GDALBase):
# Attempting to get the GDAL/OGR driver by the string name. # Attempting to get the GDAL/OGR driver by the string name.
for iface in (vcapi, rcapi): for iface in (vcapi, rcapi):
driver = iface.get_driver_by_name(force_bytes(name)) driver = c_void_p(iface.get_driver_by_name(force_bytes(name)))
if driver: if driver:
break break
elif isinstance(dr_input, int): elif isinstance(dr_input, int):

View File

@ -17,6 +17,7 @@ class Feature(GDALBase):
This class that wraps an OGR Feature, needs to be instantiated This class that wraps an OGR Feature, needs to be instantiated
from a Layer object. from a Layer object.
""" """
destructor = capi.destroy_feature
def __init__(self, feat, layer): def __init__(self, feat, layer):
""" """
@ -27,13 +28,6 @@ class Feature(GDALBase):
self.ptr = feat self.ptr = feat
self._layer = layer self._layer = layer
def __del__(self):
"Releases a reference to this object."
try:
capi.destroy_feature(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def __getitem__(self, index): def __getitem__(self, index):
""" """
Gets the Field object at the specified index, which may be either Gets the Field object at the specified index, which may be either

View File

@ -62,6 +62,7 @@ from django.utils.six.moves import range
# The OGR_G_* routines are relevant here. # The OGR_G_* routines are relevant here.
class OGRGeometry(GDALBase): class OGRGeometry(GDALBase):
"Generally encapsulates an OGR geometry." "Generally encapsulates an OGR geometry."
destructor = capi.destroy_geom
def __init__(self, geom_input, srs=None): def __init__(self, geom_input, srs=None):
"Initializes Geometry on either WKT or an OGR pointer as input." "Initializes Geometry on either WKT or an OGR pointer as input."
@ -120,13 +121,6 @@ class OGRGeometry(GDALBase):
# Setting the class depending upon the OGR Geometry Type # Setting the class depending upon the OGR Geometry Type
self.__class__ = GEO_CLASSES[self.geom_type.num] self.__class__ = GEO_CLASSES[self.geom_type.num]
def __del__(self):
"Deletes this Geometry."
try:
capi.destroy_geom(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
# Pickle routines # Pickle routines
def __getstate__(self): def __getstate__(self):
srs = self.srs srs = self.srs

View File

@ -57,6 +57,8 @@ class GDALRaster(GDALBase):
""" """
Wraps a raster GDAL Data Source object. Wraps a raster GDAL Data Source object.
""" """
destructor = capi.close_ds
def __init__(self, ds_input, write=False): def __init__(self, ds_input, write=False):
self._write = 1 if write else 0 self._write = 1 if write else 0
Driver.ensure_registered() Driver.ensure_registered()
@ -143,12 +145,6 @@ class GDALRaster(GDALBase):
else: else:
raise GDALException('Invalid data source input type: "{}".'.format(type(ds_input))) raise GDALException('Invalid data source input type: "{}".'.format(type(ds_input)))
def __del__(self):
try:
capi.close_ds(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -41,6 +41,7 @@ class SpatialReference(GDALBase):
the SpatialReference object "provide[s] services to represent coordinate the SpatialReference object "provide[s] services to represent coordinate
systems (projections and datums) and to transform between them." systems (projections and datums) and to transform between them."
""" """
destructor = capi.release_srs
def __init__(self, srs_input='', srs_type='user'): def __init__(self, srs_input='', srs_type='user'):
""" """
@ -91,13 +92,6 @@ class SpatialReference(GDALBase):
elif srs_type == 'epsg': elif srs_type == 'epsg':
self.import_epsg(srs_input) self.import_epsg(srs_input)
def __del__(self):
"Destroys this spatial reference."
try:
capi.release_srs(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def __getitem__(self, target): def __getitem__(self, target):
""" """
Returns the value of the given string attribute node, None if the node Returns the value of the given string attribute node, None if the node
@ -329,6 +323,7 @@ class SpatialReference(GDALBase):
class CoordTransform(GDALBase): class CoordTransform(GDALBase):
"The coordinate system transformation object." "The coordinate system transformation object."
destructor = capi.destroy_ct
def __init__(self, source, target): def __init__(self, source, target):
"Initializes on a source and target SpatialReference objects." "Initializes on a source and target SpatialReference objects."
@ -338,12 +333,5 @@ class CoordTransform(GDALBase):
self._srs1_name = source.name self._srs1_name = source.name
self._srs2_name = target.name self._srs2_name = target.name
def __del__(self):
"Deletes this Coordinate Transformation object."
try:
capi.destroy_ct(self._ptr)
except (AttributeError, TypeError):
pass
def __str__(self): def __str__(self):
return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name) return 'Transform from "%s" to "%s"' % (self._srs1_name, self._srs2_name)

View File

@ -1,38 +1,6 @@
from ctypes import c_void_p
from django.contrib.gis.geos.error import GEOSException from django.contrib.gis.geos.error import GEOSException
from django.contrib.gis.ptr import CPointerBase
class GEOSBase(object): class GEOSBase(CPointerBase):
""" null_ptr_exception_class = GEOSException
Base object for GEOS objects that has a pointer access property
that controls access to the underlying C pointer.
"""
# Initially the pointer is NULL.
_ptr = None
# Default allowed pointer type.
ptr_type = c_void_p
# Pointer access property.
def _get_ptr(self):
# Raise an exception if the pointer isn't valid don't
# want to be passing NULL pointers to routines --
# that's very bad.
if self._ptr:
return self._ptr
else:
raise GEOSException('NULL GEOS %s pointer encountered.' % self.__class__.__name__)
def _set_ptr(self, ptr):
# Only allow the pointer to be set with pointers of the
# compatible type or None (NULL).
if ptr is None or isinstance(ptr, self.ptr_type):
self._ptr = ptr
else:
raise TypeError('Incompatible pointer type')
# Property for controlling access to the GEOS object pointers. Using
# this raises an exception when the pointer is NULL, thus preventing
# the C library from attempting to access an invalid memory location.
ptr = property(_get_ptr, _set_ptr)

View File

@ -33,6 +33,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
_GEOS_CLASSES = None _GEOS_CLASSES = None
ptr_type = GEOM_PTR ptr_type = GEOM_PTR
destructor = capi.destroy_geom
has_cs = False # Only Point, LineString, LinearRing have coordinate sequences has_cs = False # Only Point, LineString, LinearRing have coordinate sequences
def __init__(self, geo_input, srid=None): def __init__(self, geo_input, srid=None):
@ -120,16 +121,6 @@ class GEOSGeometry(GEOSBase, ListMixin):
# geometries that do not have coordinate sequences) # geometries that do not have coordinate sequences)
self._set_cs() self._set_cs()
def __del__(self):
"""
Destroys this Geometry; in other words, frees the memory used by the
GEOS C++ object.
"""
try:
capi.destroy_geom(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def __copy__(self): def __copy__(self):
""" """
Returns a clone because the copy of a GEOSGeometry may contain an Returns a clone because the copy of a GEOSGeometry may contain an

View File

@ -9,6 +9,7 @@ class PreparedGeometry(GEOSBase):
operations. operations.
""" """
ptr_type = capi.PREPGEOM_PTR ptr_type = capi.PREPGEOM_PTR
destructor = capi.prepared_destroy
def __init__(self, geom): def __init__(self, geom):
# Keeping a reference to the original geometry object to prevent it # Keeping a reference to the original geometry object to prevent it
@ -20,12 +21,6 @@ class PreparedGeometry(GEOSBase):
raise TypeError raise TypeError
self.ptr = capi.geos_prepare(geom.ptr) self.ptr = capi.geos_prepare(geom.ptr)
def __del__(self):
try:
capi.prepared_destroy(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
def contains(self, other): def contains(self, other):
return capi.prepared_contains(self.ptr, other.ptr) return capi.prepared_contains(self.ptr, other.ptr)

View File

@ -119,17 +119,10 @@ class IOBase(GEOSBase):
self.ptr = self._constructor() self.ptr = self._constructor()
# Loading the real destructor function at this point as doing it in # Loading the real destructor function at this point as doing it in
# __del__ is too late (import error). # __del__ is too late (import error).
self._destructor.func = self._destructor.get_func( self.destructor.func = self.destructor.get_func(
*self._destructor.args, **self._destructor.kwargs *self.destructor.args, **self.destructor.kwargs
) )
def __del__(self):
# Cleaning up with the appropriate destructor.
try:
self._destructor(self._ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected
# ### Base WKB/WKT Reading and Writing objects ### # ### Base WKB/WKT Reading and Writing objects ###
@ -138,8 +131,8 @@ class IOBase(GEOSBase):
# objects. # objects.
class _WKTReader(IOBase): class _WKTReader(IOBase):
_constructor = wkt_reader_create _constructor = wkt_reader_create
_destructor = wkt_reader_destroy
ptr_type = WKT_READ_PTR ptr_type = WKT_READ_PTR
destructor = wkt_reader_destroy
def read(self, wkt): def read(self, wkt):
if not isinstance(wkt, (bytes, six.string_types)): if not isinstance(wkt, (bytes, six.string_types)):
@ -149,8 +142,8 @@ class _WKTReader(IOBase):
class _WKBReader(IOBase): class _WKBReader(IOBase):
_constructor = wkb_reader_create _constructor = wkb_reader_create
_destructor = wkb_reader_destroy
ptr_type = WKB_READ_PTR ptr_type = WKB_READ_PTR
destructor = wkb_reader_destroy
def read(self, wkb): def read(self, wkb):
"Returns a _pointer_ to C GEOS Geometry object from the given WKB." "Returns a _pointer_ to C GEOS Geometry object from the given WKB."
@ -166,8 +159,8 @@ class _WKBReader(IOBase):
# ### WKB/WKT Writer Classes ### # ### WKB/WKT Writer Classes ###
class WKTWriter(IOBase): class WKTWriter(IOBase):
_constructor = wkt_writer_create _constructor = wkt_writer_create
_destructor = wkt_writer_destroy
ptr_type = WKT_WRITE_PTR ptr_type = WKT_WRITE_PTR
destructor = wkt_writer_destroy
_trim = False _trim = False
_precision = None _precision = None
@ -219,8 +212,8 @@ class WKTWriter(IOBase):
class WKBWriter(IOBase): class WKBWriter(IOBase):
_constructor = wkb_writer_create _constructor = wkb_writer_create
_destructor = wkb_writer_destroy
ptr_type = WKB_WRITE_PTR ptr_type = WKB_WRITE_PTR
destructor = wkb_writer_destroy
def __init__(self, dim=2): def __init__(self, dim=2):
super(WKBWriter, self).__init__() super(WKBWriter, self).__init__()

View File

@ -1,23 +1,23 @@
import threading import threading
from django.contrib.gis.geos.base import GEOSBase
from django.contrib.gis.geos.libgeos import ( from django.contrib.gis.geos.libgeos import (
CONTEXT_PTR, error_h, lgeos, notice_h, CONTEXT_PTR, error_h, lgeos, notice_h,
) )
class GEOSContextHandle(object): class GEOSContextHandle(GEOSBase):
""" """
Python object representing a GEOS context handle. Python object representing a GEOS context handle.
""" """
ptr_type = CONTEXT_PTR
destructor = lgeos.finishGEOS_r
def __init__(self): def __init__(self):
# Initializing the context handler for this thread with # Initializing the context handler for this thread with
# the notice and error handler. # the notice and error handler.
self.ptr = lgeos.initGEOS_r(notice_h, error_h) self.ptr = lgeos.initGEOS_r(notice_h, error_h)
def __del__(self):
if self.ptr and lgeos:
lgeos.finishGEOS_r(self.ptr)
# Defining a thread-local object and creating an instance # Defining a thread-local object and creating an instance
# to hold a reference to GEOSContextHandle for this thread. # to hold a reference to GEOSContextHandle for this thread.

38
django/contrib/gis/ptr.py Normal file
View File

@ -0,0 +1,38 @@
from ctypes import c_void_p
class CPointerBase(object):
"""
Base class for objects that have a pointer access property
that controls access to the underlying C pointer.
"""
_ptr = None # Initially the pointer is NULL.
ptr_type = c_void_p
destructor = None
null_ptr_exception_class = AttributeError
@property
def ptr(self):
# Raise an exception if the pointer isn't valid so that NULL pointers
# aren't passed to routines -- that's very bad.
if self._ptr:
return self._ptr
raise self.null_ptr_exception_class('NULL %s pointer encountered.' % self.__class__.__name__)
@ptr.setter
def ptr(self, ptr):
# Only allow the pointer to be set with pointers of the compatible
# type or None (NULL).
if not (ptr is None or isinstance(ptr, self.ptr_type)):
raise TypeError('Incompatible pointer type: %s.' % type(ptr))
self._ptr = ptr
def __del__(self):
"""
Free the memory used by the C++ object.
"""
if self.destructor and self._ptr:
try:
self.destructor(self.ptr)
except (AttributeError, TypeError):
pass # Some part might already have been garbage collected

View File

@ -14,7 +14,6 @@ from django.contrib.gis.geos import (
LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
fromfile, fromstr, fromfile, fromstr,
) )
from django.contrib.gis.geos.base import GEOSBase
from django.contrib.gis.geos.libgeos import geos_version_info from django.contrib.gis.geos.libgeos import geos_version_info
from django.contrib.gis.shortcuts import numpy from django.contrib.gis.shortcuts import numpy
from django.template import Context from django.template import Context
@ -31,52 +30,6 @@ from ..test_data import TestDataMixin
@skipUnless(HAS_GEOS, "Geos is required.") @skipUnless(HAS_GEOS, "Geos is required.")
class GEOSTest(SimpleTestCase, TestDataMixin): class GEOSTest(SimpleTestCase, TestDataMixin):
def test_base(self):
"Tests out the GEOSBase class."
# Testing out GEOSBase class, which provides a `ptr` property
# that abstracts out access to underlying C pointers.
class FakeGeom1(GEOSBase):
pass
# This one only accepts pointers to floats
c_float_p = ctypes.POINTER(ctypes.c_float)
class FakeGeom2(GEOSBase):
ptr_type = c_float_p
# Default ptr_type is `c_void_p`.
fg1 = FakeGeom1()
# Default ptr_type is C float pointer
fg2 = FakeGeom2()
# These assignments are OK -- None is allowed because
# it's equivalent to the NULL pointer.
fg1.ptr = ctypes.c_void_p()
fg1.ptr = None
fg2.ptr = c_float_p(ctypes.c_float(5.23))
fg2.ptr = None
# Because pointers have been set to NULL, an exception should be
# raised when we try to access it. Raising an exception is
# preferable to a segmentation fault that commonly occurs when
# a C method is given a NULL memory reference.
for fg in (fg1, fg2):
# Equivalent to `fg.ptr`
with self.assertRaises(GEOSException):
fg._get_ptr()
# Anything that is either not None or the acceptable pointer type will
# result in a TypeError when trying to assign it to the `ptr` property.
# Thus, memory addresses (integers) and pointers of the incorrect type
# (in `bad_ptrs`) will not be allowed.
bad_ptrs = (5, ctypes.c_char_p(b'foobar'))
for bad_ptr in bad_ptrs:
# Equivalent to `fg.ptr = bad_ptr`
with self.assertRaises(TypeError):
fg1._set_ptr(bad_ptr)
with self.assertRaises(TypeError):
fg2._set_ptr(bad_ptr)
def test_wkt(self): def test_wkt(self):
"Testing WKT output." "Testing WKT output."
for g in self.geometries.wkt_out: for g in self.geometries.wkt_out:

View File

@ -37,7 +37,7 @@ class GEOSIOTest(SimpleTestCase):
# Creating a WKTWriter instance, testing its ptr property. # Creating a WKTWriter instance, testing its ptr property.
wkt_w = WKTWriter() wkt_w = WKTWriter()
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
wkt_w._set_ptr(WKTReader.ptr_type()) wkt_w.ptr = WKTReader.ptr_type()
ref = GEOSGeometry('POINT (5 23)') ref = GEOSGeometry('POINT (5 23)')
ref_wkt = 'POINT (5.0000000000000000 23.0000000000000000)' ref_wkt = 'POINT (5.0000000000000000 23.0000000000000000)'

View File

@ -0,0 +1,65 @@
import ctypes
from django.contrib.gis.ptr import CPointerBase
from django.test import SimpleTestCase, mock
class CPointerBaseTests(SimpleTestCase):
def test(self):
destructor_mock = mock.Mock()
class NullPointerException(Exception):
pass
class FakeGeom1(CPointerBase):
null_ptr_exception_class = NullPointerException
class FakeGeom2(FakeGeom1):
ptr_type = ctypes.POINTER(ctypes.c_float)
destructor = destructor_mock
fg1 = FakeGeom1()
fg2 = FakeGeom2()
# These assignments are OK. None is allowed because it's equivalent
# to the NULL pointer.
fg1.ptr = fg1.ptr_type()
fg1.ptr = None
fg2.ptr = fg2.ptr_type(ctypes.c_float(5.23))
fg2.ptr = None
# Because pointers have been set to NULL, an exception is raised on
# access. Raising an exception is preferable to a segmentation fault
# that commonly occurs when a C method is given a NULL reference.
for fg in (fg1, fg2):
with self.assertRaises(NullPointerException):
fg.ptr
# Anything that's either not None or the acceptable pointer type
# results in a TypeError when trying to assign it to the `ptr` property.
# Thus, memory addresses (integers) and pointers of the incorrect type
# (in `bad_ptrs`) aren't allowed.
bad_ptrs = (5, ctypes.c_char_p(b'foobar'))
for bad_ptr in bad_ptrs:
for fg in (fg1, fg2):
with self.assertRaisesMessage(TypeError, 'Incompatible pointer type'):
fg.ptr = bad_ptr
# Object can be deleted without a destructor set.
fg = FakeGeom1()
fg.ptr = fg.ptr_type(1)
del fg
# A NULL pointer isn't passed to the destructor.
fg = FakeGeom2()
fg.ptr = None
del fg
self.assertFalse(destructor_mock.called)
# The destructor is called if set.
fg = FakeGeom2()
ptr = fg.ptr_type(ctypes.c_float(1.))
fg.ptr = ptr
del fg
destructor_mock.assert_called_with(ptr)