22
33from functools import cached_property
44from itertools import zip_longest
5- from typing import Any , Dict , List , Optional
5+ from typing import Any , Callable , Dict , List , NamedTuple , Optional
66
77from .. import errors , helpers
88from ..platform import platform
1313# We can consider adding row.header property to provide more comprehensive API
1414
1515
16+ class _CellHandler (NamedTuple ):
17+ field : Field
18+ field_number : int
19+ reader : Callable [..., Any ]
20+ writer : Callable [..., Any ]
21+
22+
1623# TODO: add types
1724class Row (Dict [str , Any ]):
1825 """Row representation
@@ -29,46 +36,34 @@ class Row(Dict[str, Any]):
2936
3037 Parameters:
3138 cells (any[]): array of cells
32- field_info (dict ): special field info structure
39+ fields (Field[] ): schema fields, in the order expected in the data
3340 row_number (int): row number from 1
3441 """
3542
3643 def __init__ (
3744 self ,
3845 cells : List [Any ],
3946 * ,
40- field_info : Optional [Dict [str , Any ]] = None ,
41- fields : Optional [List [Field ]] = None ,
47+ fields : List [Field ],
4248 row_number : int ,
4349 ):
44- if field_info is None and fields is None :
45- raise TypeError ("Row requires either 'field_info' or 'fields'" )
46- if field_info is not None and fields is not None :
47- raise TypeError ("Row accepts 'field_info' or 'fields', not both" )
48- if field_info is None :
49- field_info = self .__build_field_info (fields ) # type: ignore[arg-type]
5050 self .__cells = cells
51- self .__field_info = field_info
51+ self .__field_copies : List [Field ] = [field .to_copy () for field in fields ]
52+ self .__handlers : Dict [str , _CellHandler ] = {
53+ field .name : _CellHandler (
54+ field = field ,
55+ field_number = field_number ,
56+ reader = field .create_cell_reader (),
57+ writer = field .create_cell_writer (),
58+ )
59+ for field_number , field in enumerate (fields , start = 1 )
60+ }
5261 self .__row_number = row_number
5362 self .__processed : bool = False
5463 self .__blank_cells : Dict [str , Any ] = {}
5564 self .__error_cells : Dict [str , Any ] = {}
5665 self .__errors : list [errors .RowError ] = []
5766
58- @staticmethod
59- def __build_field_info (fields : List [Field ]) -> Dict [str , Any ]:
60- info : Dict [str , Any ] = {"names" : [], "objects" : [], "mapping" : {}}
61- for field_number , field in enumerate (fields , start = 1 ):
62- info ["names" ].append (field .name )
63- info ["objects" ].append (field .to_copy ())
64- info ["mapping" ][field .name ] = (
65- field ,
66- field_number ,
67- field .create_cell_reader (),
68- field .create_cell_writer (),
69- )
70- return info
71-
7267 def __eq__ (self , other : object ):
7368 self .__process ()
7469 return super ().__eq__ (other )
@@ -87,7 +82,7 @@ def __repr__(self):
8782
8883 def __setitem__ (self , key : str , value : Any ):
8984 try :
90- _ , field_number , _ , _ = self .__field_info [ "mapping" ][ key ]
85+ field_number = self .__handlers [ key ]. field_number
9186 except KeyError :
9287 raise KeyError (f"Row does not have a field { key } " )
9388 if len (self .__cells ) < field_number :
@@ -99,30 +94,30 @@ def __missing__(self, key: str):
9994 return self .__process (key )
10095
10196 def __iter__ (self ):
102- return iter (self .__field_info [ "names" ] )
97+ return iter (self .__handlers )
10398
10499 def __len__ (self ):
105- return len (self .__field_info [ "names" ] )
100+ return len (self .__handlers )
106101
107102 def __contains__ (self , key : object ):
108- return key in self .__field_info [ "mapping" ]
103+ return key in self .__handlers
109104
110105 def __reversed__ (self ):
111- return reversed (self .__field_info [ "names" ] )
106+ return reversed (self .__handlers )
112107
113108 def keys (self ):
114- return iter (self .__field_info [ "names" ] )
109+ return iter (self .__handlers )
115110
116111 def values (self ): # type: ignore
117- for name in self .__field_info [ "names" ] :
112+ for name in self .__handlers :
118113 yield self [name ]
119114
120115 def items (self ): # type: ignore
121- for name in self .__field_info [ "names" ] :
116+ for name in self .__handlers :
122117 yield (name , self [name ])
123118
124119 def get (self , key : str , default : Optional [Any ] = None ):
125- if key not in self .__field_info [ "names" ] :
120+ if key not in self .__handlers :
126121 return default
127122 return self [key ]
128123
@@ -140,23 +135,23 @@ def fields(self):
140135 Returns:
141136 Field[]: table schema fields
142137 """
143- return self .__field_info [ "objects" ]
138+ return self .__field_copies
144139
145140 @cached_property
146141 def field_names (self ) -> List [str ]:
147142 """
148143 Returns:
149144 str[]: field names
150145 """
151- return self .__field_info [ "names" ]
146+ return list ( self .__handlers )
152147
153148 @cached_property
154149 def field_numbers (self ):
155150 """
156151 Returns:
157152 str[]: field numbers
158153 """
159- return list (range (1 , len (self .__field_info [ "names" ] ) + 1 ))
154+ return list (range (1 , len (self .__handlers ) + 1 ))
160155
161156 @cached_property
162157 def row_number (self ) -> int :
@@ -227,22 +222,22 @@ def to_list(self, *, json: bool = False, types: Optional[List[str]] = None):
227222
228223 # Prepare
229224 self .__process ()
230- result = [self [name ] for name in self .__field_info [ "names" ] ]
225+ result = [self [name ] for name in self .__handlers ]
231226 if types is None and json :
232227 types = platform .frictionless_formats .JsonParser .supported_types
233228
234229 # Convert
235230 if types is not None :
236- for index , field_mapping in enumerate (self .__field_info [ "mapping" ] .values ()):
237- field , _ , _ , cell_writer = field_mapping
231+ for index , handler in enumerate (self .__handlers .values ()):
232+ field = handler . field
238233 # Here we can optimize performance if we use a types mapping
239234 if field .type in types :
240235 continue
241236 # NOTE: Move somehow to be in the json plugin
242237 if json is True and field .type == "number" and field .float_number :
243238 continue
244239 cell = result [index ]
245- cell , _ = cell_writer (cell , ignore_missing = True )
240+ cell , _ = handler . writer (cell , ignore_missing = True )
246241 result [index ] = cell
247242
248243 # Return
@@ -261,20 +256,20 @@ def to_dict(
261256
262257 # Prepare
263258 self .__process ()
264- result = {name : self [name ] for name in self .__field_info [ "names" ] }
259+ result = {name : self [name ] for name in self .__handlers }
265260 if types is None and json :
266261 types = platform .frictionless_formats .JsonParser .supported_types
267262 if types is None and csv :
268263 types = platform .frictionless_formats .CsvParser .supported_types
269264
270265 # Convert
271266 if types is not None :
272- for field_mapping in self .__field_info [ "mapping" ] .values ():
273- field , _ , _ , cell_writer = field_mapping
267+ for handler in self .__handlers .values ():
268+ field = handler . field
274269 # Here we can optimize performance if we use a types mapping
275270 if field .type not in types :
276271 cell = result [field .name ]
277- cell , _ = cell_writer (cell , ignore_missing = True )
272+ cell , _ = handler . writer (cell , ignore_missing = True )
278273 result [field .name ] = cell
279274
280275 # Return
@@ -294,31 +289,33 @@ def __process(self, key: Optional[str] = None):
294289 # Prepare context
295290 cells = self .__cells
296291 to_str = lambda v : str (v ) if v is not None else "" # type: ignore
297- fields = self .__field_info ["objects" ]
298- field_mapping = self .__field_info ["mapping" ]
299- iterator = zip_longest (field_mapping .values (), cells )
292+ handlers = self .__handlers
300293 is_empty = not bool (super ().__len__ ())
301294 if key :
302295 try :
303- field , field_number , cell_reader , cell_writer = self .__field_info [
304- "mapping"
305- ][key ]
296+ handler = handlers [key ]
306297 except KeyError :
307298 raise KeyError (f"Row does not have a field { key } " )
308- cell = cells [field_number - 1 ] if len (cells ) >= field_number else None
309- iterator = zip ([(field , field_number , cell_reader , cell_writer )], [cell ])
299+ cell = (
300+ cells [handler .field_number - 1 ]
301+ if len (cells ) >= handler .field_number
302+ else None
303+ )
304+ iterator = zip ([handler ], [cell ])
305+ else :
306+ iterator = zip_longest (handlers .values (), cells )
310307
311308 # Iterate cells
312- for field_mapping , source in iterator :
309+ for handler , source in iterator :
313310 # Prepare context
314- if field_mapping is None :
311+ if handler is None :
315312 break
316- field , field_number , cell_reader , _ = field_mapping
313+ field = handler . field
317314 if not is_empty and super ().__contains__ (field .name ):
318315 continue
319316
320317 # Read cell
321- target , notes = cell_reader (source )
318+ target , notes = handler . reader (source )
322319 type_note = notes .pop ("type" , None ) if notes else None
323320 if target is None and not type_note :
324321 self .__blank_cells [field .name ] = source
@@ -333,7 +330,7 @@ def __process(self, key: Optional[str] = None):
333330 row_number = self .__row_number ,
334331 cell = str (source ),
335332 field_name = field .name ,
336- field_number = field_number ,
333+ field_number = handler . field_number ,
337334 )
338335 )
339336
@@ -347,7 +344,7 @@ def __process(self, key: Optional[str] = None):
347344 row_number = self .__row_number ,
348345 cell = str (source ),
349346 field_name = field .name ,
350- field_number = field_number ,
347+ field_number = handler . field_number ,
351348 )
352349 )
353350
@@ -357,10 +354,10 @@ def __process(self, key: Optional[str] = None):
357354 return target
358355
359356 # Extra cells
360- if len ( fields ) < len (cells ):
361- start = len (fields ) + 1
362- iterator = cells [ len ( fields ) :]
363- for field_number , cell in enumerate (iterator , start = start ):
357+ n_fields = len (handlers )
358+ if n_fields < len (cells ):
359+ start = n_fields + 1
360+ for field_number , cell in enumerate (cells [ n_fields :] , start = start ):
364361 self .__errors .append (
365362 errors .ExtraCellError (
366363 note = "" ,
@@ -373,24 +370,22 @@ def __process(self, key: Optional[str] = None):
373370 )
374371
375372 # Missing cells
376- if len (fields ) > len (cells ):
377- start = len (cells ) + 1
378- iterator = fields [len (cells ) :]
379- for field_number , field in enumerate (iterator , start = start ):
380- if field is not None :
381- self .__errors .append (
382- errors .MissingCellError (
383- note = "" ,
384- cells = list (map (to_str , cells )), # type: ignore
385- row_number = self .__row_number ,
386- cell = "" ,
387- field_name = field .name ,
388- field_number = field_number ,
389- )
373+ if n_fields > len (cells ):
374+ missing_handlers = list (handlers .values ())[len (cells ) :]
375+ for handler in missing_handlers :
376+ self .__errors .append (
377+ errors .MissingCellError (
378+ note = "" ,
379+ cells = list (map (to_str , cells )), # type: ignore
380+ row_number = self .__row_number ,
381+ cell = "" ,
382+ field_name = handler .field .name ,
383+ field_number = handler .field_number ,
390384 )
385+ )
391386
392387 # Blank row
393- if len ( fields ) == len (self .__blank_cells ):
388+ if n_fields == len (self .__blank_cells ):
394389 self .__errors = [
395390 errors .BlankRowError (
396391 note = "" ,
0 commit comments