Skip to content

Commit 1dcbb3b

Browse files
Remove field_info from Row class as well
Replace with CellHandler
1 parent a2937e7 commit 1dcbb3b

2 files changed

Lines changed: 73 additions & 79 deletions

File tree

frictionless/detector/detector.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from .. import helpers, settings
1111
from ..dialect import Dialect
12-
from ..exception import FrictionlessException
1312
from ..fields import AnyField
1413
from ..metadata import Metadata
1514
from ..platform import platform

frictionless/table/row.py

Lines changed: 73 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from functools import cached_property
44
from itertools import zip_longest
5-
from typing import Any, Dict, List, Optional
5+
from typing import Any, Callable, Dict, List, NamedTuple, Optional
66

77
from .. import errors, helpers
88
from ..platform import platform
@@ -13,6 +13,13 @@
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
1724
class 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

Comments
 (0)