From 61d09e61f5747d7a70268ca8d5e770486877500b Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 Apr 2015 15:45:36 +0200 Subject: [PATCH] Lazy loading of GEOS functions --- .../gis/db/backends/spatialite/features.py | 5 +- django/contrib/gis/geos/libgeos.py | 138 +++++++++++------- .../contrib/gis/geos/prototypes/coordseq.py | 103 +++++++------ .../contrib/gis/geos/prototypes/errcheck.py | 5 +- django/contrib/gis/geos/prototypes/geom.py | 128 ++++++++-------- django/contrib/gis/geos/prototypes/io.py | 126 ++++++++-------- django/contrib/gis/geos/prototypes/misc.py | 34 ++--- .../contrib/gis/geos/prototypes/predicates.py | 57 ++++---- .../contrib/gis/geos/prototypes/prepared.py | 42 +++--- .../contrib/gis/geos/prototypes/topology.py | 73 ++++----- tests/gis_tests/distapp/tests.py | 6 +- 11 files changed, 345 insertions(+), 372 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/features.py b/django/contrib/gis/db/backends/spatialite/features.py index 5e9f3b66f3..f06a9c1818 100644 --- a/django/contrib/gis/db/backends/spatialite/features.py +++ b/django/contrib/gis/db/backends/spatialite/features.py @@ -1,8 +1,7 @@ from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures -from django.contrib.gis.geos import geos_version +from django.contrib.gis.geos import geos_version_info from django.db.backends.sqlite3.features import \ DatabaseFeatures as SQLiteDatabaseFeatures -from django.utils.encoding import force_text from django.utils.functional import cached_property @@ -20,4 +19,4 @@ class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures): @cached_property def supports_3d_storage(self): - return force_text(geos_version()) >= '3.3' + return geos_version_info()['version'] >= '3.3' diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 66c61f3a82..1a74819da8 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -14,51 +14,61 @@ from ctypes.util import find_library from django.contrib.gis.geos.error import GEOSException from django.core.exceptions import ImproperlyConfigured +from django.utils.functional import SimpleLazyObject logger = logging.getLogger('django.contrib.gis') -# Custom library path set? -try: - from django.conf import settings - lib_path = settings.GEOS_LIBRARY_PATH -except (AttributeError, EnvironmentError, - ImportError, ImproperlyConfigured): - lib_path = None -# Setting the appropriate names for the GEOS-C library. -if lib_path: - lib_names = None -elif os.name == 'nt': - # Windows NT libraries - lib_names = ['geos_c', 'libgeos_c-1'] -elif os.name == 'posix': - # *NIX libraries - lib_names = ['geos_c', 'GEOS'] -else: - raise ImportError('Unsupported OS "%s"' % os.name) +def load_geos(): + # Custom library path set? + try: + from django.conf import settings + lib_path = settings.GEOS_LIBRARY_PATH + except (AttributeError, EnvironmentError, + ImportError, ImproperlyConfigured): + lib_path = None -# Using the ctypes `find_library` utility to find the path to the GEOS -# shared library. This is better than manually specifying each library name -# and extension (e.g., libgeos_c.[so|so.1|dylib].). -if lib_names: - for lib_name in lib_names: - lib_path = find_library(lib_name) - if lib_path is not None: - break + # Setting the appropriate names for the GEOS-C library. + if lib_path: + lib_names = None + elif os.name == 'nt': + # Windows NT libraries + lib_names = ['geos_c', 'libgeos_c-1'] + elif os.name == 'posix': + # *NIX libraries + lib_names = ['geos_c', 'GEOS'] + else: + raise ImportError('Unsupported OS "%s"' % os.name) -# No GEOS library could be found. -if lib_path is None: - raise ImportError( - 'Could not find the GEOS library (tried "%s"). ' - 'Try setting GEOS_LIBRARY_PATH in your settings.' % - '", "'.join(lib_names) - ) + # Using the ctypes `find_library` utility to find the path to the GEOS + # shared library. This is better than manually specifying each library name + # and extension (e.g., libgeos_c.[so|so.1|dylib].). + if lib_names: + for lib_name in lib_names: + lib_path = find_library(lib_name) + if lib_path is not None: + break + + # No GEOS library could be found. + if lib_path is None: + raise ImportError( + 'Could not find the GEOS library (tried "%s"). ' + 'Try setting GEOS_LIBRARY_PATH in your settings.' % + '", "'.join(lib_names) + ) + # Getting the GEOS C library. The C interface (CDLL) is used for + # both *NIX and Windows. + # See the GEOS C API source code for more details on the library function calls: + # http://geos.refractions.net/ro/doxygen_docs/html/geos__c_8h-source.html + _lgeos = CDLL(lib_path) + # Here we set up the prototypes for the initGEOS_r and finishGEOS_r + # routines. These functions aren't actually called until they are + # attached to a GEOS context handle -- this actually occurs in + # geos/prototypes/threadsafe.py. + _lgeos.initGEOS_r.restype = CONTEXT_PTR + _lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR] + return _lgeos -# Getting the GEOS C library. The C interface (CDLL) is used for -# both *NIX and Windows. -# See the GEOS C API source code for more details on the library function calls: -# http://geos.refractions.net/ro/doxygen_docs/html/geos__c_8h-source.html -lgeos = CDLL(lib_path) # The notice and error handler C function callback definitions. # Supposed to mimic the GEOS message handler (C below): @@ -120,11 +130,42 @@ def get_pointer_arr(n): GeomArr = GEOM_PTR * n return GeomArr() + +lgeos = SimpleLazyObject(load_geos) + + +class GEOSFuncFactory(object): + argtypes = None + restype = None + errcheck = None + + def __init__(self, func_name, *args, **kwargs): + self.func_name = func_name + self.restype = kwargs.pop('restype', self.restype) + self.errcheck = kwargs.pop('errcheck', self.errcheck) + self.argtypes = kwargs.pop('argtypes', self.argtypes) + self.args = args + self.kwargs = kwargs + self.func = None + + def __call__(self, *args, **kwargs): + if self.func is None: + self.func = self.get_func(*self.args, **self.kwargs) + return self.func(*args, **kwargs) + + def get_func(self, *args, **kwargs): + from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc + func = GEOSFunc(self.func_name) + func.argtypes = self.argtypes or [] + func.restype = self.restype + if self.errcheck: + func.errcheck = self.errcheck + return func + + # Returns the string version of the GEOS library. Have to set the restype # explicitly to c_char_p to ensure compatibility across 32 and 64-bit platforms. -geos_version = lgeos.GEOSversion -geos_version.argtypes = None -geos_version.restype = c_char_p +geos_version = GEOSFuncFactory('GEOSversion', restype=c_char_p) # Regular expression should be able to parse version strings such as # '3.0.0rc4-CAPI-1.3.3', '3.0.0-CAPI-1.4.1', '3.4.0dev-CAPI-1.8.0' or '3.4.0dev-CAPI-1.8.0 r0' @@ -147,18 +188,3 @@ def geos_version_info(): raise GEOSException('Could not parse version info string "%s"' % ver) return {key: m.group(key) for key in ( 'version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor')} - -# Version numbers and whether or not prepared geometry support is available. -_verinfo = geos_version_info() -GEOS_MAJOR_VERSION = int(_verinfo['major']) -GEOS_MINOR_VERSION = int(_verinfo['minor']) -GEOS_SUBMINOR_VERSION = int(_verinfo['subminor']) -del _verinfo -GEOS_VERSION = (GEOS_MAJOR_VERSION, GEOS_MINOR_VERSION, GEOS_SUBMINOR_VERSION) - -# Here we set up the prototypes for the initGEOS_r and finishGEOS_r -# routines. These functions aren't actually called until they are -# attached to a GEOS context handle -- this actually occurs in -# geos/prototypes/threadsafe.py. -lgeos.initGEOS_r.restype = CONTEXT_PTR -lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR] diff --git a/django/contrib/gis/geos/prototypes/coordseq.py b/django/contrib/gis/geos/prototypes/coordseq.py index 1cdc4ccdcc..982ce32613 100644 --- a/django/contrib/gis/geos/prototypes/coordseq.py +++ b/django/contrib/gis/geos/prototypes/coordseq.py @@ -1,23 +1,12 @@ from ctypes import POINTER, c_double, c_int, c_uint -from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR +from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import ( GEOSException, last_arg_byref, ) -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc # ## Error-checking routines specific to coordinate sequences. ## -def check_cs_ptr(result, func, cargs): - "Error checking on routines that return Geometries." - if not result: - raise GEOSException( - 'Error encountered checking Coordinate Sequence returned from GEOS ' - 'C function "%s".' % func.__name__ - ) - return result - - def check_cs_op(result, func, cargs): "Checks the status code of a coordinate sequence operation." if result == 0: @@ -33,63 +22,73 @@ def check_cs_get(result, func, cargs): return last_arg_byref(cargs) -# ## Coordinate sequence prototype generation functions. ## -def cs_int(func): +# ## Coordinate sequence prototype factory classes. ## +class CsInt(GEOSFuncFactory): "For coordinate sequence routines that return an integer." - func.argtypes = [CS_PTR, POINTER(c_uint)] - func.restype = c_int - func.errcheck = check_cs_get - return func + argtypes = [CS_PTR, POINTER(c_uint)] + restype = c_int + errcheck = staticmethod(check_cs_get) -def cs_operation(func, ordinate=False, get=False): +class CsOperation(GEOSFuncFactory): "For coordinate sequence operations." - if get: - # Get routines get double parameter passed-in by reference. - func.errcheck = check_cs_get - dbl_param = POINTER(c_double) - else: - func.errcheck = check_cs_op - dbl_param = c_double + restype = c_int - if ordinate: - # Get/Set ordinate routines have an extra uint parameter. - func.argtypes = [CS_PTR, c_uint, c_uint, dbl_param] - else: - func.argtypes = [CS_PTR, c_uint, dbl_param] + def get_func(self, ordinate=False, get=False): + if get: + # Get routines have double parameter passed-in by reference. + self.errcheck = check_cs_get + dbl_param = POINTER(c_double) + else: + self.errcheck = check_cs_op + dbl_param = c_double - func.restype = c_int - return func + if ordinate: + # Get/Set ordinate routines have an extra uint parameter. + self.argtypes = [CS_PTR, c_uint, c_uint, dbl_param] + else: + self.argtypes = [CS_PTR, c_uint, dbl_param] + return super(CsOperation, self).get_func() -def cs_output(func, argtypes): - "For routines that return a coordinate sequence." - func.argtypes = argtypes - func.restype = CS_PTR - func.errcheck = check_cs_ptr - return func +class CsOutput(GEOSFuncFactory): + restype = CS_PTR + + def get_func(self, argtypes): + self.argtypes = argtypes + return super(CsOutput, self).get_func() + + @staticmethod + def errcheck(result, func, cargs): + if not result: + raise GEOSException( + 'Error encountered checking Coordinate Sequence returned from GEOS ' + 'C function "%s".' % func.__name__ + ) + return result + # ## Coordinate Sequence ctypes prototypes ## # Coordinate Sequence constructors & cloning. -cs_clone = cs_output(GEOSFunc('GEOSCoordSeq_clone'), [CS_PTR]) -create_cs = cs_output(GEOSFunc('GEOSCoordSeq_create'), [c_uint, c_uint]) -get_cs = cs_output(GEOSFunc('GEOSGeom_getCoordSeq'), [GEOM_PTR]) +cs_clone = CsOutput('GEOSCoordSeq_clone', [CS_PTR]) +create_cs = CsOutput('GEOSCoordSeq_create', [c_uint, c_uint]) +get_cs = CsOutput('GEOSGeom_getCoordSeq', [GEOM_PTR]) # Getting, setting ordinate -cs_getordinate = cs_operation(GEOSFunc('GEOSCoordSeq_getOrdinate'), ordinate=True, get=True) -cs_setordinate = cs_operation(GEOSFunc('GEOSCoordSeq_setOrdinate'), ordinate=True) +cs_getordinate = CsOperation('GEOSCoordSeq_getOrdinate', ordinate=True, get=True) +cs_setordinate = CsOperation('GEOSCoordSeq_setOrdinate', ordinate=True) # For getting, x, y, z -cs_getx = cs_operation(GEOSFunc('GEOSCoordSeq_getX'), get=True) -cs_gety = cs_operation(GEOSFunc('GEOSCoordSeq_getY'), get=True) -cs_getz = cs_operation(GEOSFunc('GEOSCoordSeq_getZ'), get=True) +cs_getx = CsOperation('GEOSCoordSeq_getX', get=True) +cs_gety = CsOperation('GEOSCoordSeq_getY', get=True) +cs_getz = CsOperation('GEOSCoordSeq_getZ', get=True) # For setting, x, y, z -cs_setx = cs_operation(GEOSFunc('GEOSCoordSeq_setX')) -cs_sety = cs_operation(GEOSFunc('GEOSCoordSeq_setY')) -cs_setz = cs_operation(GEOSFunc('GEOSCoordSeq_setZ')) +cs_setx = CsOperation('GEOSCoordSeq_setX') +cs_sety = CsOperation('GEOSCoordSeq_setY') +cs_setz = CsOperation('GEOSCoordSeq_setZ') # These routines return size & dimensions. -cs_getsize = cs_int(GEOSFunc('GEOSCoordSeq_getSize')) -cs_getdims = cs_int(GEOSFunc('GEOSCoordSeq_getDimensions')) +cs_getsize = CsInt('GEOSCoordSeq_getSize') +cs_getdims = CsInt('GEOSCoordSeq_getDimensions') diff --git a/django/contrib/gis/geos/prototypes/errcheck.py b/django/contrib/gis/geos/prototypes/errcheck.py index b97440ab01..17748ba41a 100644 --- a/django/contrib/gis/geos/prototypes/errcheck.py +++ b/django/contrib/gis/geos/prototypes/errcheck.py @@ -4,13 +4,12 @@ from ctypes import c_void_p, string_at from django.contrib.gis.geos.error import GEOSException -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc +from django.contrib.gis.geos.libgeos import GEOSFuncFactory # Getting the `free` routine used to free the memory allocated for # string pointers returned by GEOS. -free = GEOSFunc('GEOSFree') +free = GEOSFuncFactory('GEOSFree') free.argtypes = [c_void_p] -free.restype = None def last_arg_byref(args): diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index da2452c169..1d4247f414 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -1,10 +1,9 @@ from ctypes import POINTER, c_char_p, c_int, c_size_t, c_ubyte -from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR +from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import ( check_geom, check_minus_one, check_sized_string, check_string, check_zero, ) -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc # This is the return type used by binary output (WKB, HEX) routines. c_uchar_p = POINTER(c_ubyte) @@ -21,109 +20,96 @@ class geos_char_p(c_char_p): pass -# ### ctypes generation functions ### -def bin_constructor(func): +# ### ctypes factory classes ### +class BinConstructor(GEOSFuncFactory): "Generates a prototype for binary construction (HEX, WKB) GEOS routines." - func.argtypes = [c_char_p, c_size_t] - func.restype = GEOM_PTR - func.errcheck = check_geom - return func + argtypes = [c_char_p, c_size_t] + restype = GEOM_PTR + errcheck = staticmethod(check_geom) # HEX & WKB output -def bin_output(func): +class BinOutput(GEOSFuncFactory): "Generates a prototype for the routines that return a sized string." - func.argtypes = [GEOM_PTR, POINTER(c_size_t)] - func.errcheck = check_sized_string - func.restype = c_uchar_p - return func + argtypes = [GEOM_PTR, POINTER(c_size_t)] + restype = c_uchar_p + errcheck = staticmethod(check_sized_string) -def geom_output(func, argtypes): +class GeomOutput(GEOSFuncFactory): "For GEOS routines that return a geometry." - if argtypes: - func.argtypes = argtypes - func.restype = GEOM_PTR - func.errcheck = check_geom - return func + restype = GEOM_PTR + errcheck = staticmethod(check_geom) + + def get_func(self, argtypes): + self.argtypes = argtypes + return super(GeomOutput, self).get_func() -def geom_index(func): - "For GEOS routines that return geometries from an index." - return geom_output(func, [GEOM_PTR, c_int]) - - -def int_from_geom(func, zero=False): +class IntFromGeom(GEOSFuncFactory): "Argument is a geometry, return type is an integer." - func.argtypes = [GEOM_PTR] - func.restype = c_int - if zero: - func.errcheck = check_zero - else: - func.errcheck = check_minus_one - return func + argtypes = [GEOM_PTR] + restype = c_int + + def get_func(self, zero=False): + if zero: + self.errcheck = check_zero + else: + self.errcheck = check_minus_one + return super(IntFromGeom, self).get_func() -def string_from_geom(func): +class StringFromGeom(GEOSFuncFactory): "Argument is a Geometry, return type is a string." - func.argtypes = [GEOM_PTR] - func.restype = geos_char_p - func.errcheck = check_string - return func + argtypes = [GEOM_PTR] + restype = geos_char_p + errcheck = staticmethod(check_string) + # ### ctypes prototypes ### # Deprecated creation routines from WKB, HEX, WKT -from_hex = bin_constructor(GEOSFunc('GEOSGeomFromHEX_buf')) -from_wkb = bin_constructor(GEOSFunc('GEOSGeomFromWKB_buf')) -from_wkt = geom_output(GEOSFunc('GEOSGeomFromWKT'), [c_char_p]) +from_hex = BinConstructor('GEOSGeomFromHEX_buf') +from_wkb = BinConstructor('GEOSGeomFromWKB_buf') +from_wkt = GeomOutput('GEOSGeomFromWKT', [c_char_p]) # Deprecated output routines -to_hex = bin_output(GEOSFunc('GEOSGeomToHEX_buf')) -to_wkb = bin_output(GEOSFunc('GEOSGeomToWKB_buf')) -to_wkt = string_from_geom(GEOSFunc('GEOSGeomToWKT')) +to_hex = BinOutput('GEOSGeomToHEX_buf') +to_wkb = BinOutput('GEOSGeomToWKB_buf') +to_wkt = StringFromGeom('GEOSGeomToWKT') # The GEOS geometry type, typeid, num_coordites and number of geometries -geos_normalize = int_from_geom(GEOSFunc('GEOSNormalize')) -geos_type = string_from_geom(GEOSFunc('GEOSGeomType')) -geos_typeid = int_from_geom(GEOSFunc('GEOSGeomTypeId')) -get_dims = int_from_geom(GEOSFunc('GEOSGeom_getDimensions'), zero=True) -get_num_coords = int_from_geom(GEOSFunc('GEOSGetNumCoordinates')) -get_num_geoms = int_from_geom(GEOSFunc('GEOSGetNumGeometries')) +geos_normalize = IntFromGeom('GEOSNormalize') +geos_type = StringFromGeom('GEOSGeomType') +geos_typeid = IntFromGeom('GEOSGeomTypeId') +get_dims = IntFromGeom('GEOSGeom_getDimensions', zero=True) +get_num_coords = IntFromGeom('GEOSGetNumCoordinates') +get_num_geoms = IntFromGeom('GEOSGetNumGeometries') # Geometry creation factories -create_point = geom_output(GEOSFunc('GEOSGeom_createPoint'), [CS_PTR]) -create_linestring = geom_output(GEOSFunc('GEOSGeom_createLineString'), [CS_PTR]) -create_linearring = geom_output(GEOSFunc('GEOSGeom_createLinearRing'), [CS_PTR]) +create_point = GeomOutput('GEOSGeom_createPoint', [CS_PTR]) +create_linestring = GeomOutput('GEOSGeom_createLineString', [CS_PTR]) +create_linearring = GeomOutput('GEOSGeom_createLinearRing', [CS_PTR]) # Polygon and collection creation routines are special and will not # have their argument types defined. -create_polygon = geom_output(GEOSFunc('GEOSGeom_createPolygon'), None) -create_collection = geom_output(GEOSFunc('GEOSGeom_createCollection'), None) +create_polygon = GeomOutput('GEOSGeom_createPolygon', None) +create_collection = GeomOutput('GEOSGeom_createCollection', None) # Ring routines -get_extring = geom_output(GEOSFunc('GEOSGetExteriorRing'), [GEOM_PTR]) -get_intring = geom_index(GEOSFunc('GEOSGetInteriorRingN')) -get_nrings = int_from_geom(GEOSFunc('GEOSGetNumInteriorRings')) +get_extring = GeomOutput('GEOSGetExteriorRing', [GEOM_PTR]) +get_intring = GeomOutput('GEOSGetInteriorRingN', [GEOM_PTR, c_int]) +get_nrings = IntFromGeom('GEOSGetNumInteriorRings') # Collection Routines -get_geomn = geom_index(GEOSFunc('GEOSGetGeometryN')) +get_geomn = GeomOutput('GEOSGetGeometryN', [GEOM_PTR, c_int]) # Cloning -geom_clone = GEOSFunc('GEOSGeom_clone') -geom_clone.argtypes = [GEOM_PTR] -geom_clone.restype = GEOM_PTR +geom_clone = GEOSFuncFactory('GEOSGeom_clone', argtypes=[GEOM_PTR], restype=GEOM_PTR) # Destruction routine. -destroy_geom = GEOSFunc('GEOSGeom_destroy') -destroy_geom.argtypes = [GEOM_PTR] -destroy_geom.restype = None +destroy_geom = GEOSFuncFactory('GEOSGeom_destroy', argtypes=[GEOM_PTR]) # SRID routines -geos_get_srid = GEOSFunc('GEOSGetSRID') -geos_get_srid.argtypes = [GEOM_PTR] -geos_get_srid.restype = c_int - -geos_set_srid = GEOSFunc('GEOSSetSRID') -geos_set_srid.argtypes = [GEOM_PTR, c_int] -geos_set_srid.restype = None +geos_get_srid = GEOSFuncFactory('GEOSGetSRID', argtypes=[GEOM_PTR], restype=c_int) +geos_set_srid = GEOSFuncFactory('GEOSSetSRID', argtypes=[GEOM_PTR, c_int]) diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 168d472311..de049e460f 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -2,12 +2,11 @@ import threading from ctypes import POINTER, Structure, byref, c_char, c_char_p, c_int, c_size_t from django.contrib.gis.geos.base import GEOSBase -from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import ( check_geom, check_sized_string, check_string, ) from django.contrib.gis.geos.prototypes.geom import c_uchar_p, geos_char_p -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc from django.utils import six from django.utils.encoding import force_bytes @@ -34,99 +33,90 @@ WKB_READ_PTR = POINTER(WKBReader_st) WKB_WRITE_PTR = POINTER(WKBReader_st) # WKTReader routines -wkt_reader_create = GEOSFunc('GEOSWKTReader_create') -wkt_reader_create.restype = WKT_READ_PTR - -wkt_reader_destroy = GEOSFunc('GEOSWKTReader_destroy') -wkt_reader_destroy.argtypes = [WKT_READ_PTR] - -wkt_reader_read = GEOSFunc('GEOSWKTReader_read') -wkt_reader_read.argtypes = [WKT_READ_PTR, c_char_p] -wkt_reader_read.restype = GEOM_PTR -wkt_reader_read.errcheck = check_geom +wkt_reader_create = GEOSFuncFactory('GEOSWKTReader_create', restype=WKT_READ_PTR) +wkt_reader_destroy = GEOSFuncFactory('GEOSWKTReader_destroy', argtypes=[WKT_READ_PTR]) +wkt_reader_read = GEOSFuncFactory( + 'GEOSWKTReader_read', argtypes=[WKT_READ_PTR, c_char_p], restype=GEOM_PTR, errcheck=check_geom +) # WKTWriter routines -wkt_writer_create = GEOSFunc('GEOSWKTWriter_create') -wkt_writer_create.restype = WKT_WRITE_PTR +wkt_writer_create = GEOSFuncFactory('GEOSWKTWriter_create', restype=WKT_WRITE_PTR) +wkt_writer_destroy = GEOSFuncFactory('GEOSWKTWriter_destroy', argtypes=[WKT_WRITE_PTR]) -wkt_writer_destroy = GEOSFunc('GEOSWKTWriter_destroy') -wkt_writer_destroy.argtypes = [WKT_WRITE_PTR] +wkt_writer_write = GEOSFuncFactory( + 'GEOSWKTWriter_write', argtypes=[WKT_WRITE_PTR, GEOM_PTR], restype=geos_char_p, errcheck=check_string +) -wkt_writer_write = GEOSFunc('GEOSWKTWriter_write') -wkt_writer_write.argtypes = [WKT_WRITE_PTR, GEOM_PTR] -wkt_writer_write.restype = geos_char_p -wkt_writer_write.errcheck = check_string -try: - wkt_writer_get_outdim = GEOSFunc('GEOSWKTWriter_getOutputDimension') - wkt_writer_get_outdim.argtypes = [WKT_WRITE_PTR] - wkt_writer_get_outdim.restype = c_int - wkt_writer_set_outdim = GEOSFunc('GEOSWKTWriter_setOutputDimension') - wkt_writer_set_outdim.argtypes = [WKT_WRITE_PTR, c_int] -except AttributeError: - # GEOSWKTWriter_get/setOutputDimension has been introduced in GEOS 3.3.0 - # Always return 2 if not available - wkt_writer_get_outdim = lambda ptr: 2 - wkt_writer_set_outdim = lambda ptr, dim: None +class WKTOutputDim(GEOSFuncFactory): + def get_func(self, *args, **kwargs): + try: + return super(WKTOutputDim, self).get_func(*args, **kwargs) + except AttributeError: + # GEOSWKTWriter_get/setOutputDimension has been introduced in GEOS 3.3.0 + # Always return 2 if not available + return { + 'GEOSWKTWriter_getOutputDimension': lambda ptr: 2, + 'GEOSWKTWriter_setOutputDimension': lambda ptr, dim: None, + }.get(self.func_name) + +wkt_writer_get_outdim = WKTOutputDim( + 'GEOSWKTWriter_getOutputDimension', argtypes=[WKT_WRITE_PTR], restype=c_int +) +wkt_writer_set_outdim = WKTOutputDim( + 'GEOSWKTWriter_setOutputDimension', argtypes=[WKT_WRITE_PTR, c_int] +) # WKBReader routines -wkb_reader_create = GEOSFunc('GEOSWKBReader_create') -wkb_reader_create.restype = WKB_READ_PTR - -wkb_reader_destroy = GEOSFunc('GEOSWKBReader_destroy') -wkb_reader_destroy.argtypes = [WKB_READ_PTR] +wkb_reader_create = GEOSFuncFactory('GEOSWKBReader_create', restype=WKB_READ_PTR) +wkb_reader_destroy = GEOSFuncFactory('GEOSWKBReader_destroy', argtypes=[WKB_READ_PTR]) -def wkb_read_func(func): +class WKBReadFunc(GEOSFuncFactory): # Although the function definitions take `const unsigned char *` # as their parameter, we use c_char_p here so the function may # take Python strings directly as parameters. Inside Python there # is not a difference between signed and unsigned characters, so # it is not a problem. - func.argtypes = [WKB_READ_PTR, c_char_p, c_size_t] - func.restype = GEOM_PTR - func.errcheck = check_geom - return func + argtypes = [WKB_READ_PTR, c_char_p, c_size_t] + restype = GEOM_PTR + errcheck = staticmethod(check_geom) -wkb_reader_read = wkb_read_func(GEOSFunc('GEOSWKBReader_read')) -wkb_reader_read_hex = wkb_read_func(GEOSFunc('GEOSWKBReader_readHEX')) + +wkb_reader_read = WKBReadFunc('GEOSWKBReader_read') +wkb_reader_read_hex = WKBReadFunc('GEOSWKBReader_readHEX') # WKBWriter routines -wkb_writer_create = GEOSFunc('GEOSWKBWriter_create') -wkb_writer_create.restype = WKB_WRITE_PTR - -wkb_writer_destroy = GEOSFunc('GEOSWKBWriter_destroy') -wkb_writer_destroy.argtypes = [WKB_WRITE_PTR] +wkb_writer_create = GEOSFuncFactory('GEOSWKBWriter_create', restype=WKB_WRITE_PTR) +wkb_writer_destroy = GEOSFuncFactory('GEOSWKBWriter_destroy', argtypes=[WKB_WRITE_PTR]) # WKB Writing prototypes. -def wkb_write_func(func): - func.argtypes = [WKB_WRITE_PTR, GEOM_PTR, POINTER(c_size_t)] - func.restype = c_uchar_p - func.errcheck = check_sized_string - return func +class WKBWriteFunc(GEOSFuncFactory): + argtypes = [WKB_WRITE_PTR, GEOM_PTR, POINTER(c_size_t)] + restype = c_uchar_p + errcheck = staticmethod(check_sized_string) -wkb_writer_write = wkb_write_func(GEOSFunc('GEOSWKBWriter_write')) -wkb_writer_write_hex = wkb_write_func(GEOSFunc('GEOSWKBWriter_writeHEX')) + +wkb_writer_write = WKBWriteFunc('GEOSWKBWriter_write') +wkb_writer_write_hex = WKBWriteFunc('GEOSWKBWriter_writeHEX') # WKBWriter property getter/setter prototypes. -def wkb_writer_get(func, restype=c_int): - func.argtypes = [WKB_WRITE_PTR] - func.restype = restype - return func +class WKBWriterGet(GEOSFuncFactory): + argtypes = [WKB_WRITE_PTR] + restype = c_int -def wkb_writer_set(func, argtype=c_int): - func.argtypes = [WKB_WRITE_PTR, argtype] - return func +class WKBWriterSet(GEOSFuncFactory): + argtypes = [WKB_WRITE_PTR, c_int] -wkb_writer_get_byteorder = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getByteOrder')) -wkb_writer_set_byteorder = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setByteOrder')) -wkb_writer_get_outdim = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getOutputDimension')) -wkb_writer_set_outdim = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setOutputDimension')) -wkb_writer_get_include_srid = wkb_writer_get(GEOSFunc('GEOSWKBWriter_getIncludeSRID'), restype=c_char) -wkb_writer_set_include_srid = wkb_writer_set(GEOSFunc('GEOSWKBWriter_setIncludeSRID'), argtype=c_char) +wkb_writer_get_byteorder = WKBWriterGet('GEOSWKBWriter_getByteOrder') +wkb_writer_set_byteorder = WKBWriterSet('GEOSWKBWriter_setByteOrder') +wkb_writer_get_outdim = WKBWriterGet('GEOSWKBWriter_getOutputDimension') +wkb_writer_set_outdim = WKBWriterSet('GEOSWKBWriter_setOutputDimension') +wkb_writer_get_include_srid = WKBWriterGet('GEOSWKBWriter_getIncludeSRID', restype=c_char) +wkb_writer_set_include_srid = WKBWriterSet('GEOSWKBWriter_setIncludeSRID', argtypes=[WKB_WRITE_PTR, c_char]) # ### Base I/O Class ### diff --git a/django/contrib/gis/geos/prototypes/misc.py b/django/contrib/gis/geos/prototypes/misc.py index 0a8b388618..4a82888d09 100644 --- a/django/contrib/gis/geos/prototypes/misc.py +++ b/django/contrib/gis/geos/prototypes/misc.py @@ -4,35 +4,35 @@ """ from ctypes import POINTER, c_double, c_int -from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import check_dbl, check_string from django.contrib.gis.geos.prototypes.geom import geos_char_p -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc from django.utils.six.moves import range __all__ = ['geos_area', 'geos_distance', 'geos_length', 'geos_isvalidreason'] -# ### ctypes generator function ### -def dbl_from_geom(func, num_geom=1): +class DblFromGeom(GEOSFuncFactory): """ Argument is a Geometry, return type is double that is passed in by reference as the last argument. """ - argtypes = [GEOM_PTR for i in range(num_geom)] - argtypes += [POINTER(c_double)] - func.argtypes = argtypes - func.restype = c_int # Status code returned - func.errcheck = check_dbl - return func + restype = c_int # Status code returned + errcheck = staticmethod(check_dbl) + + def get_func(self, num_geom=1): + argtypes = [GEOM_PTR for i in range(num_geom)] + argtypes += [POINTER(c_double)] + self.argtypes = argtypes + return super(DblFromGeom, self).get_func() + # ### ctypes prototypes ### # Area, distance, and length prototypes. -geos_area = dbl_from_geom(GEOSFunc('GEOSArea')) -geos_distance = dbl_from_geom(GEOSFunc('GEOSDistance'), num_geom=2) -geos_length = dbl_from_geom(GEOSFunc('GEOSLength')) -geos_isvalidreason = GEOSFunc('GEOSisValidReason') -geos_isvalidreason.argtypes = [GEOM_PTR] -geos_isvalidreason.restype = geos_char_p -geos_isvalidreason.errcheck = check_string +geos_area = DblFromGeom('GEOSArea') +geos_distance = DblFromGeom('GEOSDistance', num_geom=2) +geos_length = DblFromGeom('GEOSLength') +geos_isvalidreason = GEOSFuncFactory( + 'GEOSisValidReason', restype=geos_char_p, errcheck=check_string, argtypes=[GEOM_PTR] +) diff --git a/django/contrib/gis/geos/prototypes/predicates.py b/django/contrib/gis/geos/prototypes/predicates.py index 6ef979422c..e1b85f4e35 100644 --- a/django/contrib/gis/geos/prototypes/predicates.py +++ b/django/contrib/gis/geos/prototypes/predicates.py @@ -4,45 +4,38 @@ """ from ctypes import c_char, c_char_p, c_double -from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import check_predicate -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc -# ## Binary & unary predicate functions ## -def binary_predicate(func, *args): +# ## Binary & unary predicate factories ## +class UnaryPredicate(GEOSFuncFactory): + "For GEOS unary predicate functions." + argtypes = [GEOM_PTR] + restype = c_char + errcheck = staticmethod(check_predicate) + + +class BinaryPredicate(UnaryPredicate): "For GEOS binary predicate functions." argtypes = [GEOM_PTR, GEOM_PTR] - if args: - argtypes += args - func.argtypes = argtypes - func.restype = c_char - func.errcheck = check_predicate - return func -def unary_predicate(func): - "For GEOS unary predicate functions." - func.argtypes = [GEOM_PTR] - func.restype = c_char - func.errcheck = check_predicate - return func - # ## Unary Predicates ## -geos_hasz = unary_predicate(GEOSFunc('GEOSHasZ')) -geos_isempty = unary_predicate(GEOSFunc('GEOSisEmpty')) -geos_isring = unary_predicate(GEOSFunc('GEOSisRing')) -geos_issimple = unary_predicate(GEOSFunc('GEOSisSimple')) -geos_isvalid = unary_predicate(GEOSFunc('GEOSisValid')) +geos_hasz = UnaryPredicate('GEOSHasZ') +geos_isempty = UnaryPredicate('GEOSisEmpty') +geos_isring = UnaryPredicate('GEOSisRing') +geos_issimple = UnaryPredicate('GEOSisSimple') +geos_isvalid = UnaryPredicate('GEOSisValid') # ## Binary Predicates ## -geos_contains = binary_predicate(GEOSFunc('GEOSContains')) -geos_crosses = binary_predicate(GEOSFunc('GEOSCrosses')) -geos_disjoint = binary_predicate(GEOSFunc('GEOSDisjoint')) -geos_equals = binary_predicate(GEOSFunc('GEOSEquals')) -geos_equalsexact = binary_predicate(GEOSFunc('GEOSEqualsExact'), c_double) -geos_intersects = binary_predicate(GEOSFunc('GEOSIntersects')) -geos_overlaps = binary_predicate(GEOSFunc('GEOSOverlaps')) -geos_relatepattern = binary_predicate(GEOSFunc('GEOSRelatePattern'), c_char_p) -geos_touches = binary_predicate(GEOSFunc('GEOSTouches')) -geos_within = binary_predicate(GEOSFunc('GEOSWithin')) +geos_contains = BinaryPredicate('GEOSContains') +geos_crosses = BinaryPredicate('GEOSCrosses') +geos_disjoint = BinaryPredicate('GEOSDisjoint') +geos_equals = BinaryPredicate('GEOSEquals') +geos_equalsexact = BinaryPredicate('GEOSEqualsExact', argtypes=[GEOM_PTR, GEOM_PTR, c_double]) +geos_intersects = BinaryPredicate('GEOSIntersects') +geos_overlaps = BinaryPredicate('GEOSOverlaps') +geos_relatepattern = BinaryPredicate('GEOSRelatePattern', argtypes=[GEOM_PTR, GEOM_PTR, c_char_p]) +geos_touches = BinaryPredicate('GEOSTouches') +geos_within = BinaryPredicate('GEOSWithin') diff --git a/django/contrib/gis/geos/prototypes/prepared.py b/django/contrib/gis/geos/prototypes/prepared.py index 2c93e1eb2f..c3e2f1838a 100644 --- a/django/contrib/gis/geos/prototypes/prepared.py +++ b/django/contrib/gis/geos/prototypes/prepared.py @@ -1,36 +1,30 @@ from ctypes import c_char from django.contrib.gis.geos.libgeos import ( - GEOM_PTR, PREPGEOM_PTR, geos_version_info, + GEOM_PTR, PREPGEOM_PTR, GEOSFuncFactory, ) from django.contrib.gis.geos.prototypes.errcheck import check_predicate -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc # Prepared geometry constructor and destructors. -geos_prepare = GEOSFunc('GEOSPrepare') -geos_prepare.argtypes = [GEOM_PTR] -geos_prepare.restype = PREPGEOM_PTR - -prepared_destroy = GEOSFunc('GEOSPreparedGeom_destroy') -prepared_destroy.argtpes = [PREPGEOM_PTR] -prepared_destroy.restype = None +geos_prepare = GEOSFuncFactory('GEOSPrepare', argtypes=[GEOM_PTR], restype=PREPGEOM_PTR) +prepared_destroy = GEOSFuncFactory('GEOSPreparedGeom_destroy', argtpes=[PREPGEOM_PTR]) # Prepared geometry binary predicate support. -def prepared_predicate(func): - func.argtypes = [PREPGEOM_PTR, GEOM_PTR] - func.restype = c_char - func.errcheck = check_predicate - return func +class PreparedPredicate(GEOSFuncFactory): + argtypes = [PREPGEOM_PTR, GEOM_PTR] + restype = c_char + errcheck = staticmethod(check_predicate) -prepared_contains = prepared_predicate(GEOSFunc('GEOSPreparedContains')) -prepared_contains_properly = prepared_predicate(GEOSFunc('GEOSPreparedContainsProperly')) -prepared_covers = prepared_predicate(GEOSFunc('GEOSPreparedCovers')) -prepared_intersects = prepared_predicate(GEOSFunc('GEOSPreparedIntersects')) -if geos_version_info()['version'] > '3.3.0': - prepared_crosses = prepared_predicate(GEOSFunc('GEOSPreparedCrosses')) - prepared_disjoint = prepared_predicate(GEOSFunc('GEOSPreparedDisjoint')) - prepared_overlaps = prepared_predicate(GEOSFunc('GEOSPreparedOverlaps')) - prepared_touches = prepared_predicate(GEOSFunc('GEOSPreparedTouches')) - prepared_within = prepared_predicate(GEOSFunc('GEOSPreparedWithin')) +prepared_contains = PreparedPredicate('GEOSPreparedContains') +prepared_contains_properly = PreparedPredicate('GEOSPreparedContainsProperly') +prepared_covers = PreparedPredicate('GEOSPreparedCovers') +prepared_intersects = PreparedPredicate('GEOSPreparedIntersects') + +# Functions added in GEOS 3.3 +prepared_crosses = PreparedPredicate('GEOSPreparedCrosses') +prepared_disjoint = PreparedPredicate('GEOSPreparedDisjoint') +prepared_overlaps = PreparedPredicate('GEOSPreparedOverlaps') +prepared_touches = PreparedPredicate('GEOSPreparedTouches') +prepared_within = PreparedPredicate('GEOSPreparedWithin') diff --git a/django/contrib/gis/geos/prototypes/topology.py b/django/contrib/gis/geos/prototypes/topology.py index 5729256d9d..fa7af0e066 100644 --- a/django/contrib/gis/geos/prototypes/topology.py +++ b/django/contrib/gis/geos/prototypes/topology.py @@ -2,64 +2,51 @@ This module houses the GEOS ctypes prototype functions for the topological operations on geometries. """ -__all__ = ['geos_boundary', 'geos_buffer', 'geos_cascaded_union', - 'geos_centroid', 'geos_convexhull', 'geos_difference', - 'geos_envelope', 'geos_intersection', 'geos_linemerge', - 'geos_pointonsurface', 'geos_preservesimplify', 'geos_simplify', - 'geos_symdifference', 'geos_union', 'geos_relate', - 'geos_project', 'geos_interpolate', 'geos_project_normalized', - 'geos_interpolate_normalized'] - from ctypes import c_double, c_int -from django.contrib.gis.geos.libgeos import GEOM_PTR +from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOSFuncFactory from django.contrib.gis.geos.prototypes.errcheck import ( check_geom, check_minus_one, check_string, ) from django.contrib.gis.geos.prototypes.geom import geos_char_p -from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc -def topology(func, *args, **kwargs): +class Topology(GEOSFuncFactory): "For GEOS unary topology functions." argtypes = [GEOM_PTR] - if args: - argtypes += args - func.argtypes = argtypes - func.restype = kwargs.get('restype', GEOM_PTR) - func.errcheck = kwargs.get('errcheck', check_geom) - return func + restype = GEOM_PTR + errcheck = staticmethod(check_geom) + # Topology Routines -geos_boundary = topology(GEOSFunc('GEOSBoundary')) -geos_buffer = topology(GEOSFunc('GEOSBuffer'), c_double, c_int) -geos_centroid = topology(GEOSFunc('GEOSGetCentroid')) -geos_convexhull = topology(GEOSFunc('GEOSConvexHull')) -geos_difference = topology(GEOSFunc('GEOSDifference'), GEOM_PTR) -geos_envelope = topology(GEOSFunc('GEOSEnvelope')) -geos_intersection = topology(GEOSFunc('GEOSIntersection'), GEOM_PTR) -geos_linemerge = topology(GEOSFunc('GEOSLineMerge')) -geos_pointonsurface = topology(GEOSFunc('GEOSPointOnSurface')) -geos_preservesimplify = topology(GEOSFunc('GEOSTopologyPreserveSimplify'), c_double) -geos_simplify = topology(GEOSFunc('GEOSSimplify'), c_double) -geos_symdifference = topology(GEOSFunc('GEOSSymDifference'), GEOM_PTR) -geos_union = topology(GEOSFunc('GEOSUnion'), GEOM_PTR) +geos_boundary = Topology('GEOSBoundary') +geos_buffer = Topology('GEOSBuffer', argtypes=[GEOM_PTR, c_double, c_int]) +geos_centroid = Topology('GEOSGetCentroid') +geos_convexhull = Topology('GEOSConvexHull') +geos_difference = Topology('GEOSDifference', argtypes=[GEOM_PTR, GEOM_PTR]) +geos_envelope = Topology('GEOSEnvelope') +geos_intersection = Topology('GEOSIntersection', argtypes=[GEOM_PTR, GEOM_PTR]) +geos_linemerge = Topology('GEOSLineMerge') +geos_pointonsurface = Topology('GEOSPointOnSurface') +geos_preservesimplify = Topology('GEOSTopologyPreserveSimplify', argtypes=[GEOM_PTR, c_double]) +geos_simplify = Topology('GEOSSimplify', argtypes=[GEOM_PTR, c_double]) +geos_symdifference = Topology('GEOSSymDifference', argtypes=[GEOM_PTR, GEOM_PTR]) +geos_union = Topology('GEOSUnion', argtypes=[GEOM_PTR, GEOM_PTR]) -geos_cascaded_union = GEOSFunc('GEOSUnionCascaded') -geos_cascaded_union.argtypes = [GEOM_PTR] -geos_cascaded_union.restype = GEOM_PTR +geos_cascaded_union = GEOSFuncFactory('GEOSUnionCascaded', argtypes=[GEOM_PTR], restype=GEOM_PTR) # GEOSRelate returns a string, not a geometry. -geos_relate = GEOSFunc('GEOSRelate') -geos_relate.argtypes = [GEOM_PTR, GEOM_PTR] -geos_relate.restype = geos_char_p -geos_relate.errcheck = check_string +geos_relate = GEOSFuncFactory( + 'GEOSRelate', argtypes=[GEOM_PTR, GEOM_PTR], restype=geos_char_p, errcheck=check_string +) # Linear referencing routines -geos_project = topology(GEOSFunc('GEOSProject'), GEOM_PTR, - restype=c_double, errcheck=check_minus_one) -geos_interpolate = topology(GEOSFunc('GEOSInterpolate'), c_double) +geos_project = GEOSFuncFactory( + 'GEOSProject', argtypes=[GEOM_PTR, GEOM_PTR], restype=c_double, errcheck=check_minus_one +) +geos_interpolate = Topology('GEOSInterpolate', argtypes=[GEOM_PTR, c_double]) -geos_project_normalized = topology(GEOSFunc('GEOSProjectNormalized'), - GEOM_PTR, restype=c_double, errcheck=check_minus_one) -geos_interpolate_normalized = topology(GEOSFunc('GEOSInterpolateNormalized'), c_double) +geos_project_normalized = GEOSFuncFactory( + 'GEOSProjectNormalized', argtypes=[GEOM_PTR, GEOM_PTR], restype=c_double, errcheck=check_minus_one +) +geos_interpolate_normalized = Topology('GEOSInterpolateNormalized', argtypes=[GEOM_PTR, c_double]) diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py index 077675f3ee..6638b9ed00 100644 --- a/tests/gis_tests/distapp/tests.py +++ b/tests/gis_tests/distapp/tests.py @@ -23,14 +23,14 @@ if HAS_GEOS: class DistanceTest(TestCase): fixtures = ['initial'] - if HAS_GEOS: + def setUp(self): # A point we are testing distances with -- using a WGS84 # coordinate that'll be implicitly transformed to that to # the coordinate system of the field, EPSG:32140 (Texas South Central # w/units in meters) - stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326) + self.stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326) # Another one for Australia - au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) + self.au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) def get_names(self, qs): cities = [c.name for c in qs]