@@ -341,6 +341,141 @@ def _from_geojson(geoj):
341
341
@property
342
342
def shapeTypeName (self ):
343
343
return SHAPETYPE_LOOKUP [self .shapeType ]
344
+
345
+ class _Record (list ):
346
+ """
347
+ A class to hold a record. Subclasses list to ensure compatibility with
348
+ former work and allows to use all the optimazations of the builtin list.
349
+ In addition to the list interface, the values of the record
350
+ can also be retrieved using the fields name. Eg. if the dbf contains
351
+ a field ID at position 0, the ID can be retrieved with the position, the field name
352
+ as a key or the field name as an attribute.
353
+
354
+ >>> # Create a Record with one field, normally the record is created by the Reader class
355
+ >>> r = _Record({'ID': 0}, [0])
356
+ >>> print(r[0])
357
+ >>> print(r['ID'])
358
+ >>> print(r.ID)
359
+ """
360
+
361
+ def __init__ (self , field_positions , values , oid = None ):
362
+ """
363
+ A Record should be created by the Reader class
364
+
365
+ :param field_positions: A dict mapping field names to field positions
366
+ :param values: A sequence of values
367
+ :param oid: The object id, an int (optional)
368
+ """
369
+ self .__field_positions = field_positions
370
+ if oid is not None :
371
+ self .__oid = oid
372
+ else :
373
+ self .__oid = - 1
374
+ list .__init__ (self , values )
375
+
376
+ def __getattr__ (self , item ):
377
+ """
378
+ __getattr__ is called if an attribute is used that does
379
+ not exist in the normal sense. Eg. r=Record(...), r.ID
380
+ calls r.__getattr__('ID'), but r.index(5) calls list.index(r, 5)
381
+ :param item: The field name, used as attribute
382
+ :return: Value of the field
383
+ :raises: Attribute error, if field does not exist
384
+ and IndexError, if field exists but not values in the Record
385
+ """
386
+ try :
387
+ index = self .__field_positions [item ]
388
+ return list .__getitem__ (self , index )
389
+ except KeyError :
390
+ raise AttributeError ('{} is not a field name' .format (item ))
391
+ except IndexError :
392
+ raise IndexError ('{} found as a field but not enough values available.' .format (item ))
393
+
394
+ def __setattr__ (self , key , value ):
395
+ """
396
+ Sets a value of a field attribute
397
+ :param key: The field name
398
+ :param value: the value of that field
399
+ :return: None
400
+ :raises: AttributeError, if key is not a field of the shapefile
401
+ """
402
+ if key .startswith ('_' ): # Prevent infinite loop when setting mangled attribute
403
+ return list .__setattr__ (self , key , value )
404
+ try :
405
+ index = self .__field_positions [key ]
406
+ return list .__setitem__ (self , index , value )
407
+ except KeyError :
408
+ raise AttributeError ('{} is not a field name' .format (key ))
409
+
410
+ def __getitem__ (self , item ):
411
+ """
412
+ Extends the normal list item access with
413
+ access using a fieldname
414
+
415
+ Eg. r['ID'], r[0]
416
+ :param item: Either the position of the value or the name of a field
417
+ :return: the value of the field
418
+ """
419
+ try :
420
+ return list .__getitem__ (self , item )
421
+ except TypeError :
422
+ try :
423
+ index = self .__field_positions [item ]
424
+ except KeyError :
425
+ index = None
426
+ if index is not None :
427
+ return list .__getitem__ (self , index )
428
+ else :
429
+ raise IndexError ('"{}" is not a field name and not an int' .format (item ))
430
+
431
+ def __setitem__ (self , key , value ):
432
+ """
433
+ Extends the normal list item access with
434
+ access using a fieldname
435
+
436
+ Eg. r['ID']=2, r[0]=2
437
+ :param key: Either the position of the value or the name of a field
438
+ :param value: the new value of the field
439
+ """
440
+ try :
441
+ return list .__setitem__ (self , key , value )
442
+ except TypeError :
443
+ index = self .__field_positions .get (key )
444
+ if index is not None :
445
+ return list .__setitem__ (self , index , value )
446
+ else :
447
+ raise IndexError ('{} is not a field name and not an int' .format (key ))
448
+
449
+ @property
450
+ def oid (self ):
451
+ """The index position of the record in the original shapefile"""
452
+ return self .__oid
453
+
454
+ @property
455
+ def fields (self ):
456
+ """The field names of the record"""
457
+ srt = sorted (self .__field_positions .items (), key = lambda (f ,i ): i )
458
+ return [f for f ,i in srt ]
459
+
460
+ def as_dict (self ):
461
+ """
462
+ Returns this Record as a dictionary using the field names as keys
463
+ :return: dict
464
+ """
465
+ return dict ((f , self [i ]) for f , i in self .__field_positions .items ())
466
+
467
+ def __str__ (self ):
468
+ return 'record #{} ' .format (self .__oid )
469
+
470
+ def __dir__ (self ):
471
+ """
472
+ Helps to show the field names in an interactive environment like IPython.
473
+ See: http://ipython.readthedocs.io/en/stable/config/integrating.html
474
+
475
+ :return: List of method names and fields
476
+ """
477
+ attrs = [attr for attr in vars (type (self )) if not attr .startswith ('_' )]
478
+ return attrs + self .fields
344
479
345
480
class ShapeRecord (object ):
346
481
"""A ShapeRecord object containing a shape along with its attributes."""
@@ -380,6 +515,7 @@ def __init__(self, *args, **kwargs):
380
515
self .numRecords = None
381
516
self .fields = []
382
517
self .__dbfHdrLength = 0
518
+ self .__fieldposition_lookup = {}
383
519
self .encoding = kwargs .pop ('encoding' , 'utf-8' )
384
520
self .encodingErrors = kwargs .pop ('encodingErrors' , 'strict' )
385
521
# See if a shapefile name was passed as an argument
@@ -738,6 +874,9 @@ def __dbfHeader(self):
738
874
fmt ,fmtSize = self .__recordFmt ()
739
875
self .__recStruct = Struct (fmt )
740
876
877
+ # Store the field positions
878
+ self .__fieldposition_lookup = dict ((f [0 ], i ) for i , f in enumerate (self .fields [1 :]))
879
+
741
880
def __recordFmt (self ):
742
881
"""Calculates the format and size of a .dbf record."""
743
882
if self .numRecords is None :
@@ -751,7 +890,7 @@ def __recordFmt(self):
751
890
fmtSize += 1
752
891
return (fmt , fmtSize )
753
892
754
- def __record (self ):
893
+ def __record (self , oid = None ):
755
894
"""Reads and returns a dbf record row as a list of values."""
756
895
f = self .__getFileObj (self .dbf )
757
896
recordContents = self .__recStruct .unpack (f .read (self .__recStruct .size ))
@@ -814,7 +953,8 @@ def __record(self):
814
953
value = u (value , self .encoding , self .encodingErrors )
815
954
value = value .strip ()
816
955
record .append (value )
817
- return record
956
+
957
+ return _Record (self .__fieldposition_lookup , record , oid )
818
958
819
959
def record (self , i = 0 ):
820
960
"""Returns a specific dbf record based on the supplied index."""
@@ -825,7 +965,7 @@ def record(self, i=0):
825
965
recSize = self .__recStruct .size
826
966
f .seek (0 )
827
967
f .seek (self .__dbfHdrLength + (i * recSize ))
828
- return self .__record ()
968
+ return self .__record (oid = i )
829
969
830
970
def records (self ):
831
971
"""Returns all records in a dbf file."""
@@ -835,7 +975,7 @@ def records(self):
835
975
f = self .__getFileObj (self .dbf )
836
976
f .seek (self .__dbfHdrLength )
837
977
for i in range (self .numRecords ):
838
- r = self .__record ()
978
+ r = self .__record (oid = i )
839
979
if r :
840
980
records .append (r )
841
981
return records
0 commit comments