Reputation: 73
I'm trying to avoid having a bunch of if
s or assert
s in the code using this class.
class TemplateRow(tp.NamedTuple):
"""Parsed template row, if not error."""
template: Template | None = None
error: str | None = None
@property
def valid(self) -> bool:
"""Determine if this row is valid.
Returns:
bool: is this template row valid
"""
return self.error is None
def read_template(filename: str | Path) -> tp.Iterator[TemplateRow]:
"""Read a TSV file into a list of templates.
Args:
filename (str): Path to the TSV file
Yields:
TemplateRow : parsed template row, or error message
"""
with open(filename, "r", encoding="utf-8") as f:
reader = csv.DictReader(f, dialect=csv.excel_tab)
for row in reader:
try:
yield TemplateRow(template=Template.parse_obj(row))
except pydantic.ValidationError as e:
yield TemplateRow(error=str(e))
So, I read in a TSV and try to convert it to a Pydantic model with a bunch of validators. If it works, I return the filled-in model. If it fails, I return the validation error string
To make the caller be able to just expect one type, I return a NamedTuple which can contain the parsed model instance, or an error string.
There's a simple property to say if it's valid, mainly for readability and for a possible future enhancement.
Anyway, if I have a loop such as:
ok: list[Template]=[]
notok: list[str] = []
for row in read_template(file):
if row.valid:
ok.append(row.template)
else:
notok.append(row.error)
I will get mypy errors that row.error might be None
As I've already checked the .valid
property, it can't be.
Is there anyway to communicate this to type checkers?
thanks
Upvotes: 0
Views: 63
Reputation: 18751
I don't understand the restriction of having the iterator yield only one type.
Why not just do it like this?
import csv
from collections.abc import Iterator
from pathlib import Path
from pydantic import BaseModel, ValidationError
class Template(BaseModel):
foo: str
bar: int
def read_template(filename: str | Path) -> Iterator[Template | ValidationError]:
with open(filename, "r", encoding="utf-8") as f:
reader = csv.DictReader(f, dialect=csv.excel_tab)
for row in reader:
try:
yield Template.parse_obj(row)
except ValidationError as e:
yield e
def main() -> None:
ok: list[Template] = []
notok: list[str] = []
for obj in read_template("path/to/file.tsv"):
if isinstance(obj, Template):
ok.append(obj)
else:
notok.append(str(obj))
...
This seems to accomplish the same thing without the need for TemplateRow
. If you have a good reason for that restriction, please elaborate.
Upvotes: 1
Reputation: 3439
Instead of a TypeGuard
, you could actually define two types - ValidTemplateRow
and InvalidTemplateRow
. Then, simply use isinstance
to check for either.
class ValidTemplateRow(tp.NamedTuple):
template: Template
class InvalidTemplateRow(tp.NamedTuple):
error: str
def read_template(filename: str | Path) -> tp.Iterator[InvalidTemplateRow | ValidTemplateRow]:
"""Read a TSV file into a list of templates.
Args:
filename (str): Path to the TSV file
Yields:
TemplateRow : parsed template row, or error message
"""
with open(filename, "r", encoding="utf-8") as f:
reader = csv.DictReader(f, dialect=csv.excel_tab)
for row in reader:
try:
yield ValidTemplateRow(template=Template.parse_obj(row))
except pydantic.ValidationError as e:
yield InvalidTemplateRow(error=str(e))
ok: list[Template] = []
notok: list[str] = []
for row in read_template("file"):
if isinstance(row, ValidTemplateRow):
ok.append(row.template)
else:
notok.append(row.error)
Upvotes: 2