[Git][debian-gis-team/pyshp][upstream] New upstream version 2.1.1+ds

Bas Couwenberg gitlab at salsa.debian.org
Thu Sep 10 05:05:44 BST 2020



Bas Couwenberg pushed to branch upstream at Debian GIS Project / pyshp


Commits:
87249afd by Bas Couwenberg at 2020-09-10T05:55:50+02:00
New upstream version 2.1.1+ds
- - - - -


21 changed files:

- PKG-INFO
- README.md
- changelog.txt
- + requirements.test.txt
- setup.py
- shapefile.py
- shapefiles/test/balancing.dbf
- shapefiles/test/contextwriter.dbf
- shapefiles/test/dtype.dbf
- shapefiles/test/line.dbf
- shapefiles/test/linem.dbf
- shapefiles/test/linez.dbf
- shapefiles/test/multipatch.dbf
- shapefiles/test/multipoint.dbf
- shapefiles/test/onlydbf.dbf
- shapefiles/test/point.dbf
- shapefiles/test/polygon.dbf
- shapefiles/test/polygon.shp
- shapefiles/test/polygon.shx
- shapefiles/test/shapetype.dbf
- shapefiles/test/testfile.dbf


Changes:

=====================================
PKG-INFO
=====================================
@@ -1,12 +1,12 @@
 Metadata-Version: 2.1
 Name: pyshp
-Version: 2.1.0
+Version: 2.1.1
 Summary: Pure Python read/write support for ESRI Shapefile format
 Home-page: https://github.com/GeospatialPython/pyshp
 Author: Joel Lawhead
 Author-email: jlawhead at geospatialpython.com
 License: MIT
-Download-URL: https://github.com/GeospatialPython/pyshp/archive/2.1.0.tar.gz
+Download-URL: https://github.com/GeospatialPython/pyshp/archive/2.1.1.tar.gz
 Description: # PyShp
         
         The Python Shapefile Library (PyShp) reads and writes ESRI Shapefiles in pure Python.
@@ -23,16 +23,12 @@ Description: # PyShp
         
         [Examples](#examples)
         - [Reading Shapefiles](#reading-shapefiles)
-          - [Reading Shapefiles Using the Context Manager](#reading-shapefiles-using-the-context-manager)
-          - [Reading Shapefiles from File-Like Objects](#reading-shapefiles-from-file-like-objects)
-          - [Reading Shapefile Meta-Data](#reading-shapefile-meta-data)
+          - [The Reader Class](#the-reader-class)
           - [Reading Geometry](#reading-geometry)
           - [Reading Records](#reading-records)
           - [Reading Geometry and Records Simultaneously](#reading-geometry-and-records-simultaneously)
         - [Writing Shapefiles](#writing-shapefiles)
-          - [Writing Shapefiles Using the Context Manager](#writing-shapefiles-using-the-context-manager)
-          - [Writing Shapefiles to File-Like Objects](#writing-shapefiles-to-file-like-objects)
-          - [Setting the Shape Type](#setting-the-shape-type)
+          - [The Writer Class](#the-writer-class)
           - [Adding Records](#adding-records)
           - [Adding Geometry](#adding-geometry)
           - [Geometry and Record Balancing](#geometry-and-record-balancing)
@@ -83,9 +79,27 @@ Description: # PyShp
         
         # Version Changes
         
+        ## 2.1.1
+        
+        ### Improvements:
+        
+        - Handle shapes with no coords and represent as geojson with no coords (GeoJSON null-equivalent)
+        - Expand testing to Python 3.6, 3.7, 3.8 and PyPy; drop 3.3 and 3.4 [@mwtoews]
+        - Added pytest testing [@jmoujaes]
+        
+        ### Bug fixes:
+        
+        - Fix incorrect geo interface handling of multipolygons with complex exterior-hole relations [see #202]
+        - Enforce shapefile requirement of at least one field, to avoid writing invalid shapefiles [@Jonty]
+        - Fix Reader geo interface including DeletionFlag field in feature properties [@nnseva]
+        - Fix polygons not being auto closed, which was accidentally dropped
+        - Fix error for null geometries in feature geojson
+        - Misc docstring cleanup [@fiveham]
+        
         ## 2.1.0
         
         ### New Features:
+        
         - Added back read/write support for unicode field names. 
         - Improved Record representation
         - More support for geojson on Reader, ShapeRecord, ShapeRecords, and shapes()
@@ -149,6 +163,8 @@ Description: # PyShp
         
         ## Reading Shapefiles
         
+        ### The Reader Class
+        
         To read a shapefile create a new "Reader" object and pass it the name of an
         existing shapefile. The shapefile format is actually a collection of three
         files. You specify the base filename of the shapefile or the complete filename
@@ -170,7 +186,7 @@ Description: # PyShp
         OR any of the other 5+ formats which are potentially part of a shapefile. The
         library does not care about file extensions.
         
-        ### Reading Shapefiles Using the Context Manager
+        #### Reading Shapefiles Using the Context Manager
         
         The "Reader" class can be used as a context manager, to ensure open file
         objects are properly closed when done reading the data:
@@ -181,7 +197,7 @@ Description: # PyShp
                 663 shapes (type 'POLYGON')
                 663 records (44 fields)
         
-        ### Reading Shapefiles from File-Like Objects
+        #### Reading Shapefiles from File-Like Objects
         
         You can also load shapefiles from any Python file-like object using keyword
         arguments to specify any of the three files. This feature is very powerful and
@@ -199,7 +215,7 @@ Description: # PyShp
         shx file to access shape records a little faster but will do just fine without
         it.
         
-        ### Reading Shapefile Meta-Data
+        #### Reading Shapefile Meta-Data
         
         Shapefiles have a number of attributes for inspecting the file contents.
         A shapefile is a container for a specific type of geometry, and this can be checked using the 
@@ -514,6 +530,8 @@ Description: # PyShp
         
         ## Writing Shapefiles
         
+        ### The Writer Class
+        
         PyShp tries to be as flexible as possible when writing shapefiles while
         maintaining some degree of automatic validation to make sure you don't
         accidentally write an invalid file.
@@ -533,6 +551,7 @@ Description: # PyShp
         
         
         	>>> w = shapefile.Writer('shapefiles/test/testfile')
+        	>>> w.field('field1', 'C')
         	
         File extensions are optional when reading or writing shapefiles. If you specify
         them PyShp ignores them anyway. When you save files you can specify a base
@@ -541,11 +560,12 @@ Description: # PyShp
         
         
         	>>> w = shapefile.Writer(dbf='shapefiles/test/onlydbf.dbf')
+        	>>> w.field('field1', 'C')
         	
         In that case, any file types not assigned will not
         save and only file types with file names will be saved. 
         
-        ### Writing Shapefiles Using the Context Manager
+        #### Writing Shapefiles Using the Context Manager
         
         The "Writer" class automatically closes the open files and writes the final headers once it is garbage collected.
         In case of a crash and to make the code more readable, it is nevertheless recommended 
@@ -558,10 +578,11 @@ Description: # PyShp
         objects are properly closed and final headers written once you exit the with-clause:
         
         
-        	>>> with shapefile.Writer("shapefiles/test/contextwriter") as shp:
-        	...		pass
+        	>>> with shapefile.Writer("shapefiles/test/contextwriter") as w:
+        	... 	w.field('field1', 'C')
+        	... 	pass
         
-        ### Writing Shapefiles to File-Like Objects
+        #### Writing Shapefiles to File-Like Objects
         
         Just as you can read shapefiles from python file-like objects you can also
         write to them:
@@ -581,7 +602,7 @@ Description: # PyShp
         	>>> w.close()
         	>>> # To read back the files you could call the "StringIO.getvalue()" method later.
         	
-        ### Setting the Shape Type
+        #### Setting the Shape Type
         
         The shape type defines the type of geometry contained in the shapefile. All of
         the shapes must match the shape type setting.
@@ -595,6 +616,7 @@ Description: # PyShp
         
         
         	>>> w = shapefile.Writer('shapefiles/test/shapetype', shapeType=3)
+        	>>> w.field('field1', 'C')
         
         	>>> w.shapeType
         	3
@@ -809,9 +831,9 @@ Description: # PyShp
         	>>> w.field('name', 'C')
         
         	>>> w.poly([
-        	...	        [[122,37], [117,36], [115,32], [118,20], [113,24]], # poly 1
-        	...	        [[15,2], [17,6], [22,7]], # hole 1
-        	...         [[122,37], [117,36], [115,32]] # poly 2
+        	...	        [[113,24], [112,32], [117,36], [122,37], [118,20]], # poly 1
+        	...	        [[116,29],[116,26],[119,29],[119,32]], # hole 1
+        	...         [[15,2], [17,6], [22,7]]  # poly 2
         	...        ])
         	>>> w.record('polygon1')
         	
@@ -1105,10 +1127,12 @@ Description: # PyShp
         davidh-ssec
         Evan Heidtmann
         ezcitron
+        fiveham
         geospatialpython
         Hannes
         Ignacio Martinez Vazquez
         Jason Moujaes
+        Jonty Wareing
         Karim Bahgat
         Kyle Kelley
         Louis Tiao
@@ -1127,6 +1151,7 @@ Description: # PyShp
         Tobias Megies
         Tommi Penttinen
         Uli Köhler
+        Vsevolod Novikov
         Zac Miller
         ```
         
@@ -1135,6 +1160,10 @@ Platform: UNKNOWN
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
 Classifier: Topic :: Scientific/Engineering :: GIS
 Classifier: Topic :: Software Development :: Libraries
 Classifier: Topic :: Software Development :: Libraries :: Python Modules


=====================================
README.md
=====================================
@@ -14,16 +14,12 @@ The Python Shapefile Library (PyShp) reads and writes ESRI Shapefiles in pure Py
 
 [Examples](#examples)
 - [Reading Shapefiles](#reading-shapefiles)
-  - [Reading Shapefiles Using the Context Manager](#reading-shapefiles-using-the-context-manager)
-  - [Reading Shapefiles from File-Like Objects](#reading-shapefiles-from-file-like-objects)
-  - [Reading Shapefile Meta-Data](#reading-shapefile-meta-data)
+  - [The Reader Class](#the-reader-class)
   - [Reading Geometry](#reading-geometry)
   - [Reading Records](#reading-records)
   - [Reading Geometry and Records Simultaneously](#reading-geometry-and-records-simultaneously)
 - [Writing Shapefiles](#writing-shapefiles)
-  - [Writing Shapefiles Using the Context Manager](#writing-shapefiles-using-the-context-manager)
-  - [Writing Shapefiles to File-Like Objects](#writing-shapefiles-to-file-like-objects)
-  - [Setting the Shape Type](#setting-the-shape-type)
+  - [The Writer Class](#the-writer-class)
   - [Adding Records](#adding-records)
   - [Adding Geometry](#adding-geometry)
   - [Geometry and Record Balancing](#geometry-and-record-balancing)
@@ -74,9 +70,27 @@ part of your geospatial project.
 
 # Version Changes
 
+## 2.1.1
+
+### Improvements:
+
+- Handle shapes with no coords and represent as geojson with no coords (GeoJSON null-equivalent)
+- Expand testing to Python 3.6, 3.7, 3.8 and PyPy; drop 3.3 and 3.4 [@mwtoews]
+- Added pytest testing [@jmoujaes]
+
+### Bug fixes:
+
+- Fix incorrect geo interface handling of multipolygons with complex exterior-hole relations [see #202]
+- Enforce shapefile requirement of at least one field, to avoid writing invalid shapefiles [@Jonty]
+- Fix Reader geo interface including DeletionFlag field in feature properties [@nnseva]
+- Fix polygons not being auto closed, which was accidentally dropped
+- Fix error for null geometries in feature geojson
+- Misc docstring cleanup [@fiveham]
+
 ## 2.1.0
 
 ### New Features:
+
 - Added back read/write support for unicode field names. 
 - Improved Record representation
 - More support for geojson on Reader, ShapeRecord, ShapeRecords, and shapes()
@@ -140,6 +154,8 @@ repository of the PyShp GitHub site.
 
 ## Reading Shapefiles
 
+### The Reader Class
+
 To read a shapefile create a new "Reader" object and pass it the name of an
 existing shapefile. The shapefile format is actually a collection of three
 files. You specify the base filename of the shapefile or the complete filename
@@ -161,7 +177,7 @@ OR
 OR any of the other 5+ formats which are potentially part of a shapefile. The
 library does not care about file extensions.
 
-### Reading Shapefiles Using the Context Manager
+#### Reading Shapefiles Using the Context Manager
 
 The "Reader" class can be used as a context manager, to ensure open file
 objects are properly closed when done reading the data:
@@ -172,7 +188,7 @@ objects are properly closed when done reading the data:
         663 shapes (type 'POLYGON')
         663 records (44 fields)
 
-### Reading Shapefiles from File-Like Objects
+#### Reading Shapefiles from File-Like Objects
 
 You can also load shapefiles from any Python file-like object using keyword
 arguments to specify any of the three files. This feature is very powerful and
@@ -190,7 +206,7 @@ file. This file is optional for reading. If it's available PyShp will use the
 shx file to access shape records a little faster but will do just fine without
 it.
 
-### Reading Shapefile Meta-Data
+#### Reading Shapefile Meta-Data
 
 Shapefiles have a number of attributes for inspecting the file contents.
 A shapefile is a container for a specific type of geometry, and this can be checked using the 
@@ -505,6 +521,8 @@ The blockgroup key and population count:
 
 ## Writing Shapefiles
 
+### The Writer Class
+
 PyShp tries to be as flexible as possible when writing shapefiles while
 maintaining some degree of automatic validation to make sure you don't
 accidentally write an invalid file.
@@ -524,6 +542,7 @@ the file path and name to save to:
 
 
 	>>> w = shapefile.Writer('shapefiles/test/testfile')
+	>>> w.field('field1', 'C')
 	
 File extensions are optional when reading or writing shapefiles. If you specify
 them PyShp ignores them anyway. When you save files you can specify a base
@@ -532,11 +551,12 @@ one or more file types:
 
 
 	>>> w = shapefile.Writer(dbf='shapefiles/test/onlydbf.dbf')
+	>>> w.field('field1', 'C')
 	
 In that case, any file types not assigned will not
 save and only file types with file names will be saved. 
 
-### Writing Shapefiles Using the Context Manager
+#### Writing Shapefiles Using the Context Manager
 
 The "Writer" class automatically closes the open files and writes the final headers once it is garbage collected.
 In case of a crash and to make the code more readable, it is nevertheless recommended 
@@ -549,10 +569,11 @@ Alternatively, you can also use the "Writer" class as a context manager, to ensu
 objects are properly closed and final headers written once you exit the with-clause:
 
 
-	>>> with shapefile.Writer("shapefiles/test/contextwriter") as shp:
-	...		pass
+	>>> with shapefile.Writer("shapefiles/test/contextwriter") as w:
+	... 	w.field('field1', 'C')
+	... 	pass
 
-### Writing Shapefiles to File-Like Objects
+#### Writing Shapefiles to File-Like Objects
 
 Just as you can read shapefiles from python file-like objects you can also
 write to them:
@@ -572,7 +593,7 @@ write to them:
 	>>> w.close()
 	>>> # To read back the files you could call the "StringIO.getvalue()" method later.
 	
-### Setting the Shape Type
+#### Setting the Shape Type
 
 The shape type defines the type of geometry contained in the shapefile. All of
 the shapes must match the shape type setting.
@@ -586,6 +607,7 @@ To manually set the shape type for a Writer object when creating the Writer:
 
 
 	>>> w = shapefile.Writer('shapefiles/test/shapetype', shapeType=3)
+	>>> w.field('field1', 'C')
 
 	>>> w.shapeType
 	3
@@ -800,9 +822,9 @@ The direction of your polygons determines how shapefile readers will distinguish
 	>>> w.field('name', 'C')
 
 	>>> w.poly([
-	...	        [[122,37], [117,36], [115,32], [118,20], [113,24]], # poly 1
-	...	        [[15,2], [17,6], [22,7]], # hole 1
-	...         [[122,37], [117,36], [115,32]] # poly 2
+	...	        [[113,24], [112,32], [117,36], [122,37], [118,20]], # poly 1
+	...	        [[116,29],[116,26],[119,29],[119,32]], # hole 1
+	...         [[15,2], [17,6], [22,7]]  # poly 2
 	...        ])
 	>>> w.record('polygon1')
 	
@@ -1096,10 +1118,12 @@ David A. Riggs
 davidh-ssec
 Evan Heidtmann
 ezcitron
+fiveham
 geospatialpython
 Hannes
 Ignacio Martinez Vazquez
 Jason Moujaes
+Jonty Wareing
 Karim Bahgat
 Kyle Kelley
 Louis Tiao
@@ -1118,5 +1142,6 @@ Ryan Brideau
 Tobias Megies
 Tommi Penttinen
 Uli Köhler
+Vsevolod Novikov
 Zac Miller
 ```


=====================================
changelog.txt
=====================================
@@ -1,4 +1,20 @@
 
+VERSION 2.1.1
+
+2020-09-09
+	Improvements:
+	* Handle shapes with no coords and represent as geojson with no coords (GeoJSON null-equivalent)
+	* Expand testing to Python 3.6, 3.7, 3.8 and PyPy; drop 3.3 and 3.4 [@mwtoews]
+	* Added pytest testing [@jmoujaes]
+
+	Bug fixes:
+	* Fix incorrect geo interface handling of multipolygons with complex exterior-hole relations [see #202]
+	* Enforce shapefile requirement of at least one field, to avoid writing invalid shapefiles [@Jonty]
+	* Fix Reader geo interface including DeletionFlag field in feature properties [@nnseva]
+	* Fix polygons not being auto closed, which was accidentally dropped
+	* Fix error for null geometries in feature geojson
+	* Misc docstring cleanup [@fiveham]
+
 VERSION 2.1.0
 
 2019-02-15


=====================================
requirements.test.txt
=====================================
@@ -0,0 +1,2 @@
+pytest==3.2.5
+setuptools


=====================================
setup.py
=====================================
@@ -7,14 +7,14 @@ def read_file(file):
     return data.decode('utf-8')
 
 setup(name='pyshp',
-      version='2.1.0',
+      version='2.1.1',
       description='Pure Python read/write support for ESRI Shapefile format',
       long_description=read_file('README.md'),
       long_description_content_type='text/markdown',
       author='Joel Lawhead',
       author_email='jlawhead at geospatialpython.com',
       url='https://github.com/GeospatialPython/pyshp',
-      download_url='https://github.com/GeospatialPython/pyshp/archive/2.1.0.tar.gz',
+      download_url='https://github.com/GeospatialPython/pyshp/archive/2.1.1.tar.gz',
       py_modules=['shapefile'],
       license='MIT',
       zip_safe=False,
@@ -23,6 +23,10 @@ setup(name='pyshp',
       classifiers=['Programming Language :: Python',
                    'Programming Language :: Python :: 2.7',
                    'Programming Language :: Python :: 3',
+                   'Programming Language :: Python :: 3.5',
+                   'Programming Language :: Python :: 3.6',
+                   'Programming Language :: Python :: 3.7',
+                   'Programming Language :: Python :: 3.8',
                    'Topic :: Scientific/Engineering :: GIS',
                    'Topic :: Software Development :: Libraries',
                    'Topic :: Software Development :: Libraries :: Python Modules'])


=====================================
shapefile.py
=====================================
@@ -2,11 +2,11 @@
 shapefile.py
 Provides read and write support for ESRI Shapefiles.
 author: jlawhead<at>geospatialpython.com
-version: 2.1.0
+version: 2.1.1
 Compatible with Python versions 2.7-3.x
 """
 
-__version__ = "2.1.0"
+__version__ = "2.1.1"
 
 from struct import pack, unpack, calcsize, error, Struct
 import os
@@ -18,6 +18,8 @@ import warnings
 import io
 from datetime import date
 
+warnings.simplefilter("always")
+
 
 # Constants for shape types
 NULL = 0
@@ -151,7 +153,7 @@ else:
 # Begin
 
 class _Array(array.array):
-    """Converts python tuples to lits of the appropritate type.
+    """Converts python tuples to lists of the appropritate type.
     Used to unpack different shapefile header parts."""
     def __repr__(self):
         return str(self.tolist())
@@ -165,13 +167,235 @@ def signed_area(coords):
     ys.append(ys[1])
     return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0
 
+def ring_bbox(coords):
+    """Calculates and returns the bounding box of a ring.
+    """
+    xs,ys = zip(*coords)
+    bbox = min(xs),min(ys),max(xs),max(ys)
+    return bbox
+
+def bbox_overlap(bbox1, bbox2):
+    """Tests whether two bounding boxes overlap, returning a boolean
+    """
+    xmin1,ymin1,xmax1,ymax1 = bbox1
+    xmin2,ymin2,xmax2,ymax2 = bbox2
+    overlap = (xmin1 <= xmax2 and xmax1 >= xmin2 and ymin1 <= ymax2 and ymax1 >= ymin2)
+    return overlap
+
+def bbox_contains(bbox1, bbox2):
+    """Tests whether bbox1 fully contains bbox2, returning a boolean
+    """
+    xmin1,ymin1,xmax1,ymax1 = bbox1
+    xmin2,ymin2,xmax2,ymax2 = bbox2
+    contains = (xmin1 < xmin2 and xmax1 > xmax2 and ymin1 < ymin2 and ymax1 > ymax2)
+    return contains
+
+def ring_contains_point(coords, p):
+    """Fast point-in-polygon crossings algorithm, MacMartin optimization..
+
+    Adapted from code by Eric Haynes
+    http://www.realtimerendering.com/resources/GraphicsGems//gemsiv/ptpoly_haines/ptinpoly.c
+    
+    Original description:
+        Shoot a test ray along +X axis.  The strategy, from MacMartin, is to
+        compare vertex Y values to the testing point's Y and quickly discard
+        edges which are entirely to one side of the test ray.
+    """
+    tx,ty = p
+
+    # get initial test bit for above/below X axis
+    vtx0 = coords[0]
+    yflag0 = ( vtx0[1] >= ty )
+
+    inside_flag = False
+    for vtx1 in coords[1:]: 
+        yflag1 = ( vtx1[1] >= ty )
+        # check if endpoints straddle (are on opposite sides) of X axis
+        # (i.e. the Y's differ); if so, +X ray could intersect this edge..
+        if yflag0 != yflag1: 
+            xflag0 = ( vtx0[0] >= tx )
+            # check if endpoints are on same side of the Y axis (i.e. X's
+            # are the same); if so, it's easy to test if edge hits or misses.
+            if xflag0 == ( vtx1[0] >= tx ):
+                # if edge's X values both right of the point, must hit
+                if xflag0:
+                    inside_flag = not inside_flag
+            else:
+                # compute intersection of pgon segment with +X ray, note
+                # if >= point's X; if so, the ray hits it.
+                if ( vtx1[0] - (vtx1[1]-ty) * ( vtx0[0]-vtx1[0]) / (vtx0[1]-vtx1[1]) ) >= tx:
+                    inside_flag = not inside_flag
+
+        # move to next pair of vertices, retaining info as possible
+        yflag0 = yflag1
+        vtx0 = vtx1
+
+    return inside_flag
+
+def ring_sample(coords, ccw=False):
+    """Return a sample point guaranteed to be within a ring, by efficiently
+    finding the first centroid of a coordinate triplet whose orientation
+    matches the orientation of the ring and passes the point-in-ring test.
+    The orientation of the ring is assumed to be clockwise, unless ccw
+    (counter-clockwise) is set to True. 
+    """
+    coords = tuple(coords) + (coords[1],) # add the second coordinate to the end to allow checking the last triplet
+    triplet = []
+    for p in coords:
+        # add point to triplet (but not if duplicate)
+        if p not in triplet:
+            triplet.append(p)
+            
+        # new triplet, try to get sample
+        if len(triplet) == 3:
+            # check that triplet does not form a straight line (not a triangle)
+            is_straight_line = (triplet[0][1] - triplet[1][1]) * (triplet[0][0] - triplet[2][0]) == (triplet[0][1] - triplet[2][1]) * (triplet[0][0] - triplet[1][0])
+            if not is_straight_line:
+                # get triplet orientation
+                closed_triplet = triplet + [triplet[0]]
+                triplet_ccw = signed_area(closed_triplet) >= 0
+                # check that triplet has the same orientation as the ring (means triangle is inside the ring)
+                if ccw == triplet_ccw:
+                    # get triplet centroid
+                    xs,ys = zip(*triplet)
+                    xmean,ymean = sum(xs) / 3.0, sum(ys) / 3.0
+                    # check that triplet centroid is truly inside the ring
+                    if ring_contains_point(coords, (xmean,ymean)):
+                        return xmean,ymean
+
+            # failed to get sample point from this triplet
+            # remove oldest triplet coord to allow iterating to next triplet
+            triplet.pop(0)
+            
+    else:
+        raise Exception('Unexpected error: Unable to find a ring sample point.')
+
+def ring_contains_ring(coords1, coords2):
+    '''Returns True if all vertexes in coords2 are fully inside coords1.
+    '''
+    return all((ring_contains_point(coords1, p2) for p2 in coords2))
+
+def organize_polygon_rings(rings):
+    '''Organize a list of coordinate rings into one or more polygons with holes.
+    Returns a list of polygons, where each polygon is composed of a single exterior
+    ring, and one or more interior holes.
+
+    Rings must be closed, and cannot intersect each other (non-self-intersecting polygon).
+    Rings are determined as exteriors if they run in clockwise direction, or interior
+    holes if they run in counter-clockwise direction. This method is used to construct
+    GeoJSON (multi)polygons from the shapefile polygon shape type, which does not
+    explicitly store the structure of the polygons beyond exterior/interior ring orientation. 
+    '''
+    # first iterate rings and classify as exterior or hole
+    exteriors = []
+    holes = []
+    for ring in rings:
+        # shapefile format defines a polygon as a sequence of rings
+        # where exterior rings are clockwise, and holes counterclockwise
+        if signed_area(ring) < 0:
+            # ring is exterior
+            exteriors.append(ring)
+        else:
+            # ring is a hole
+            holes.append(ring)
+                
+    # if only one exterior, then all holes belong to that exterior
+    if len(exteriors) == 1:
+        # exit early
+        poly = [exteriors[0]] + holes
+        polys = [poly]
+        return polys
+
+    # multiple exteriors, ie multi-polygon, have to group holes with correct exterior
+    # shapefile format does not specify which holes belong to which exteriors
+    # so have to do efficient multi-stage checking of hole-to-exterior containment
+    elif len(exteriors) > 1:
+        # exit early if no holes
+        if not holes:
+            polys = []
+            for ext in exteriors:
+                poly = [ext]
+                polys.append(poly)
+            return polys
+        
+        # first determine each hole's candidate exteriors based on simple bbox contains test
+        hole_exteriors = dict([(hole_i,[]) for hole_i in xrange(len(holes))])
+        exterior_bboxes = [ring_bbox(ring) for ring in exteriors]
+        for hole_i in hole_exteriors.keys():
+            hole_bbox = ring_bbox(holes[hole_i])
+            for ext_i,ext_bbox in enumerate(exterior_bboxes):
+                if bbox_contains(ext_bbox, hole_bbox):
+                    hole_exteriors[hole_i].append( ext_i )
+
+        # then, for holes with still more than one possible exterior, do more detailed hole-in-ring test
+        for hole_i,exterior_candidates in hole_exteriors.items():
+            
+            if len(exterior_candidates) > 1:
+                # get hole sample point
+                hole_sample = ring_sample(holes[hole_i], ccw=True)
+                # collect new exterior candidates
+                new_exterior_candidates = []
+                for ext_i in exterior_candidates:
+                    # check that hole sample point is inside exterior
+                    hole_in_exterior = ring_contains_point(exteriors[ext_i], hole_sample)
+                    if hole_in_exterior:
+                        new_exterior_candidates.append(ext_i)
+
+                # set new exterior candidates
+                hole_exteriors[hole_i] = new_exterior_candidates
+
+        # if still holes with more than one possible exterior, means we have an exterior hole nested inside another exterior's hole
+        for hole_i,exterior_candidates in hole_exteriors.items():
+            
+            if len(exterior_candidates) > 1:
+                # exterior candidate with the smallest area is the hole's most immediate parent
+                ext_i = sorted(exterior_candidates, key=lambda x: abs(signed_area(exteriors[x])))[0]
+                hole_exteriors[hole_i] = [ext_i]
+
+        # separate out holes that are orphaned (not contained by any exterior)
+        orphan_holes = []
+        for hole_i,exterior_candidates in list(hole_exteriors.items()):
+            if not exterior_candidates:
+                warnings.warn('Shapefile shape has invalid polygon: found orphan hole (not contained by any of the exteriors); interpreting as exterior.')
+                orphan_holes.append( hole_i )
+                del hole_exteriors[hole_i]
+                continue
+
+        # each hole should now only belong to one exterior, group into exterior-holes polygons
+        polys = []
+        for ext_i,ext in enumerate(exteriors):
+            poly = [ext]
+            # find relevant holes
+            poly_holes = []
+            for hole_i,exterior_candidates in list(hole_exteriors.items()):
+                # hole is relevant if previously matched with this exterior
+                if exterior_candidates[0] == ext_i:
+                    poly_holes.append( holes[hole_i] )
+            poly += poly_holes
+            polys.append(poly)
+
+        # add orphan holes as exteriors
+        for hole_i in orphan_holes:
+            ext = holes[hole_i] # could potentially reverse their order, but in geojson winding order doesn't matter
+            poly = [ext]
+            polys.append(poly)
+
+        return polys
+
+    # no exteriors, be nice and assume due to incorrect winding order
+    else:
+        warnings.warn('Shapefile shape has invalid polygon: no exterior rings found (must have clockwise orientation); interpreting holes as exteriors.')
+        exteriors = holes # could potentially reverse their order, but in geojson winding order doesn't matter
+        polys = [[ext] for ext in exteriors]
+        return polys
+
 class Shape(object):
     def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
         """Stores the geometry of the different shape types
         specified in the Shapefile spec. Shape types are
         usually point, polyline, or polygons. Every shape type
         except the "Null" type contains points at some level for
-        example verticies in a polygon. If a shape type has
+        example vertices in a polygon. If a shape type has
         multiple shapes containing points within a single
         geometry record then those shapes are called parts. Parts
         are designated by their starting index in geometry record's
@@ -186,26 +410,44 @@ class Shape(object):
 
     @property
     def __geo_interface__(self):
-        if not self.parts or not self.points:
-            Exception('Invalid shape, cannot create GeoJSON representation. Shape type is "%s" but does not contain any parts and/or points.' % SHAPETYPE_LOOKUP[self.shapeType])
-
         if self.shapeType in [POINT, POINTM, POINTZ]:
-            return {
-            'type': 'Point',
-            'coordinates': tuple(self.points[0])
-            }
+            # point
+            if len(self.points) == 0:
+                # the shape has no coordinate information, i.e. is 'empty'
+                # the geojson spec does not define a proper null-geometry type
+                # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries
+                return {'type':'Point', 'coordinates':tuple()}
+            else:
+                return {
+                'type': 'Point',
+                'coordinates': tuple(self.points[0])
+                }
         elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]:
-            return {
-            'type': 'MultiPoint',
-            'coordinates': tuple([tuple(p) for p in self.points])
-            }
+            if len(self.points) == 0:
+                # the shape has no coordinate information, i.e. is 'empty'
+                # the geojson spec does not define a proper null-geometry type
+                # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries
+                return {'type':'MultiPoint', 'coordinates':[]}
+            else:
+                # multipoint
+                return {
+                'type': 'MultiPoint',
+                'coordinates': [tuple(p) for p in self.points]
+                }
         elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]:
-            if len(self.parts) == 1:
+            if len(self.parts) == 0:
+                # the shape has no coordinate information, i.e. is 'empty'
+                # the geojson spec does not define a proper null-geometry type
+                # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries
+                return {'type':'LineString', 'coordinates':[]}
+            elif len(self.parts) == 1:
+                # linestring
                 return {
                 'type': 'LineString',
-                'coordinates': tuple([tuple(p) for p in self.points])
+                'coordinates': [tuple(p) for p in self.points]
                 }
             else:
+                # multilinestring
                 ps = None
                 coordinates = []
                 for part in self.parts:
@@ -213,51 +455,51 @@ class Shape(object):
                         ps = part
                         continue
                     else:
-                        coordinates.append(tuple([tuple(p) for p in self.points[ps:part]]))
+                        coordinates.append([tuple(p) for p in self.points[ps:part]])
                         ps = part
                 else:
-                    coordinates.append(tuple([tuple(p) for p in self.points[part:]]))
+                    coordinates.append([tuple(p) for p in self.points[part:]])
                 return {
                 'type': 'MultiLineString',
-                'coordinates': tuple(coordinates)
+                'coordinates': coordinates
                 }
         elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]:
-            if len(self.parts) == 1:
-                return {
-                'type': 'Polygon',
-                'coordinates': (tuple([tuple(p) for p in self.points]),)
-                }
+            if len(self.parts) == 0:
+                # the shape has no coordinate information, i.e. is 'empty'
+                # the geojson spec does not define a proper null-geometry type
+                # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries
+                return {'type':'Polygon', 'coordinates':[]}
             else:
-                ps = None
+                # get all polygon rings
                 rings = []
-                for part in self.parts:
-                    if ps == None:
-                        ps = part
-                        continue
-                    else:
-                        rings.append(tuple([tuple(p) for p in self.points[ps:part]]))
-                        ps = part
-                else:
-                    rings.append(tuple([tuple(p) for p in self.points[part:]]))
-                polys = []
-                poly = [rings[0]]
-                for ring in rings[1:]:
-                    if signed_area(ring) < 0:
-                        polys.append(poly)
-                        poly = [ring]
-                    else:
-                        poly.append(ring)
-                polys.append(poly)
+                for i in xrange(len(self.parts)):
+                    # get indexes of start and end points of the ring
+                    start = self.parts[i]
+                    try:
+                        end = self.parts[i+1]
+                    except IndexError:
+                        end = len(self.points)
+
+                    # extract the points that make up the ring
+                    ring = [tuple(p) for p in self.points[start:end]]
+                    rings.append(ring)
+
+                # organize rings into list of polygons, where each polygon is defined as list of rings.
+                # the first ring is the exterior and any remaining rings are holes (same as GeoJSON). 
+                polys = organize_polygon_rings(rings)
+
+                # return as geojson
                 if len(polys) == 1:
                     return {
                     'type': 'Polygon',
-                    'coordinates': tuple(polys[0])
+                    'coordinates': polys[0]
                     }
-                elif len(polys) > 1:
+                else:
                     return {
                     'type': 'MultiPolygon',
                     'coordinates': polys
                     }
+
         else:
             raise Exception('Shape type "%s" cannot be represented as GeoJSON.' % SHAPETYPE_LOOKUP[self.shapeType])
 
@@ -344,11 +586,11 @@ class Shape(object):
 class _Record(list):
     """
     A class to hold a record. Subclasses list to ensure compatibility with
-    former work and allows to use all the optimazations of the builtin list.
+    former work and to reuse all the optimizations of the builtin list.
     In addition to the list interface, the values of the record
-    can also be retrieved using the fields name. Eg. if the dbf contains
+    can also be retrieved using the field's name. For example if the dbf contains
     a field ID at position 0, the ID can be retrieved with the position, the field name
-    as a key or the field name as an attribute.
+    as a key, or the field name as an attribute.
 
     >>> # Create a Record with one field, normally the record is created by the Reader class
     >>> r = _Record({'ID': 0}, [0])
@@ -375,12 +617,13 @@ class _Record(list):
     def __getattr__(self, item):
         """
         __getattr__ is called if an attribute is used that does
-        not exist in the normal sense. Eg. r=Record(...), r.ID
+        not exist in the normal sense. For example r=Record(...), r.ID
         calls r.__getattr__('ID'), but r.index(5) calls list.index(r, 5)
         :param item: The field name, used as attribute
         :return: Value of the field
-        :raises: Attribute error, if field does not exist
-                and IndexError, if field exists but not values in the Record
+        :raises: AttributeError, if item is not a field of the shapefile
+                and IndexError, if the field exists but the field's 
+                corresponding value in the Record does not exist
         """
         try:
             index = self.__field_positions[item]
@@ -411,7 +654,7 @@ class _Record(list):
         Extends the normal list item access with
         access using a fieldname
 
-        Eg. r['ID'], r[0]
+        For example r['ID'], r[0]
         :param item: Either the position of the value or the name of a field
         :return: the value of the field
         """
@@ -432,7 +675,7 @@ class _Record(list):
         Extends the normal list item access with
         access using a fieldname
 
-        Eg. r['ID']=2, r[0]=2
+        For example r['ID']=2, r[0]=2
         :param key: Either the position of the value or the name of a field
         :param value: the new value of the field
         """
@@ -468,7 +711,7 @@ class _Record(list):
         :return: List of method names and fields
         """
         default = list(dir(type(self))) # default list methods and attributes of this class
-        fnames = list(self.__field_positions.keys()) # plus field names (random order)
+        fnames = list(self.__field_positions.keys()) # plus field names (random order if Python version < 3.6)
         return default + fnames 
         
 class ShapeRecord(object):
@@ -482,27 +725,29 @@ class ShapeRecord(object):
     def __geo_interface__(self):
         return {'type': 'Feature',
                 'properties': self.record.as_dict(),
-                'geometry': self.shape.__geo_interface__}
+                'geometry': None if self.shape.shapeType == NULL else self.shape.__geo_interface__}
 
 class Shapes(list):
     """A class to hold a list of Shape objects. Subclasses list to ensure compatibility with
-    former work and allows to use all the optimazations of the builtin list.
+    former work and to reuse all the optimizations of the builtin list.
     In addition to the list interface, this also provides the GeoJSON __geo_interface__
-    to return a GeometryCollection dictionary. """
+    to return a GeometryCollection dictionary."""
 
     def __repr__(self):
         return 'Shapes: {}'.format(list(self))
 
     @property
     def __geo_interface__(self):
+        # Note: currently this will fail if any of the shapes are null-geometries
+        # could be fixed by storing the shapefile shapeType upon init, returning geojson type with empty coords
         return {'type': 'GeometryCollection',
-                'geometries': [g.__geo_interface__ for g in self]}
+                'geometries': [shape.__geo_interface__ for shape in self]}
 
 class ShapeRecords(list):
     """A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with
-    former work and allows to use all the optimazations of the builtin list.
+    former work and to reuse all the optimizations of the builtin list.
     In addition to the list interface, this also provides the GeoJSON __geo_interface__
-    to return a FeatureCollection dictionary. """
+    to return a FeatureCollection dictionary."""
 
     def __repr__(self):
         return 'ShapeRecords: {}'.format(list(self))
@@ -510,7 +755,7 @@ class ShapeRecords(list):
     @property
     def __geo_interface__(self):
         return {'type': 'FeatureCollection',
-                'features': [f.__geo_interface__ for f in self]}
+                'features': [shaperec.__geo_interface__ for shaperec in self]}
 
 class ShapefileException(Exception):
     """An exception to handle shapefile specific problems."""
@@ -576,7 +821,7 @@ class Reader(object):
                     self.dbf.seek(0)
                 except (NameError, io.UnsupportedOperation):
                     self.dbf = io.BytesIO(self.dbf.read())
-        if self.shp or self.dbf:        
+        if self.shp or self.dbf:
             self.load()
         else:
             raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")
@@ -617,16 +862,10 @@ class Reader(object):
 
     @property
     def __geo_interface__(self):
-        fieldnames = [f[0] for f in self.fields]
-        features = []
-        for feat in self.iterShapeRecords():
-            fdict = {'type': 'Feature',
-                     'properties': dict(zip(fieldnames,feat.record)),
-                     'geometry': feat.shape.__geo_interface__}
-            features.append(fdict)
-        return {'type': 'FeatureCollection',
-                'bbox': self.bbox,
-                'features': features}
+        shaperecords = self.shapeRecords()
+        fcollection = shaperecords.__geo_interface__
+        fcollection['bbox'] = self.bbox
+        return fcollection
 
     @property
     def shapeTypeName(self):
@@ -840,7 +1079,7 @@ class Reader(object):
             return self._offsets[i]
 
     def shape(self, i=0):
-        """Returns a shape object for a shape in the the geometry
+        """Returns a shape object for a shape in the geometry
         record file."""
         shp = self.__getFileObj(self.shp)
         i = self.__restrictIndex(i)
@@ -869,14 +1108,14 @@ class Reader(object):
         return shapes
 
     def iterShapes(self):
-        """Serves up shapes in a shapefile as an iterator. Useful
+        """Returns a generator of shapes in a shapefile. Useful
         for handling large shapefiles."""
         shp = self.__getFileObj(self.shp)
         shp.seek(0,2)
         self.shpLength = shp.tell()
         shp.seek(100)
         while shp.tell() < self.shpLength:
-            yield self.__shape()    
+            yield self.__shape()
 
     def __dbfHeader(self):
         """Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""
@@ -1015,7 +1254,7 @@ class Reader(object):
         return records
 
     def iterRecords(self):
-        """Serves up records in a dbf file as an iterator.
+        """Returns a generator of records in a dbf file.
         Useful for large shapefiles or dbf files."""
         if self.numRecords is None:
             self.__dbfHeader()
@@ -1035,8 +1274,7 @@ class Reader(object):
     def shapeRecords(self):
         """Returns a list of combination geometry/attribute records for
         all records in a shapefile."""
-        return ShapeRecords([ShapeRecord(shape=rec[0], record=rec[1]) \
-                                for rec in zip(self.shapes(), self.records())])
+        return ShapeRecords(self.iterShapeRecords())
 
     def iterShapeRecords(self):
         """Returns a generator of combination geometry/attribute records for
@@ -1076,8 +1314,8 @@ class Writer(object):
         self._bbox = None
         self._zbox = None
         self._mbox = None
-        # Use deletion flags in dbf? Default is false (0).
-        self.deletionFlag = 0
+        # Use deletion flags in dbf? Default is false (0). Note: Currently has no effect, records should NOT contain deletion flags.
+        self.deletionFlag = 0 
         # Encoding
         self.encoding = kwargs.pop('encoding', 'utf-8')
         self.encodingErrors = kwargs.pop('encodingErrors', 'strict')
@@ -1307,22 +1545,23 @@ class Writer(object):
         version = 3
         year, month, day = time.localtime()[:3]
         year -= 1900
-        # Remove deletion flag placeholder from fields
-        for field in self.fields:
-            if field[0].startswith("Deletion"):
-                self.fields.remove(field)
+        # Get all fields, ignoring DeletionFlag if specified
+        fields = [field for field in self.fields if field[0] != 'DeletionFlag']
+        # Ensure has at least one field
+        if not fields:
+            raise ShapefileException("Shapefile dbf file must contain at least one field.")
         numRecs = self.recNum
-        numFields = len(self.fields)
+        numFields = len(fields)
         headerLength = numFields * 32 + 33
         if headerLength >= 65535:
             raise ShapefileException(
                     "Shapefile dbf header length exceeds maximum length.")
-        recordLength = sum([int(field[2]) for field in self.fields]) + 1
+        recordLength = sum([int(field[2]) for field in fields]) + 1
         header = pack('<BBBBLHH20x', version, year, month, day, numRecs,
                 headerLength, recordLength)
         f.write(header)
         # Field descriptors
-        for field in self.fields:
+        for field in fields:
             name, fieldType, size, decimal = field
             name = b(name, self.encoding, self.encodingErrors)
             name = name.replace(b' ', b'_')
@@ -1505,7 +1744,7 @@ class Writer(object):
         """Creates a dbf attribute record. You can submit either a sequence of
         field values or keyword arguments of field names and values. Before
         adding records you must add fields for the record values using the
-        fields() method. If the record values exceed the number of fields the
+        field() method. If the record values exceed the number of fields the
         extra ones won't be added. In the case of using keyword arguments to specify
         field/value pairs only fields matching the already registered fields
         will be added."""
@@ -1513,14 +1752,13 @@ class Writer(object):
         if self.autoBalance and self.recNum > self.shpNum:
             self.balance()
             
-        record = []
-        fieldCount = len(self.fields)
-        # Compensate for deletion flag
-        if self.fields[0][0].startswith("Deletion"): fieldCount -= 1
         if recordList:
-            record = [recordList[i] for i in range(fieldCount)]
+            record = list(recordList)
         elif recordDict:
+            record = []
             for field in self.fields:
+                if field[0] == 'DeletionFlag':
+                    continue # ignore deletionflag field in case it was specified
                 if field[0] in recordDict:
                     val = recordDict[field[0]]
                     if val is None:
@@ -1529,7 +1767,7 @@ class Writer(object):
                         record.append(val)
         else:
             # Blank fields for empty record
-            record = ["" for i in range(fieldCount)]
+            record = ["" for field in self.fields if field[0] != 'DeletionFlag']
         self.__dbfRecord(record)
 
     def __dbfRecord(self, record):
@@ -1540,11 +1778,13 @@ class Writer(object):
             # allowing us to write the dbf header
             # cannot change the fields after this point
             self.__dbfHeader()
+        # first byte of the record is deletion flag, always disabled
+        f.write(b' ')
         # begin
         self.recNum += 1
-        if not self.fields[0][0].startswith("Deletion"):
-            f.write(b' ') # deletion flag
-        for (fieldName, fieldType, size, deci), value in zip(self.fields, record):
+        fields = (field for field in self.fields if field[0] != 'DeletionFlag') # ignore deletionflag field in case it was specified
+        for (fieldName, fieldType, size, deci), value in zip(fields, record):
+            # write 
             fieldType = fieldType.upper()
             size = int(size)
             if fieldType in ("N","F"):
@@ -1747,6 +1987,12 @@ class Writer(object):
         polyShape = Shape(shapeType)
         polyShape.parts = []
         polyShape.points = []
+        # Make sure polygon rings (parts) are closed
+        if shapeType in (5,15,25,31):
+            for part in parts:
+                if part[0] != part[-1]:
+                    part.append(part[0])
+        # Add points and part indexes
         for part in parts:
             # set part index position
             polyShape.parts.append(len(polyShape.points))


=====================================
shapefiles/test/balancing.dbf
=====================================
Binary files a/shapefiles/test/balancing.dbf and b/shapefiles/test/balancing.dbf differ


=====================================
shapefiles/test/contextwriter.dbf
=====================================
Binary files a/shapefiles/test/contextwriter.dbf and b/shapefiles/test/contextwriter.dbf differ


=====================================
shapefiles/test/dtype.dbf
=====================================
Binary files a/shapefiles/test/dtype.dbf and b/shapefiles/test/dtype.dbf differ


=====================================
shapefiles/test/line.dbf
=====================================
Binary files a/shapefiles/test/line.dbf and b/shapefiles/test/line.dbf differ


=====================================
shapefiles/test/linem.dbf
=====================================
Binary files a/shapefiles/test/linem.dbf and b/shapefiles/test/linem.dbf differ


=====================================
shapefiles/test/linez.dbf
=====================================
Binary files a/shapefiles/test/linez.dbf and b/shapefiles/test/linez.dbf differ


=====================================
shapefiles/test/multipatch.dbf
=====================================
Binary files a/shapefiles/test/multipatch.dbf and b/shapefiles/test/multipatch.dbf differ


=====================================
shapefiles/test/multipoint.dbf
=====================================
Binary files a/shapefiles/test/multipoint.dbf and b/shapefiles/test/multipoint.dbf differ


=====================================
shapefiles/test/onlydbf.dbf
=====================================
Binary files a/shapefiles/test/onlydbf.dbf and b/shapefiles/test/onlydbf.dbf differ


=====================================
shapefiles/test/point.dbf
=====================================
Binary files a/shapefiles/test/point.dbf and b/shapefiles/test/point.dbf differ


=====================================
shapefiles/test/polygon.dbf
=====================================
Binary files a/shapefiles/test/polygon.dbf and b/shapefiles/test/polygon.dbf differ


=====================================
shapefiles/test/polygon.shp
=====================================
Binary files a/shapefiles/test/polygon.shp and b/shapefiles/test/polygon.shp differ


=====================================
shapefiles/test/polygon.shx
=====================================
Binary files a/shapefiles/test/polygon.shx and b/shapefiles/test/polygon.shx differ


=====================================
shapefiles/test/shapetype.dbf
=====================================
Binary files a/shapefiles/test/shapetype.dbf and b/shapefiles/test/shapetype.dbf differ


=====================================
shapefiles/test/testfile.dbf
=====================================
Binary files a/shapefiles/test/testfile.dbf and b/shapefiles/test/testfile.dbf differ



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/commit/87249afd0d56c0352e36266b14e36f80eedcc08a

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/commit/87249afd0d56c0352e36266b14e36f80eedcc08a
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20200910/966f2dab/attachment-0001.html>


More information about the Pkg-grass-devel mailing list