NCCOOS Trac Projects: Top | Web | Platforms | Processing | Viz | Sprints | Sandbox | (Wind)

Changeset 266

Show
Ignore:
Timestamp:
12/01/09 18:27:13
Author:
cbc
Message:

More robust Profile class.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • sodar/branches/scintec-branch/sodar/scintec/maindata.py

    r265 r266  
    1010__license__ = 'GPL2' 
    1111 
     12from datetime import datetime, timedelta 
     13 
    1214class MainData(list): 
    1315    """ 
     
    2022    >>> main_data = MainData(good_mnd) 
    2123     
    22     Parse the format header: 
    23     >>> len(main_data._format_header_spec) 
    24     4 
    25     >>> main_data._format_header_spec[0] 
    26     'FORMAT-1' 
    27      
    28     Parse the file header after the format header: 
    29     >>> len(main_data._file_header_body) 
    30     26 
    31     >>> main_data._file_header_body[1] 
    32     '# file information' 
    33      
    3424    Parse the profile data: 
    3525    >>> len(main_data) 
     
    3929    >>> len(main_data[0]) 
    4030    39 
    41     >>> main_data[0].timestamp 
    42     '2009-11-17 00:30:00 00:30:00' 
     31    >>> main_data[0].start 
     32    datetime.datetime(2009, 11, 17, 0, 0) 
     33    >>> main_data[0].stop 
     34    datetime.datetime(2009, 11, 17, 0, 30) 
    4335    >>> main_data[0].variables 
    4436    ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error'] 
     
    4739    ...                     'error':'0'} 
    4840    True 
     41    >>> main_data[0][0].z 
     42    '10' 
     43    >>> main_data[0]['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     44    ...                     'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     45    ...                     'error':'0'} 
     46    True 
     47    >>> main_data[0]['10'].speed 
     48    '99.99' 
    4949    >>> main_data[0][-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
    5050    ...                      'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
    5151    ...                      'error':'0'} 
    5252    True 
     53    >>> main_data[0][-1].dir 
     54    '999.9' 
     55    >>> main_data[0]['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
     56    ...                         'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
     57    ...                         'error':'0'} 
     58    True 
     59    >>> main_data[0]['200'].W 
     60    '-0.07' 
    5361     
    5462    Parse the last profile: 
    5563    >>> len(main_data[-1]) 
    5664    39 
    57     >>> main_data[-1].timestamp 
    58     '2009-11-18 00:00:00 00:30:00' 
     65    >>> main_data[-1].start 
     66    datetime.datetime(2009, 11, 17, 23, 30) 
     67    >>> main_data[-1].stop 
     68    datetime.datetime(2009, 11, 18, 0, 0) 
    5969    >>> main_data[-1].variables 
    6070    ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error'] 
     
    99109        """ 
    100110         
    101         super(MainData, self).__init__() 
     111        super(self.__class__, self).__init__() 
    102112         
    103113        self.file_name = '' 
     
    113123        # Divide the data into blocks 
    114124        # across boundaries separated by blank lines. 
    115         self._blocks = [self._block.strip() 
    116                         for self._block 
    117                         in mnd.split('\n\n') 
    118                         if self._block.strip()] 
     125        blocks = [block.strip() 
     126                  for block 
     127                  in mnd.split('\n\n') 
     128                  if block.strip()] 
    119129         
    120130        # The first block is the specification of the format header. 
    121131        # Divide it into lines. 
    122         self._format_header_spec = [self._line.strip() 
    123                                     for self._line 
    124                                     in self._blocks[0].split('\n') 
    125                                     if self._line.strip()] 
     132        format_header_spec = [line.strip() 
     133                              for line 
     134                              in blocks[0].split('\n') 
     135                              if line.strip()] 
    126136         
    127137        # The second block is the body of the format header. 
    128138        # Divide it into lines. 
    129         self._file_header_body = [self._line.strip() 
    130                                   for self._line 
    131                                   in self._blocks[1].split('\n') 
    132                                   if self._line.strip()] 
     139        file_header_body = [line.strip() 
     140                            for line 
     141                            in blocks[1].split('\n') 
     142                            if line.strip()] 
    133143         
    134144        # All the remaing blocks are individual profiles 
    135145        # in chronological order. 
    136         self.extend([Profile([self._line.strip() 
    137                               for self._line 
    138                               in self._block.split('\n') 
    139                               if self._line.strip()]) 
    140                      for self._block 
    141                      in self._blocks[2:]]) 
    142          
    143         # No need to haul around bookkeeping storage used for initialization. 
    144         del self._blocks, self._block, self._line 
     146        self.extend([Profile([line.strip() 
     147                              for line 
     148                              in block.split('\n') 
     149                              if line.strip()]) 
     150                     for block 
     151                     in blocks[2:]]) 
     152         
    145153 
    146154class Profile(list): 
     
    153161    Parse a known good profile block: 
    154162    >>> profile = Profile(good_profile) 
    155     >>> profile.timestamp 
    156     '2009-11-17 00:30:00 00:30:00' 
     163     
     164    Access the timestamp attributes: 
     165    >>> profile.start 
     166    datetime.datetime(2009, 11, 17, 0, 0) 
     167    >>> profile.stop 
     168    datetime.datetime(2009, 11, 17, 0, 30) 
     169     
     170    Access the variable list: 
    157171    >>> profile.variables 
    158172    ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error'] 
     173     
     174    Access profiles by sequence number and variables by attribute name: 
    159175    >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    160176    ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    161177    ...                'error':'0'} 
    162178    True 
     179    >>> profile[0].z 
     180    '10' 
    163181    >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
    164182    ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
    165183    ...                 'error':'0'} 
    166184    True 
     185    >>> profile[-1].speed 
     186    '99.99' 
     187     
     188    Access profiles by elevation and variables by attribute name: 
     189    >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     190    ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     191    ...                   'error':'0'} 
     192    True 
     193    >>> profile['10'].dir 
     194    '999.9' 
     195    >>> profile['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
     196    ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
     197    ...                    'error':'0'} 
     198    True 
     199    >>> profile['200'].W 
     200    '-0.07' 
    167201    """ 
    168202     
     
    174208         
    175209        Where: 
    176          
     210         
    177211            profile_block is a list of str objects containing all the lines 
    178212            from a single profile in a Scintec .mnd daily sodar file. 
    179  
     213         
    180214        Parse a known good profile block: 
    181215        >>> profile = Profile(good_profile) 
    182         >>> profile.timestamp 
    183         '2009-11-17 00:30:00 00:30:00' 
     216         
     217        Access the timestamp attributes: 
     218        >>> profile.start 
     219        datetime.datetime(2009, 11, 17, 0, 0) 
     220        >>> profile.stop 
     221        datetime.datetime(2009, 11, 17, 0, 30) 
     222         
     223        Access the variable list: 
    184224        >>> profile.variables 
    185225        ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error'] 
    186         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    187         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    188         ...                'error':'0'} 
    189         True 
     226         
     227        Access profiles by sequence number and variables by attribute name: 
     228        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     229        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     230        ...                'error':'0'} 
     231        True 
     232        >>> profile[0].z 
     233        '10' 
    190234        >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
    191235        ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
    192236        ...                 'error':'0'} 
    193237        True 
    194         """ 
    195          
    196         super(Profile, self).__init__() 
     238        >>> profile[-1].speed 
     239        '99.99' 
     240        """ 
     241         
     242        super(self.__class__, self).__init__() 
    197243 
    198244        # The first line in the profile block is the timestamp.        
    199         self.timestamp = profile_block[0] 
     245        timestamp = profile_block[0] 
     246        date, stop, duration = timestamp.split() 
     247        date_year, date_month, date_day = date.split('-') 
     248        stop_hour, stop_minute, stop_second = stop.split(':') 
     249        duration_hour, duration_minute, duration_second = duration.split(':') 
     250        interval = timedelta(0,                    # days 
     251                             int(duration_second), 
     252                             0,                    # microseconds 
     253                             0,                    # milliseconds 
     254                             int(duration_minute), 
     255                             int(duration_hour))   # omit optional weeks 
     256        self.stop = datetime(int(date_year), 
     257                             int(date_month), 
     258                             int(date_day), 
     259                             int(stop_hour), 
     260                             int(stop_minute),     # omit optional microseconds 
     261                             int(stop_second))     # and tzinfo 
     262        self.start = self.stop - interval 
    200263         
    201264        # The second line in the profile block is 
     
    206269        # individual obervation columns in the same order 
    207270        # as the variable names. 
    208         self.extend([Observation(observation.split(),self.variables) 
    209                      for observation in profile_block[2:]]) 
     271        super(self.__class__, self).extend([Observation(observation.split(), 
     272                                                        self.variables) 
     273                                            for observation 
     274                                            in profile_block[2:]]) 
     275     
     276    def __getitem__(self,key): 
     277        """ 
     278        Get a profile by elevation. 
     279         
     280        profile_instance[elevation] -> <Observation object> 
     281         
     282        Where: 
     283         
     284            elevation is a str object object representing the elevation 
     285            (value keyed by 'z') of the Observation object. 
     286         
     287        Access profiles by elevation and variables by attribute name: 
     288        >>> profile = Profile(good_profile) 
     289        >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     290        ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     291        ...                   'error':'0'} 
     292        True 
     293        >>> profile['10'].dir 
     294        '999.9' 
     295        >>> profile['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
     296        ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
     297        ...                    'error':'0'} 
     298        True 
     299        >>> profile['200'].W 
     300        '-0.07' 
     301        """ 
     302         
     303        try: 
     304            return super(self.__class__, self).__getitem__(key) 
     305        except TypeError: 
     306            for observation in self: 
     307                if observation['z'] == key: 
     308                    return observation 
     309            return super(self.__class__, self).__getitem__(key) 
     310     
     311    def __setitem__(self,key,value): 
     312        """ 
     313        Protect items. 
     314         
     315        profile_instance[object1] = object2 
     316         
     317        Raise an Exception if attempting to mutate a profile: 
     318        >>> profile = Profile(good_profile) 
     319        >>> profile[0] = 'OK' 
     320        Traceback (most recent call last): 
     321            ... 
     322        TypeError: 'Profile' instance's items are read-only 
     323        >>> profile['10'] = 'OK' 
     324        Traceback (most recent call last): 
     325            ... 
     326        TypeError: 'Profile' instance's items are read-only 
     327        >>> profile[100000] = 'OK' 
     328        Traceback (most recent call last): 
     329            ... 
     330        TypeError: 'Profile' instance's items are read-only 
     331        >>> profile[100000] 
     332        Traceback (most recent call last): 
     333            ... 
     334        IndexError: list index out of range 
     335        >>> profile['100000'] = 'OK' 
     336        Traceback (most recent call last): 
     337            ... 
     338        TypeError: 'Profile' instance's items are read-only 
     339        >>> profile['100000'] 
     340        Traceback (most recent call last): 
     341            ... 
     342        TypeError: list indices must be integers 
     343         
     344        Without affecting the observation: 
     345        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     346        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     347        ...                'error':'0'} 
     348        True 
     349        >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     350        ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     351        ...                   'error':'0'} 
     352        True 
     353        """ 
     354         
     355        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     356                                   'instance\'s items are read-only',]) 
     357     
     358    def __delitem__(self,key): 
     359        """ 
     360        Protect items. 
     361         
     362        del profile_instance[object] 
     363         
     364        Raise an Exception if attempting to mutate a profile: 
     365        >>> profile = Profile(good_profile) 
     366        >>> del profile[0] 
     367        Traceback (most recent call last): 
     368            ... 
     369        TypeError: 'Profile' instance's items are read-only 
     370        >>> del profile['10'] 
     371        Traceback (most recent call last): 
     372            ... 
     373        TypeError: 'Profile' instance's items are read-only 
     374        >>> del profile[100000] 
     375        Traceback (most recent call last): 
     376            ... 
     377        TypeError: 'Profile' instance's items are read-only 
     378        >>> del profile['100000'] 
     379        Traceback (most recent call last): 
     380            ... 
     381        TypeError: 'Profile' instance's items are read-only 
     382         
     383        Without affecting the observation: 
     384        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     385        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     386        ...                'error':'0'} 
     387        True 
     388        >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     389        ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     390        ...                   'error':'0'} 
     391        True 
     392        """ 
     393         
     394        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     395                                   'instance\'s items are read-only',]) 
     396     
     397    def __setslice__(self,i,j,seq): 
     398        """ 
     399        Protect items. 
     400         
     401        profile_instance[i:j] = seq 
     402         
     403        Raise an Exception if attempting to mutate a profile: 
     404        >>> profile = Profile(good_profile) 
     405        >>> profile[0:10] = 'OK' 
     406        Traceback (most recent call last): 
     407            ... 
     408        TypeError: 'Profile' instance's items are read-only 
     409         
     410        Without affecting the observations: 
     411        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     412        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     413        ...                'error':'0'} 
     414        True 
     415        >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     416        ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     417        ...                   'error':'0'} 
     418        True 
     419        """ 
     420         
     421        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     422                                   'instance\'s items are read-only',]) 
     423     
     424    def __delslice__(self,i,j): 
     425        """ 
     426        Protect items. 
     427         
     428        del profile_instance[i:j] 
     429         
     430        Raise an Exception if attempting to mutate a profile: 
     431        >>> profile = Profile(good_profile) 
     432        >>> del profile[0:10] 
     433        Traceback (most recent call last): 
     434            ... 
     435        TypeError: 'Profile' instance's items are read-only 
     436         
     437        Without affecting the observations: 
     438        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     439        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     440        ...                'error':'0'} 
     441        True 
     442        >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     443        ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     444        ...                   'error':'0'} 
     445        True 
     446        """ 
     447         
     448        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     449                                   'instance\'s items are read-only',]) 
     450     
     451    def append(self,item): 
     452        """ 
     453        Protect items. 
     454         
     455        Raise an Exception if attempting to mutate a profile: 
     456        >>> profile = Profile(good_profile) 
     457        >>> profile.append('OK') 
     458        Traceback (most recent call last): 
     459            ... 
     460        TypeError: 'Profile' instance's items are read-only 
     461         
     462        Without affecting the observations: 
     463        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     464        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     465        ...                'error':'0'} 
     466        True 
     467        """ 
     468         
     469        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     470                                   'instance\'s items are read-only',]) 
     471     
     472    def extend(self,seq): 
     473        """ 
     474        Protect items. 
     475         
     476        Raise an Exception if attempting to mutate a profile: 
     477        >>> profile = Profile(good_profile) 
     478        >>> profile.extend('OK') 
     479        Traceback (most recent call last): 
     480            ... 
     481        TypeError: 'Profile' instance's items are read-only 
     482         
     483        Without affecting the observations: 
     484        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     485        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     486        ...                'error':'0'} 
     487        True 
     488        """ 
     489         
     490        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     491                                   'instance\'s items are read-only',]) 
     492     
     493    def insert(self,i,item): 
     494        """ 
     495        Protect items. 
     496         
     497        Raise an Exception if attempting to mutate a profile: 
     498        >>> profile = Profile(good_profile) 
     499        >>> profile.insert(0,'OK') 
     500        Traceback (most recent call last): 
     501            ... 
     502        TypeError: 'Profile' instance's items are read-only 
     503         
     504        Without affecting the observations: 
     505        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     506        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     507        ...                'error':'0'} 
     508        True 
     509        """ 
     510         
     511        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     512                                   'instance\'s items are read-only',]) 
     513     
     514    def pop(self,*args): 
     515        """ 
     516        Protect items. 
     517         
     518        Raise an Exception if attempting to mutate a profile: 
     519        >>> profile = Profile(good_profile) 
     520        >>> profile.pop() 
     521        Traceback (most recent call last): 
     522            ... 
     523        TypeError: 'Profile' instance's items are read-only 
     524        >>> profile.pop(0) 
     525        Traceback (most recent call last): 
     526            ... 
     527        TypeError: 'Profile' instance's items are read-only 
     528         
     529        Without affecting the observations: 
     530        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     531        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     532        ...                'error':'0'} 
     533        True 
     534        >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9', 
     535        ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37', 
     536        ...                 'error':'0'} 
     537        True 
     538        """ 
     539         
     540        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     541                                   'instance\'s items are read-only',]) 
     542     
     543    def remove(self,i): 
     544        """ 
     545        Protect items. 
     546         
     547        Raise an Exception if attempting to mutate a profile: 
     548        >>> profile = Profile(good_profile) 
     549        >>> observation = profile[0] 
     550        >>> profile.remove(observation) 
     551        Traceback (most recent call last): 
     552            ... 
     553        TypeError: 'Profile' instance's items are read-only 
     554         
     555        Without affecting the observations: 
     556        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     557        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     558        ...                'error':'0'} 
     559        True 
     560        """ 
     561         
     562        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     563                                   'instance\'s items are read-only',]) 
     564     
     565    def reverse(self): 
     566        """ 
     567        Protect items. 
     568         
     569        Raise an Exception if attempting to mutate a profile: 
     570        >>> profile = Profile(good_profile) 
     571        >>> profile.reverse() 
     572        Traceback (most recent call last): 
     573            ... 
     574        TypeError: 'Profile' instance's items are read-only 
     575         
     576        Without affecting the observations: 
     577        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     578        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     579        ...                'error':'0'} 
     580        True 
     581        """ 
     582         
     583        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     584                                   'instance\'s items are read-only',]) 
     585     
     586    def sort(self,**kwargs): 
     587        """ 
     588        Protect items. 
     589         
     590        Raise an Exception if attempting to mutate a profile: 
     591        >>> profile = Profile(good_profile) 
     592        >>> profile.sort() 
     593        Traceback (most recent call last): 
     594            ... 
     595        TypeError: 'Profile' instance's items are read-only 
     596        >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower())) 
     597        Traceback (most recent call last): 
     598            ... 
     599        TypeError: 'Profile' instance's items are read-only 
     600        >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()), 
     601        ...              key=str.lower) 
     602        Traceback (most recent call last): 
     603            ... 
     604        TypeError: 'Profile' instance's items are read-only 
     605        >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()), 
     606        ...              key=str.lower, 
     607        ...              reverse=True) 
     608        Traceback (most recent call last): 
     609            ... 
     610        TypeError: 'Profile' instance's items are read-only 
     611         
     612         
     613        Without affecting the observations: 
     614        >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     615        ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     616        ...                'error':'0'} 
     617        True 
     618        """ 
     619         
     620        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     621                                   'instance\'s items are read-only',]) 
    210622 
    211623class Observation(dict): 
     
    223635    ...                 'error':'0'} 
    224636    True 
     637     
     638    Access observation variables as attributes: 
    225639    >>> observation.z 
    226640    '10' 
     
    257671        ...                 'error':'0'} 
    258672        True 
     673         
     674        Raise an Exception if more variable names than values: 
    259675        >>> extra_variables = good_variables + ['extra'] 
    260676        >>> observation = Observation(good_observation,extra_variables) 
     
    263679        AttributeError: Same number of attributes and values required in 
    264680        'Observation' object 
     681         
     682        Raise an Exception if more variable values than names: 
    265683        >>> extra_observation = good_observation + ['extra'] 
    266684        >>> observation = Observation(extra_observation,good_variables) 
     
    289707        observation_instance.variable_name -> variable_value 
    290708         
     709        Access observation variables as attributes: 
    291710        >>> observation = Observation(good_observation,good_variables) 
    292711        >>> observation.z 
     
    304723        >>> observation.error 
    305724        '0' 
     725         
     726        Raise an Exception for an undefined attribute: 
    306727        >>> observation.y 
    307728        Traceback (most recent call last): 
    308729            ... 
    309730        AttributeError: 'Observation' object has no attribute 'y' 
     731         
     732        Allow the creation and access of attributes other than variables. 
    310733        >>> observation.y = 'OK' 
    311734        >>> observation.y 
    312735        'OK' 
     736         
     737        Without affecting the variables: 
    313738        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    314739        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     
    332757         
    333758        But retrieve any other bound attribute values. 
    334  
     759         
     760        Raise an Exception if attempting to rebind a variable: 
    335761        >>> observation = Observation(good_observation,good_variables) 
    336762        >>> observation.z = 'OK' 
     
    339765        AttributeError: Rebinding attribute 'z' disallowed in 
    340766        'Observation' object 
     767         
     768        However, allow attributes other than variables to be bound: 
    341769        >>> observation.y = 'OK' 
    342770        >>> observation.y 
    343771        'OK' 
     772         
     773        Without affecting the variables: 
    344774        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    345775        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     
    366796         
    367797        But delete any other bound attribute values. 
    368  
     798         
     799        Raise an Exception if attempting to unbind a variable: 
    369800        >>> observation = Observation(good_observation,good_variables) 
    370801        >>> del observation.z 
     
    373804        AttributeError: Unbinding attribute 'z' disallowed in 
    374805        'Observation' object 
     806         
     807        Raise an Exception for attempting to unbind an undefined attribute: 
    375808        >>> del observation.y 
    376809        Traceback (most recent call last): 
    377810            ... 
    378811        AttributeError: 'Observation' object has no attribute 'y' 
     812         
     813        However, allow attributes other than variables to be unbound: 
    379814        >>> observation.y = 'OK' 
    380815        >>> del observation.y 
     
    383818            ... 
    384819        AttributeError: y 
     820         
     821        Without affecting the variables: 
    385822        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    386823        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     
    406843        observation_instance[variable_name] = variable_value 
    407844         
     845        Raise an Exception if attempting to mutate an observation: 
    408846        >>> observation = Observation(good_observation,good_variables) 
    409847        >>> observation['z'] = 'OK' 
    410848        Traceback (most recent call last): 
    411849            ... 
    412         TypeError: 'Observation' object's items are read-only 
     850        TypeError: 'Observation' instance's items are read-only 
    413851        >>> observation['y'] = 'OK' 
    414852        Traceback (most recent call last): 
    415853            ... 
    416         TypeError: 'Observation' object's items are read-only 
    417         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    418         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    419         ...                 'error':'0'} 
    420         True 
    421         """ 
    422          
    423         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    424                                    'object\'s items are read-only',]) 
     854        TypeError: 'Observation' instance's items are read-only 
     855        >>> observation.y 
     856        Traceback (most recent call last): 
     857            ... 
     858        AttributeError: 'Observation' object has no attribute 'y' 
     859        >>> observation.y = 'OK' 
     860        >>> observation['y'] = 'Not OK' 
     861        Traceback (most recent call last): 
     862            ... 
     863        TypeError: 'Observation' instance's items are read-only 
     864        >>> observation.y 
     865        'OK' 
     866         
     867        Without affecting the variables: 
     868        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     869        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     870        ...                 'error':'0'} 
     871        True 
     872        """ 
     873         
     874        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     875                                   'instance\'s items are read-only',]) 
    425876     
    426877    def __delitem__(self,key): 
     
    430881        observation_instance[variable_name] = variable_value 
    431882         
     883        Raise an Exception if attempting to mutate an observation: 
    432884        >>> observation = Observation(good_observation,good_variables) 
    433885        >>> del observation['z'] 
    434886        Traceback (most recent call last): 
    435887            ... 
    436         TypeError: 'Observation' object's items are read-only 
     888        TypeError: 'Observation' instance's items are read-only 
     889        >>> observation.y = 'OK' 
    437890        >>> del observation['y'] 
    438891        Traceback (most recent call last): 
    439892            ... 
    440         TypeError: 'Observation' object's items are read-only 
    441         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    442         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    443         ...                 'error':'0'} 
    444         True 
    445         """ 
    446          
    447         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    448                                    'object\'s items are read-only',]) 
     893        TypeError: 'Observation' instance's items are read-only 
     894         
     895        Without affecting the variables: 
     896        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     897        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     898        ...                 'error':'0'} 
     899        True 
     900        """ 
     901         
     902        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     903                                   'instance\'s items are read-only',]) 
    449904     
    450905    def clear(self): 
     
    452907        Protect items. 
    453908         
     909        Raise an Exception if attempting to mutate an observation: 
    454910        >>> observation = Observation(good_observation,good_variables) 
    455911        >>> observation.clear() 
    456912        Traceback (most recent call last): 
    457913            ... 
    458         TypeError: 'Observation' object's items are read-only 
    459         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    460         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    461         ...                 'error':'0'} 
    462         True 
    463         """ 
    464          
    465         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    466                                    'object\'s items are read-only',]) 
     914        TypeError: 'Observation' instance's items are read-only 
     915         
     916        Without affecting the variables: 
     917        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     918        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     919        ...                 'error':'0'} 
     920        True 
     921        """ 
     922         
     923        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     924                                   'instance\'s items are read-only',]) 
    467925     
    468926    def pop(self,key,*args): 
     
    470928        Protect items. 
    471929         
     930        Raise an Exception if attempting to mutate an observation: 
    472931        >>> observation = Observation(good_observation,good_variables) 
    473932        >>> observation.pop('z') 
    474933        Traceback (most recent call last): 
    475934            ... 
    476         TypeError: 'Observation' object's items are read-only 
     935        TypeError: 'Observation' instance's items are read-only 
    477936        >>> observation.pop('y') 
    478937        Traceback (most recent call last): 
    479938            ... 
    480         TypeError: 'Observation' object's items are read-only 
     939        TypeError: 'Observation' instance's items are read-only 
    481940        >>> observation.pop('y','OK') 
    482941        Traceback (most recent call last): 
    483942            ... 
    484         TypeError: 'Observation' object's items are read-only 
    485         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    486         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    487         ...                 'error':'0'} 
    488         True 
    489         """ 
    490          
    491         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    492                                    'object\'s items are read-only',]) 
     943        TypeError: 'Observation' instance's items are read-only 
     944         
     945        Without affecting the variables: 
     946        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     947        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     948        ...                 'error':'0'} 
     949        True 
     950        """ 
     951         
     952        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     953                                   'instance\'s items are read-only',]) 
    493954     
    494955    def popitem(self): 
     
    496957        Protect items. 
    497958         
     959        Raise an Exception if attempting to mutate an observation: 
    498960        >>> observation = Observation(good_observation,good_variables) 
    499961        >>> observation.popitem() 
    500962        Traceback (most recent call last): 
    501963            ... 
    502         TypeError: 'Observation' object's items are read-only 
    503         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    504         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    505         ...                 'error':'0'} 
    506         True 
    507         """ 
    508          
    509         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    510                                    'object\'s items are read-only',]) 
     964        TypeError: 'Observation' instance's items are read-only 
     965         
     966        Without affecting the variables: 
     967        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     968        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     969        ...                 'error':'0'} 
     970        True 
     971        """ 
     972         
     973        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     974                                   'instance\'s items are read-only',]) 
    511975     
    512976    def setdefault(self,key,*args): 
     
    514978        Protect items. 
    515979         
     980        Raise an Exception if attempting to mutate an observation: 
    516981        >>> observation = Observation(good_observation,good_variables) 
    517982        >>> observation.setdefault('z') 
    518983        Traceback (most recent call last): 
    519984            ... 
    520         TypeError: 'Observation' object's items are read-only 
     985        TypeError: 'Observation' instance's items are read-only 
    521986        >>> observation.setdefault('y') 
    522987        Traceback (most recent call last): 
    523988            ... 
    524         TypeError: 'Observation' object's items are read-only 
     989        TypeError: 'Observation' instance's items are read-only 
    525990        >>> observation.setdefault('y','OK') 
    526991        Traceback (most recent call last): 
    527992            ... 
    528         TypeError: 'Observation' object's items are read-only 
    529         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    530         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    531         ...                 'error':'0'} 
    532         True 
    533         """ 
    534          
    535         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    536                                    'object\'s items are read-only',]) 
     993        TypeError: 'Observation' instance's items are read-only 
     994         
     995        Without affecting the variables: 
     996        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     997        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     998        ...                 'error':'0'} 
     999        True 
     1000        """ 
     1001         
     1002        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     1003                                   'instance\'s items are read-only',]) 
    5371004     
    5381005    def update(self,*args,**kwargs): 
     
    5401007        Protect items. 
    5411008         
     1009        Raise an Exception if attempting to mutate an observation: 
    5421010        >>> observation = Observation(good_observation,good_variables) 
    5431011        >>> observation.update({'UP':'up','DOWN':'down',}) 
    5441012        Traceback (most recent call last): 
    5451013            ... 
    546         TypeError: 'Observation' object's items are read-only 
    547         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    548         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    549         ...                 'error':'0'} 
    550         True 
     1014        TypeError: 'Observation' instance's items are read-only 
     1015         
     1016        Without affecting the variables: 
     1017        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     1018        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     1019        ...                 'error':'0'} 
     1020        True 
     1021         
     1022        Raise an Exception if attempting to mutate an observation: 
    5511023        >>> observation.update((('UP','up'),('DOWN','down'),)) 
    5521024        Traceback (most recent call last): 
    5531025            ... 
    554         TypeError: 'Observation' object's items are read-only 
    555         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    556         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    557         ...                 'error':'0'} 
    558         True 
     1026        TypeError: 'Observation' instance's items are read-only 
     1027 
     1028        Without affecting the variables: 
     1029        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     1030        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     1031        ...                 'error':'0'} 
     1032        True 
     1033         
     1034        Raise an Exception if attempting to mutate an observation: 
    5591035        >>> observation.update(UP='up',DOWN='down') 
    5601036        Traceback (most recent call last): 
    5611037            ... 
    562         TypeError: 'Observation' object's items are read-only 
    563         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
    564         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
    565         ...                 'error':'0'} 
    566         True 
    567         """ 
    568          
    569         raise TypeError, ' '.join([repr(self.__class__.__name__), 
    570                                    'object\'s items are read-only',]) 
     1038        TypeError: 'Observation' instance's items are read-only 
     1039         
     1040        Without affecting the variables: 
     1041        >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9', 
     1042        ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03', 
     1043        ...                 'error':'0'} 
     1044        True 
     1045        """ 
     1046         
     1047        raise TypeError, ' '.join([repr(self.__class__.__name__), 
     1048                                   'instance\'s items are read-only',]) 
    5711049 
    5721050def _test():