Source code for linkml.validator.loaders.delimited_file_loader
import csv
import re
from abc import ABC, abstractmethod
from collections.abc import Iterator
from pathlib import Path
from linkml.validator.loaders.loader import Loader
def _parse_numeric(value: str):
if not isinstance(value, str) or not re.search(r"[0-9]", value):
return value
try:
return int(value)
except (TypeError, ValueError):
pass
try:
return float(value)
except (TypeError, ValueError, OverflowError):
return value
_NUMERIC_TYPE_NAMES = frozenset({"integer", "float", "double", "decimal"})
def _get_numeric_slots(schema_path: str | Path, target_class: str) -> set[str]:
"""Return column names whose schema range is a numeric type.
Only these columns should be passed through ``_parse_numeric``. All others
(string, enum, uri, date, custom string-derived types, etc.) are returned
as-is to avoid breaking validation.
Uses ``SchemaView.type_ancestors()`` to walk ``typeof`` chains, so custom
types like ``typeof: string`` are handled correctly.
"""
from linkml_runtime import SchemaView
sv = SchemaView(str(schema_path))
numeric_slots: set[str] = set()
all_types = sv.all_types()
for slot in sv.class_induced_slots(target_class):
if slot.range in all_types:
ancestors = sv.type_ancestors(slot.range)
if any(a in _NUMERIC_TYPE_NAMES for a in ancestors):
numeric_slots.add(slot.name)
if slot.alias:
numeric_slots.add(slot.alias)
return numeric_slots
class _DelimitedFileLoader(Loader, ABC):
"""Base class for TSV and CSV loaders"""
@property
@abstractmethod
def delimiter(self):
pass
def __init__(
self,
source,
*,
skip_empty_rows: bool = False,
index_slot_name: str | None = None,
schema_path: str | Path | None = None,
target_class: str | None = None,
) -> None:
super().__init__(source)
self.skip_empty_rows = skip_empty_rows
self.index_slot_name = index_slot_name
# None means "no schema provided" → coerce everything (backward compat)
# An empty set means "schema provided but no numeric slots" → coerce nothing
self._numeric_slots: set[str] | None = (
_get_numeric_slots(schema_path, target_class)
if schema_path is not None and target_class is not None
else None
)
def _coerce_value(self, key: str, value: str):
"""Return *value* coerced to the appropriate Python type.
When schema info is available, only columns with numeric ranges go
through ``_parse_numeric``; everything else is returned as-is.
Without schema info (``_numeric_slots is None``), all columns are
coerced for backward compatibility.
"""
if self._numeric_slots is not None and key not in self._numeric_slots:
return value
return _parse_numeric(value)
def _rows(self) -> Iterator[dict]:
with open(self.source) as file:
reader: csv.DictReader = csv.DictReader(file, delimiter=self.delimiter, skipinitialspace=True)
for row in reader:
if self.skip_empty_rows and not any(row.values()):
continue
yield {k: self._coerce_value(k, v) for k, v in row.items() if k is not None and v != ""}
def iter_instances(self) -> Iterator[dict]:
if self.index_slot_name is not None:
yield {self.index_slot_name: list(self._rows())}
else:
yield from self._rows()
[docs]
class CsvLoader(_DelimitedFileLoader):
"""A loader for instances serialized as CSV
:param skip_empty_rows: If ``True``, skip empty rows instead of yielding empty dicts. Defaults
to ``False``.
:param index_slot_name: If provided, ``iter_instances`` will yield one dict where all rows of
the CSV file are collected into a list with ``index_slot_name`` as the key. If ``None``,
``iter_instances`` will yield each row as a dict individually. Defaults to ``None``.
"""
@property
def delimiter(self):
return ","
[docs]
class TsvLoader(_DelimitedFileLoader):
"""A loader for instances serialized as TSV
:param skip_empty_rows: If ``True``, skip empty rows instead of yielding empty dicts. Defaults
to ``False``.
:param index_slot_name: If provided, ``iter_instances`` will yield one dict where all rows of
the TSV file are collected into a list with ``index_slot_name`` as the key. If ``None``,
``iter_instances`` will yield each row as a dict individually. Defaults to ``None``.
"""
@property
def delimiter(self):
return "\t"