From ee4ad2f95134f584d7eb2af3f267074d0d42dd13 Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Fri, 17 Jan 2025 10:37:40 +0100 Subject: [PATCH 1/6] first attempt --- frictionless/resources/table.py | 33 ++----------- frictionless/table/row.py | 85 ++++++++++++++++++++------------- 2 files changed, 56 insertions(+), 62 deletions(-) diff --git a/frictionless/resources/table.py b/frictionless/resources/table.py index 5891ace0b9..d47ab131a2 100644 --- a/frictionless/resources/table.py +++ b/frictionless/resources/table.py @@ -14,6 +14,7 @@ from ..indexer import Indexer from ..platform import platform from ..resource import Resource +from ..schema.fields_info import FieldsInfo from ..system import system from ..table import Header, Lookup, Row, Table from ..transformer import Transformer @@ -263,24 +264,7 @@ def __open_lookup(self): self.__lookup[source_name][source_key].add(cells) def __open_row_stream(self): - # TODO: we need to rework this field_info / row code - # During row streaming we create a field info structure - # This structure is optimized and detached version of schema.fields - # We create all data structures in-advance to share them between rows - - # Create field info - field_number = 0 - field_info: Dict[str, Any] = {"names": [], "objects": [], "mapping": {}} - for field in self.schema.fields: - field_number += 1 - field_info["names"].append(field.name) - field_info["objects"].append(field.to_copy()) - field_info["mapping"][field.name] = ( - field, - field_number, - field.create_cell_reader(), - field.create_cell_writer(), - ) + field_info = FieldsInfo(self.schema.fields) # Create state memory_unique: Dict[str, Any] = {} @@ -403,13 +387,13 @@ def row_stream(): self.__row_stream = row_stream() def remove_missing_required_label_from_field_info( - self, field: Field, field_info: Dict[str, Any] + self, field: Field, fields_info: FieldsInfo ): is_case_sensitive = self.dialect.header_case if self.label_is_missing( - field.name, field_info["names"], self.labels, is_case_sensitive + field.name, fields_info.ls(), self.labels, is_case_sensitive ): - self.remove_field_from_field_info(field.name, field_info) + fields_info.rm(field.name) @staticmethod def label_is_missing( @@ -430,13 +414,6 @@ def label_is_missing( return field_name not in table_labels and field_name in expected_field_names - @staticmethod - def remove_field_from_field_info(field_name: str, field_info: Dict[str, Any]): - field_index = field_info["names"].index(field_name) - del field_info["names"][field_index] - del field_info["objects"][field_index] - del field_info["mapping"][field_name] - def primary_key_cells(self, row: Row, case_sensitive: bool) -> Tuple[Any, ...]: """Create a tuple containg all cells from a given row associated to primary keys""" diff --git a/frictionless/table/row.py b/frictionless/table/row.py index b2947ba677..780d454e15 100644 --- a/frictionless/table/row.py +++ b/frictionless/table/row.py @@ -6,6 +6,7 @@ from .. import errors, helpers from ..platform import platform +from ..schema.fields_info import FieldsInfo # NOTE: # Currently dict.update/setdefault/pop/popitem/clear is not disabled (can be confusing) @@ -36,11 +37,11 @@ def __init__( self, cells: List[Any], *, - field_info: Dict[str, Any], + field_info: FieldsInfo, row_number: int, ): self.__cells = cells - self.__field_info = field_info + self.__fields_info = field_info self.__row_number = row_number self.__processed: bool = False self.__blank_cells: Dict[str, Any] = {} @@ -61,7 +62,7 @@ def __repr__(self): def __setitem__(self, key: str, value: Any): try: - _, field_number, _, _ = self.__field_info["mapping"][key] + field_number = self.__fields_info.get(key).field_number except KeyError: raise KeyError(f"Row does not have a field {key}") if len(self.__cells) < field_number: @@ -73,38 +74,38 @@ def __missing__(self, key: str): return self.__process(key) def __iter__(self): - return iter(self.__field_info["names"]) + return iter(self.__fields_info.ls()) def __len__(self): - return len(self.__field_info["names"]) + return len(self.__fields_info.ls()) def __contains__(self, key: object): - return key in self.__field_info["mapping"] + return key in self.__fields_info.ls() def __reversed__(self): - return reversed(self.__field_info["names"]) + return reversed(self.__fields_info.ls()) def keys(self): - return iter(self.__field_info["names"]) + return iter(self.__fields_info.ls()) def values(self): # type: ignore - for name in self.__field_info["names"]: + for name in self.__fields_info.ls(): yield self[name] def items(self): # type: ignore - for name in self.__field_info["names"]: + for name in self.__fields_info.ls(): yield (name, self[name]) def get(self, key: str, default: Optional[Any] = None): - if key not in self.__field_info["names"]: + if key not in self.__fields_info.ls(): return default return self[key] @cached_property def cells(self): """ - Returns: - Field[]: table schema fields + .ls(): + Field[]: table schema fields """ return self.__cells @@ -114,7 +115,7 @@ def fields(self): Returns: Field[]: table schema fields """ - return self.__field_info["objects"] + return self.__fields_info.get_copies() @cached_property def field_names(self) -> List[str]: @@ -122,7 +123,7 @@ def field_names(self) -> List[str]: Returns: str[]: field names """ - return self.__field_info["names"] + return self.__fields_info.ls() @cached_property def field_numbers(self): @@ -130,7 +131,7 @@ def field_numbers(self): Returns: str[]: field numbers """ - return list(range(1, len(self.__field_info["names"]) + 1)) + return list(range(1, len(self.__fields_info.ls()) + 1)) @cached_property def row_number(self) -> int: @@ -201,14 +202,18 @@ def to_list(self, *, json: bool = False, types: Optional[List[str]] = None): # Prepare self.__process() - result = [self[name] for name in self.__field_info["names"]] + result = [self[name] for name in self.__fields_info.ls()] if types is None and json: types = platform.frictionless_formats.JsonParser.supported_types # Convert if types is not None: - for index, field_mapping in enumerate(self.__field_info["mapping"].values()): - field, _, _, cell_writer = field_mapping + field_names = self.__fields_info.ls() + for index, field_name in enumerate(field_names): + field_info = self.__fields_info.get(field_name) + field = field_info.field + cell_writer = field_info.cell_writer + # Here we can optimize performance if we use a types mapping if field.type in types: continue @@ -223,7 +228,11 @@ def to_list(self, *, json: bool = False, types: Optional[List[str]] = None): return result def to_dict( - self, *, csv: bool = False, json: bool = False, types: Optional[List[str]] = None + self, + *, + csv: bool = False, + json: bool = False, + types: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Parameters: @@ -235,7 +244,7 @@ def to_dict( # Prepare self.__process() - result = {name: self[name] for name in self.__field_info["names"]} + result = {name: self[name] for name in self.__fields_info.ls()} if types is None and json: types = platform.frictionless_formats.JsonParser.supported_types if types is None and csv: @@ -243,8 +252,12 @@ def to_dict( # Convert if types is not None: - for field_mapping in self.__field_info["mapping"].values(): - field, _, _, cell_writer = field_mapping + field_names = self.__fields_info.ls() + for field_name in field_names: + field_info = self.__fields_info.get(field_name) + field = field_info.field + cell_writer = field_info.cell_writer + # Here we can optimize performance if we use a types mapping if field.type not in types: cell = result[field.name] @@ -268,26 +281,30 @@ def __process(self, key: Optional[str] = None): # Prepare context cells = self.__cells to_str = lambda v: str(v) if v is not None else "" # type: ignore - fields = self.__field_info["objects"] - field_mapping = self.__field_info["mapping"] - iterator = zip_longest(field_mapping.values(), cells) + fields = self.__fields_info.get_copies() + names = self.__fields_info.ls() + field_infos = [self.__fields_info.get(name) for name in names] + iterator = zip_longest(field_infos, cells) is_empty = not bool(super().__len__()) + if key: try: - field, field_number, cell_reader, cell_writer = self.__field_info[ - "mapping" - ][key] - except KeyError: + field_info = self.__fields_info.get(key) + field_number = field_info.field_number + except ValueError: raise KeyError(f"Row does not have a field {key}") cell = cells[field_number - 1] if len(cells) >= field_number else None - iterator = zip([(field, field_number, cell_reader, cell_writer)], [cell]) + iterator = zip([field_info], [cell]) # Iterate cells - for field_mapping, source in iterator: + for field_info, source in iterator: # Prepare context - if field_mapping is None: + if field_info is None: break - field, field_number, cell_reader, _ = field_mapping + field = field_info.field + field_number = field_info.field_number + cell_reader = field_info.cell_reader + if not is_empty and super().__contains__(field.name): continue From 39aa0f2afdf88c210d132e86d8b2a74ce4c6794a Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Fri, 24 Jan 2025 15:14:15 +0100 Subject: [PATCH 2/6] squash! first attempt --- frictionless/schema/fields_info.py | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 frictionless/schema/fields_info.py diff --git a/frictionless/schema/fields_info.py b/frictionless/schema/fields_info.py new file mode 100644 index 0000000000..081e435600 --- /dev/null +++ b/frictionless/schema/fields_info.py @@ -0,0 +1,47 @@ +from typing import List + +from frictionless.schema.field import Field + + +class FieldInfo: + """Private class to store additional data to a field""" + + def __init__(self, field: Field, field_number: int): + """field_number: 1-indexed rank of the field""" + self.field = field + self.field_number = field_number + self.cell_reader = field.create_cell_reader() + self.cell_writer = field.create_cell_writer() + + +class FieldsInfo: + def __init__(self, fields: List[Field]): + self._fields: List[FieldInfo] = [ + FieldInfo(field, i + 1) for i, field in enumerate(fields) + ] + + def ls(self) -> List[str]: + """List all field names""" + return [fi.field.name for fi in self._fields] + + def get(self, field_name: str) -> FieldInfo: + """Get a Field by its name + + Raises: + ValueError: Field with name fieldname does not exist + """ + try: + return next(fi for fi in self._fields if fi.field.name == field_name) + except StopIteration: + raise ValueError(f"'{field_name}' is not in fields data") + + def get_copies(self) -> List[Field]: + """Return field copies""" + return [fi.field.to_copy() for fi in self._fields] + + def rm(self, field_name: str): + try: + i = self.ls().index(field_name) + del self._fields[i] + except ValueError: + raise ValueError(f"'{field_name}' is not in fields data") From 70aadac6d2bb23be42e3e148ee6e4e2bfce06eb7 Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Fri, 24 Jan 2025 15:25:24 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=B5=20Mv=20to=20resources/table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frictionless/resources/table.py | 51 +++++++++++++++++++++++++++++- frictionless/schema/fields_info.py | 44 -------------------------- frictionless/table/row.py | 6 ++-- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/frictionless/resources/table.py b/frictionless/resources/table.py index d47ab131a2..9ca502f818 100644 --- a/frictionless/resources/table.py +++ b/frictionless/resources/table.py @@ -14,7 +14,6 @@ from ..indexer import Indexer from ..platform import platform from ..resource import Resource -from ..schema.fields_info import FieldsInfo from ..system import system from ..table import Header, Lookup, Row, Table from ..transformer import Transformer @@ -677,3 +676,53 @@ def write( self, target: Optional[Union[Resource, Any]] = None, **options: Any ) -> TableResource: return self.write_table(target, **options) + + +class _FieldInfo: + """Private class to store additional data alongside a field.""" + + def __init__(self, field: Field, field_number: int): + """field_number: 1-indexed rank of the field""" + self.field = field + self.field_number = field_number + self.cell_reader = field.create_cell_reader() + self.cell_writer = field.create_cell_writer() + + +class FieldsInfo: + """Helper class to store additional data to a collection of fields + + This class is not Public API, and should be used only in non-public + interfaces. + """ + + def __init__(self, fields: List[Field]): + self._fields: List[_FieldInfo] = [ + _FieldInfo(field, i + 1) for i, field in enumerate(fields) + ] + + def ls(self) -> List[str]: + """List all field names""" + return [fi.field.name for fi in self._fields] + + def get(self, field_name: str) -> _FieldInfo: + """Get a Field by its name + + Raises: + ValueError: Field with name fieldname does not exist + """ + try: + return next(fi for fi in self._fields if fi.field.name == field_name) + except StopIteration: + raise ValueError(f"'{field_name}' is not in fields data") + + def get_copies(self) -> List[Field]: + """Return field copies""" + return [fi.field.to_copy() for fi in self._fields] + + def rm(self, field_name: str): + try: + i = self.ls().index(field_name) + del self._fields[i] + except ValueError: + raise ValueError(f"'{field_name}' is not in fields data") diff --git a/frictionless/schema/fields_info.py b/frictionless/schema/fields_info.py index 081e435600..0dc1c83ba7 100644 --- a/frictionless/schema/fields_info.py +++ b/frictionless/schema/fields_info.py @@ -1,47 +1,3 @@ from typing import List from frictionless.schema.field import Field - - -class FieldInfo: - """Private class to store additional data to a field""" - - def __init__(self, field: Field, field_number: int): - """field_number: 1-indexed rank of the field""" - self.field = field - self.field_number = field_number - self.cell_reader = field.create_cell_reader() - self.cell_writer = field.create_cell_writer() - - -class FieldsInfo: - def __init__(self, fields: List[Field]): - self._fields: List[FieldInfo] = [ - FieldInfo(field, i + 1) for i, field in enumerate(fields) - ] - - def ls(self) -> List[str]: - """List all field names""" - return [fi.field.name for fi in self._fields] - - def get(self, field_name: str) -> FieldInfo: - """Get a Field by its name - - Raises: - ValueError: Field with name fieldname does not exist - """ - try: - return next(fi for fi in self._fields if fi.field.name == field_name) - except StopIteration: - raise ValueError(f"'{field_name}' is not in fields data") - - def get_copies(self) -> List[Field]: - """Return field copies""" - return [fi.field.to_copy() for fi in self._fields] - - def rm(self, field_name: str): - try: - i = self.ls().index(field_name) - del self._fields[i] - except ValueError: - raise ValueError(f"'{field_name}' is not in fields data") diff --git a/frictionless/table/row.py b/frictionless/table/row.py index 780d454e15..74e7883c2f 100644 --- a/frictionless/table/row.py +++ b/frictionless/table/row.py @@ -2,16 +2,18 @@ from functools import cached_property from itertools import zip_longest -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional from .. import errors, helpers from ..platform import platform -from ..schema.fields_info import FieldsInfo # NOTE: # Currently dict.update/setdefault/pop/popitem/clear is not disabled (can be confusing) # We can consider adding row.header property to provide more comprehensive API +if TYPE_CHECKING: + from ..resources.table import FieldsInfo + # TODO: add types class Row(Dict[str, Any]): From add8dacf7c790cd1dd0b924c84f6012131e8f9b2 Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Mon, 27 Jan 2025 15:53:16 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=94=B5=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frictionless/detector/detector.py | 6 +++--- frictionless/resources/table.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frictionless/detector/detector.py b/frictionless/detector/detector.py index fc3e47e0a1..84acdccf88 100644 --- a/frictionless/detector/detector.py +++ b/frictionless/detector/detector.py @@ -415,8 +415,8 @@ def detect_schema( note = '"schema_sync" requires unique labels in the header' raise FrictionlessException(note) - mapped_fields = self.mapped_schema_fields_names( - schema.fields, # type: ignore + mapped_fields = self.map_schema_fields_by_name( + schema.fields, case_sensitive, ) @@ -445,7 +445,7 @@ def detect_schema( return schema @staticmethod - def mapped_schema_fields_names( + def map_schema_fields_by_name( fields: List[Field], case_sensitive: bool ) -> Dict[str, Field]: """Create a dictionnary to map field names with schema fields""" diff --git a/frictionless/resources/table.py b/frictionless/resources/table.py index 9ca502f818..ca2d69e231 100644 --- a/frictionless/resources/table.py +++ b/frictionless/resources/table.py @@ -382,7 +382,6 @@ def row_stream(): for field in self.schema.fields: self.remove_missing_required_label_from_field_info(field, field_info) - # Create row stream self.__row_stream = row_stream() def remove_missing_required_label_from_field_info( From e26393b4349137ccf12515ffeb8e3676c77cd74b Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Mon, 27 Jan 2025 17:31:21 +0100 Subject: [PATCH 5/6] Schema sync functionnality inside FieldsInfo Test passes, surprisingly. No special effort has been made to support `header_case` option, or "required" columns with `schema_sync` --- frictionless/detector/detector.py | 45 +++++++++++----------- frictionless/resources/table.py | 62 ++++++++++++++++++++++--------- frictionless/table/row.py | 8 ++-- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/frictionless/detector/detector.py b/frictionless/detector/detector.py index 84acdccf88..fb7b421d16 100644 --- a/frictionless/detector/detector.py +++ b/frictionless/detector/detector.py @@ -404,31 +404,30 @@ def detect_schema( schema.fields = fields # type: ignore # Sync schema - if self.schema_sync: - if labels: - case_sensitive = options["header_case"] + if self.schema_sync and labels: + case_sensitive = options["header_case"] - if not case_sensitive: - labels = [label.lower() for label in labels] + if not case_sensitive: + labels = [label.lower() for label in labels] - if len(labels) != len(set(labels)): - note = '"schema_sync" requires unique labels in the header' - raise FrictionlessException(note) + if len(labels) != len(set(labels)): + note = '"schema_sync" requires unique labels in the header' + raise FrictionlessException(note) - mapped_fields = self.map_schema_fields_by_name( - schema.fields, - case_sensitive, - ) + mapped_fields = self.map_schema_fields_by_name( + schema.fields, + case_sensitive, + ) - self.rearrange_schema_fields_given_labels( - mapped_fields, - schema, - labels, - ) + self.rearrange_schema_fields_given_labels( + mapped_fields, + schema, + labels, + ) - self.add_missing_required_labels_to_schema_fields( - mapped_fields, schema, labels, case_sensitive - ) + self.add_missing_required_labels_to_schema_fields( + mapped_fields, schema, labels, case_sensitive + ) # Patch schema if self.schema_patch: @@ -460,8 +459,10 @@ def rearrange_schema_fields_given_labels( schema: Schema, labels: List[str], ): - """Rearrange fields according to the order of labels. All fields - missing from labels are dropped""" + """Rearrange fields according to the order of labels. + All fields missing from labels are dropped. + Any extra-field is filled in with a default `"type": "any"` field. + """ schema.clear_fields() for name in labels: diff --git a/frictionless/resources/table.py b/frictionless/resources/table.py index ca2d69e231..a3749fdd97 100644 --- a/frictionless/resources/table.py +++ b/frictionless/resources/table.py @@ -263,7 +263,9 @@ def __open_lookup(self): self.__lookup[source_name][source_key].add(cells) def __open_row_stream(self): - field_info = FieldsInfo(self.schema.fields) + fields_info = FieldsInfo( + self.schema.fields, self.labels, self.detector.schema_sync + ) # Create state memory_unique: Dict[str, Any] = {} @@ -296,7 +298,7 @@ def row_stream(): row = Row( cells, - field_info=field_info, + fields_info=fields_info, row_number=row_number, ) @@ -378,9 +380,9 @@ def row_stream(): if self.detector.schema_sync: # Missing required labels are not included in the - # field_info parameter used for row creation + # fields_info parameter used for row creation for field in self.schema.fields: - self.remove_missing_required_label_from_field_info(field, field_info) + self.remove_missing_required_label_from_field_info(field, fields_info) self.__row_stream = row_stream() @@ -415,7 +417,9 @@ def label_is_missing( def primary_key_cells(self, row: Row, case_sensitive: bool) -> Tuple[Any, ...]: """Create a tuple containg all cells from a given row associated to primary keys""" - return tuple(row[label] for label in self.primary_key_labels(row, case_sensitive)) + return tuple( + row[label] for label in self.primary_key_labels(row, case_sensitive) + ) def primary_key_labels( self, @@ -689,39 +693,63 @@ def __init__(self, field: Field, field_number: int): class FieldsInfo: - """Helper class to store additional data to a collection of fields + """Helper class for linking columns to schema fields. + + It abstracts away the different ways of making this link. In particular, the + reference may be the schema (`detector.schema_sync = False`), or the labels + (`detector.schema_sync = True`). This class is not Public API, and should be used only in non-public interfaces. """ - def __init__(self, fields: List[Field]): - self._fields: List[_FieldInfo] = [ - _FieldInfo(field, i + 1) for i, field in enumerate(fields) - ] + def __init__( + self, fields: List[Field], labels: Optional[List[str]], schema_sync: bool + ): + if schema_sync and labels: + self._expected_fields: List[_FieldInfo] = [] + if len(labels) != len(set(labels)): + note = '"schema_sync" requires unique labels in the header' + raise FrictionlessException(note) + + for label_index, label in enumerate(labels): + try: + field = next(f for f in fields if f.name == label) + except StopIteration: + field = Field.from_descriptor({"name": label, "type": "any"}) + self._expected_fields.append(_FieldInfo(field, label_index + 1)) + else: + self._expected_fields = [ + _FieldInfo(field, i + 1) for i, field in enumerate(fields) + ] def ls(self) -> List[str]: - """List all field names""" - return [fi.field.name for fi in self._fields] + """List all column names""" + return [fi.field.name for fi in self._expected_fields] def get(self, field_name: str) -> _FieldInfo: """Get a Field by its name + In case no field with field_name exists, the behavior depends on + the `detector.schema_sync` option: + Raises: - ValueError: Field with name fieldname does not exist + ValueError """ try: - return next(fi for fi in self._fields if fi.field.name == field_name) + return next( + fi for fi in self._expected_fields if fi.field.name == field_name + ) except StopIteration: - raise ValueError(f"'{field_name}' is not in fields data") + raise ValueError(f"{field_name} is missing from expected fields") def get_copies(self) -> List[Field]: """Return field copies""" - return [fi.field.to_copy() for fi in self._fields] + return [fi.field.to_copy() for fi in self._expected_fields] def rm(self, field_name: str): try: i = self.ls().index(field_name) - del self._fields[i] + del self._expected_fields[i] except ValueError: raise ValueError(f"'{field_name}' is not in fields data") diff --git a/frictionless/table/row.py b/frictionless/table/row.py index 74e7883c2f..b817b51759 100644 --- a/frictionless/table/row.py +++ b/frictionless/table/row.py @@ -39,11 +39,11 @@ def __init__( self, cells: List[Any], *, - field_info: FieldsInfo, + fields_info: FieldsInfo, row_number: int, ): self.__cells = cells - self.__fields_info = field_info + self.__fields_info = fields_info self.__row_number = row_number self.__processed: bool = False self.__blank_cells: Dict[str, Any] = {} @@ -65,7 +65,7 @@ def __repr__(self): def __setitem__(self, key: str, value: Any): try: field_number = self.__fields_info.get(key).field_number - except KeyError: + except ValueError: raise KeyError(f"Row does not have a field {key}") if len(self.__cells) < field_number: self.__cells.extend([None] * (field_number - len(self.__cells))) @@ -87,7 +87,7 @@ def __contains__(self, key: object): def __reversed__(self): return reversed(self.__fields_info.ls()) - def keys(self): + def keys(self): # type: ignore return iter(self.__fields_info.ls()) def values(self): # type: ignore From 8b72ae802f10799700809f528398818e2800df56 Mon Sep 17 00:00:00 2001 From: Pierre Camilleri Date: Wed, 29 Jan 2025 15:13:33 +0100 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=94=B5=20remove=20empty=20/=20unused?= =?UTF-8?q?=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frictionless/schema/fields_info.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 frictionless/schema/fields_info.py diff --git a/frictionless/schema/fields_info.py b/frictionless/schema/fields_info.py deleted file mode 100644 index 0dc1c83ba7..0000000000 --- a/frictionless/schema/fields_info.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import List - -from frictionless.schema.field import Field