77from .. import errors , helpers , types
88from ..exception import FrictionlessException
99from ..schema import Field
10+ from .label_matching import LabelMatching
1011
1112# The `fieldsMatch` modes are told apart by which mismatch they tolerate: a
1213# label with no matching field, or a declared field with no matching label
@@ -25,7 +26,7 @@ class Header(List[str]): # type: ignore
2526 > Constructor of this object is not Public API
2627
2728 > Deprecated: using a `Header` directly as a list is deprecated, as the list
28- > ambiguously holds the names of the *schema* fieldss . Use its properties instead:
29+ > ambiguously holds the names of the *schema* fields . Use its properties instead:
2930 > `labels` for the header row as read from the data source, `fields`/`field_names`
3031 > for the schema fields.
3132
@@ -64,6 +65,7 @@ def __init__(
6465 self .__labels = labels
6566 self .__errors : List [errors .HeaderError ] = []
6667 self .__expected_fields : Optional [List [Field ]] = None
68+ self .__matching = LabelMatching (labels , self .__fields , ignore_case = ignore_case )
6769 self .__process ()
6870
6971 # Deprecated
@@ -201,7 +203,7 @@ def get_expected_fields(self) -> List[Field]:
201203
202204 expected : List [Field ] = []
203205 for label in self .__labels :
204- field = self .__find_field_by_name (label )
206+ field = self .__matching . matching_field (label )
205207 if field is None :
206208 field = Field .from_descriptor ({"name" : label , "type" : "any" })
207209 expected .append (field )
@@ -233,7 +235,7 @@ def _get_extra_labels(self) -> List[Tuple[int, str]]:
233235 return [
234236 (number , label )
235237 for number , label in enumerate (labels , start = 1 )
236- if self .__find_field_by_name (label ) is None
238+ if self .__matching . matching_field (label ) is None
237239 ]
238240
239241 def _get_missing_fields (self ) -> List [Tuple [int , Field ]]:
@@ -257,39 +259,19 @@ def _get_missing_fields(self) -> List[Tuple[int, Field]]:
257259 if not self .__matches_by_name :
258260 missing = fields [len (labels ) :] if len (fields ) > len (labels ) else []
259261 else :
260- normalized_labels = [self .__normalize (label ) for label in labels ]
261-
262- def is_absent (field : Field ) -> bool :
263- return self .__normalize (field .name ) not in normalized_labels
264262
265263 def is_required (field : Field ) -> bool :
266264 return field .required or (
267265 field .schema is not None and field .name in field .schema .primary_key
268266 )
269267
270- missing = [ field for field in fields if is_absent ( field )]
268+ missing = self . __matching . unmatched_fields
271269 if self .__fields_match in TOLERATES_MISSING_FIELDS :
272270 missing = [field for field in missing if is_required (field )]
273271
274272 start = len (labels ) + 1
275273 return [(start + offset , field ) for offset , field in enumerate (missing )]
276274
277- def __has_matching_field (self ) -> bool :
278- """Whether at least one label corresponds to a schema field"""
279- return any (
280- self .__find_field_by_name (label ) is not None for label in self .__labels
281- )
282-
283- def __find_field_by_name (self , name : str ) -> Optional [Field ]:
284- target = self .__normalize (name )
285- for f in self .__fields :
286- if self .__normalize (f .name ) == target :
287- return f
288- return None
289-
290- def __normalize (self , s : str ) -> str :
291- return s .lower () if self .__ignore_case else s
292-
293275 # Convert
294276
295277 def to_str (self ):
@@ -330,11 +312,7 @@ def __process(self):
330312 )
331313
332314 # Unmatched header
333- if (
334- self .__fields_match == "partial"
335- and fields
336- and not self .__has_matching_field ()
337- ):
315+ if self .__fields_match == "partial" and fields and not self .__matching .has_match :
338316 self .__errors .append (
339317 errors .UnmatchedHeaderError (
340318 note = "" ,
0 commit comments